Оптимизатор конкурирующего роя — Competitive Swarm Optimizer (CSO)
Содержание
Введение
Подбор параметров торговой системы — задача, с которой сталкивается каждый, кто работает с алгоритмическим трейдингом. Метаэвристики справляются с ней неплохо на старте: за первые итерации популяция быстро находит приличное решение. Но дальше начинается знакомая картина — алгоритм "прилипает". Агенты стягиваются к глобальному лидеру, разнообразие популяции резко падает, и оставшийся бюджет прогонов расходуется впустую: улучшений нет, а выбраться из локального экстремума уже нечем.
Причина этой патологии в классическом 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. Интуиция здесь проста: в высоких размерностях агентам труднее самостоятельно найти верное направление, и коллективный центр роя служит полезным ориентиром; в низких размерностях его притяжение лишь мешает свободному исследованию.
Рисунок 1. Иллюстрация работы алгоритма CSO
Иллюстрация состоит из четырёх блоков, идущих по логике алгоритма:
- Step 1 Initialization — шесть агентов разбросаны случайно, скорости равны нулю.
- Step 2 Swarm Center — те же агенты, пунктирные спицы сходятся к центру роя x̄.
- Step 3 Pairing & Contest — три пары, победители выделены зелёным с галочкой, проигравшие красным.
- Step 4 Loser Velocity Update — конкретный пример агента A: зелёная стрелка тянет к победителю D, жёлтая — к центру, синяя итоговая стрелка показывает новую позицию A′; рядом формула с цветовой разбивкой трёх слагаемых.
Полный цикл работы алгоритма:
ПОКА бюджет вычислений не исчерпан:
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 последний агент в текущем эпохе не участвует в соревновании.
Метод "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, но и трекеры лучших решений на каждом запуске фитнес-функции. Ниже разберем код реализации трехмерного стенда.

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

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

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

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

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, сохранив при этом соревновательную схему формирования пар. Исходный код и протокол тестирования открыты для воспроизведения и дальнейших исследований.
Рисунок 2. Цветовая градация алгоритмов по соответствующим тестам

Рисунок 3. Гистограмма результатов тестирования алгоритмов (по шкале от 0 до 100, чем больше, тем лучше, где 100 — максимально возможный теоретический результат, в архиве скрипт для расчета рейтинговой таблицы)
Плюсы и минусы алгоритма CSO
Плюсы:
- Один дополнительный параметр кроме популяции.
- Простая и понятная концепция.
Минусы:
- Заметных минусов не выявлено.
К статье прикреплён архив с актуальными версиями кодов алгоритмов. Автор статьи не несёт ответственности за абсолютную точность в описании канонических алгоритмов, во многие из них были внесены изменения для улучшения поисковых возможностей. Выводы и суждения, представленные в статьях, основываются на результатах проведённых экспериментов.
Программы, используемые в статье
| # | Имя | Тип | Описание |
|---|---|---|---|
| 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 |
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Преодоление ограничений машинного обучения (Часть 7): Автоматический выбор стратегии
Нейросети в трейдинге: Оптимизация Cross-Attention для анализа длинных последовательностей рынка (Окончание)
Возможности Мастера MQL5, которые вам нужно знать (Часть 73): Использование паттернов Ишимоку и ADX-Wilder
Автоматизация торговых стратегий на MQL5 (Часть 22): Создание системы зонального восстановления для трендовой торговли по индикатору Envelopes
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования

