preview
Алгоритм Цветовой Гармонии — Color Harmony Algorithm (CHA)

Алгоритм Цветовой Гармонии — Color Harmony Algorithm (CHA)

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

Содержание

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


Введение

Алгоритм цветовой гармонии (Color Harmony Algorithm, CHA), предложенный исследователями Заэйми и Годдосианом в 2020 году, обращается к источнику инспирации, который кажется слишком далёким от вычислительной природы задачи. На первый взгляд связь между правилами Манселла о сочетании цветов в живописи и численной оптимизацией функций выглядит надуманной. Но если разобраться, метафора оказывается не столько украшением, сколько структурным каркасом всего алгоритма — и именно эта структура делает его интересным объектом для разбора.

В стандартных метаэвристиках популяция — это «толпа», в которой все решения равноправны и взаимодействуют по одинаковым правилам. CHA вводит явную иерархию: популяция организована в круг тонов на 100 секторов в 10 цветовых группах, в каждой группе один лидер-агент, между группами действуют правила гармоничного сочетания. Кандидаты строятся не как мутации одиночных решений, а как цветовые комбинации — линейные смеси по строго определённым правилам отбора родителей.

С точки зрения трейдера или исследователя, выбирающего метаэвристику для своей задачи, у CHA есть две привлекательные особенности. Первая — заведомо структурированная популяция: алгоритм не сваливается в одну точку из-за случайной кластеризации, потому что геометрия круга тонов поддерживает разнообразие по построению. Вторая — явное переключение между фазами концентрации (эксплуатация) и рассеивания (исследование) по понятному критерию, а не через скрытые гиперпараметры.

Однако эти достоинства имеют цену. К концу статьи у читателя будет полная картина: каркас алгоритма, рабочая реализация на MQL5 в рамках стандартного фреймворка C_AO, объективные результаты на стандартизированном бенчмарке и понимание, в каких задачах CHA имеет смысл применять, а в каких лучше выбрать что-то другое.


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

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

Геометрия алгоритма строится на цветовом круге Манселла, разделённом на 100 секторов. Эти секторы сгруппированы в 10 «цветовых групп» по 10 секторов каждая (красный, оранжевый, жёлтый и так далее). В каждой группе есть один особый сектор — чистейший (purest), на котором располагается лучшее решение группы, называемое агентом. Чистейшие секторы имеют номера 5, 15, 25, ..., 95 — это центры групп. Остальные 90 решений популяции распределены по оставшимся 90 секторам.

Работа алгоритма состоит из двух основных фаз, которые чередуются по мере необходимости.

Фаза концентрации — поиск лучших решений через комбинирование уже найденных. Алгоритм берёт пары решений, выбранные по правилам гармонии, и строит между ними линейные комбинации с возмущением. Это аналог того, как художник смешивает сочетающиеся краски, надеясь получить новый перспективный оттенок. Например, если у нас есть хорошее красное решение и хорошее синее решение, мы пробуем «фиолетовое» между ними, и если оно лучше любого из родителей — оно занимает место в популяции.

Фаза рассеивания — введение разнообразия. Когда популяция становится слишком кучной (все решения слишком похожи), алгоритм заменяет несколько ближайших к центру решений новыми, взятыми из «цветовой памяти» — хранилища ранее найденных перспективных, но не использованных решений. Это похоже на то, как художник может добавить неожиданный цвет на палитру, чтобы вырваться из заигранной гаммы.

Переключение между фазами регулируется измеренным разнообразием популяции Dpop и текущим порогом Dth. Если Dpop < Dth, следующая итерация идёт по фазе рассеивания, после чего порог Dth сжимается умножением на коэффициент damp (обычно 0.5 или 0.96). Со временем порог становится всё более строгим, и алгоритм фокусируется на всё более точной локализации оптимума.


Шаблоны гармонии

Гармонические цвета в теории Манселла — это не произвольные пары, а сочетания, попадающие в определённые геометрические шаблоны на круге тонов; авторы статьи отобрали пять таких шаблонов:

  • X-тип: две дуги по 26% круга, расположенные на 180° друг от друга
  • V-тип: одна дуга 26%
  • Y-тип: малая дуга 5% и большая дуга 26% на расстоянии 180°
  • T-тип: одна большая дуга 50%
  • L-тип: малая дуга 5% и средняя дуга 22% на расстоянии 90°

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


Правила построения комбинаций

В каждой фазе концентрации каждый из 10 агентов порождает Ncomb комбинаций (по умолчанию 4). Тип каждой комбинации определяется параметром PA (Power of Agent), который растёт от 0 до Ncomb по мере прогресса алгоритма. Чем больше PA — тем сильнее эксплуатация, тем больше комбинаций включают агентов как родителей.

Различаются три типа комбинаций:

  • Тип (i) — якорный агент комбинируется с не-агентом из гармоничных секторов. Это локальный эксплуатационный механизм: лидер группы тянет лучшие из своих рядовых членов в свою сторону.
  • Тип (ii) — глобально лучший агент комбинируется с якорным агентом. Это мост между группами: отстающая группа может «зацепиться» за достижения лидера через своего агента.
  • Тип (iii) — два не-агента из гармоничных секторов. Чисто исследовательский механизм, не привязанный к лидерам.

Комбинация строится по формуле:

X_new = r1·X_a + r2·X_b + perturbation,

где r1 + r2 = 1, r1 берётся равномерно из [0.1, 0.9] (исключаем вырожденные случаи, когда кандидат почти совпадает с одним из родителей), а perturbation — гауссово возмущение, амплитуда которого плавно убывает с течением алгоритма от 10% до 1% диапазона по координате.

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

На гладких унимодальных функциях из оригинальной статьи это не проблема: оптимум обычно внутри начального облака. На функциях с резкими пиками вне начального облака (Forest, Megacity бенчмарка) это блокирует алгоритм полностью. Возмущение — минимальное расширение, позволяющее кандидатам выходить за пределы выпуклой оболочки.


Фаза рассеивания

Когда Dpop < Dth, запускается фаза рассеивания. Алгоритм определяет центр популяции (среднее по координатам), находит Ns ближайших к центру решений (исключая агентов — они никогда не вытесняются) и заменяет их новыми цветами по формуле: X_new = rcm·X_CM + (1 - rcm)·X_old,

где X_CM — точка из цветовой памяти, rcm ∈ [rcm0, 1] (по умолчанию rcm0 = 0.7, то есть новый цвет в основном определяется памятью, но сохраняет некоторую связь со старой позицией).

Цветовая память (Color Memory, CM) — это хранилище из Ksets наборов по Ns точек каждый. В неё попадают комбинации, которые были построены, но не приняли в популяцию (потому что оказались хуже своих слотов в фазе концентрации). Идея в том, что отвергнутые в одной фазе кандидаты могут оказаться полезными в фазе рассеивания: они уже представляют какое-то разнообразие, не сводящееся к текущему центру масс.

cha_fig1

Рисунок 1. Цветовой тональный круг алгоритма.

Hue Circle, 10 крупных закрашенных лепестков-групп с цветовыми именами (Red, Orange, ..., Magenta) вокруг. Тонкие штришки за пределами круга показывают границы 100 секторов. Чистейшие сектора видны как точки агентов: красные обводные кружки с белой заливкой. Внутри круга — мелкие белые точки (обычные решения), в самом центре — жёлтый кружок (глобальный лучший).

cha_fig2_templates

Рисунок 2. Пять мини-кругов

Five Harmony Templates. Пять мини-кругов, каждый с тёмными «серыми дугами», как в статье. Анкорный агент (красная точка) сверху, при переходе глазами между шаблонами легко сравнивать. Размеры дуг точно по Appendix A: X — две по 26%, V — одна 26%, Y — 5%+26%, T — одна 50%, L — 5%+22%. Снизу пояснение про размеры и углы.

cha_fig3_phases

Рисунок 3. Фазы работы алгоритма

Two Phases. Слева Concentration: два родителя (красный агент + серый не-агент), пунктирная линия между ними, на ней — точка линейной комбинации, вокруг неё голубое облако возмущения с кандидатами. Справа Dispersion: чёрный квадрат — центр популяции, серые круги вокруг — ближайшие к центру (заменяемые), фиолетовые ромбы — точки из памяти CM, фиолетовые стрелки показывают прыжок. Красные большие круги — агенты на отдалении, не затрагиваются. Снизу легенда на 5 типов символов.

Составим общий псевдокод алгоритма:

1. Инициализация
   1.1. Установить параметры (popSize=50, Ncomb=4, Ns=10, Ksets=4,
        damp=0.5, Dthf=0.01, rcm0=0.7)
   1.2. Установить пороги разнообразия:
        Dthi = 0.5 × max(rangeMax - rangeMin)
        Dth_cur = Dthi
        N_Dth = floor(log(Dthf/Dthi) / log(damp))
   1.3. Сгенерировать начальную популяцию равномерно в [rangeMin, rangeMax]
   1.4. Оценить FF, разместить top-10 на чистейших секторах как агентов,
        остальных — равномерно по группам

2. Основной цикл
   2.1. Если Dpop < Dth_cur:
        запустить фазу рассеивания
        Dth_cur ← Dth_cur × damp
        iter_dp ← iter_dp + 1
        iter_cp ← 0
   2.2. Иначе:
        запустить фазу концентрации
        iter_cp ← iter_cp + 1
   2.3. Обновить лучшее решение fB, cB
   2.4. Если не достигнут критерий останова — на шаг 2.1

3. Фаза концентрации
   3.1. Рассчитать PA по формулам (8)-(10) Заэйми и Годдосиана
   3.2. Для каждого из 10 агентов:
        - перебрать 5 шаблонов гармонии
        - для каждого собрать множество H гармоничных решений
        - сгенерировать Ncomb комбинаций (типы i/ii/iii по PA)
        - выбрать набор шаблона с максимальным разнообразием
   3.3. Разместить кандидатов в слоты с подходящими секторами родителей
   3.4. Оценить FF
   3.5. Greedy-селекция: каждый слот сравнивает кандидата со своим
        snapshot, лучший побеждает
   3.6. Отвергнутых — в CM_temp
   3.7. Переназначить агентов групп

4. Фаза рассеивания
   4.1. Найти центр популяции
   4.2. Найти Ns ближайших к центру не-агентов
   4.3. Источник SCM:
        если CM заполнена — взять самый разнообразный набор
        иначе — случайные точки из CM_temp или случайные в пространстве
   4.4. Сгенерировать новые цвета по формуле Манселла
   4.5. Записать SCM в основную CM (заменив самый бедный по разнообразию набор)

Переходим к реализации. S_CHA_Vector. Это техническая обёртка над одномерным динамическим массивом v[]. Метод Init принимает размерность задачи и резервирует под неё внутренний массив v. Вся подмена двумерности происходит на уровне обращения: вместо X[i][c] пишется X[i].v[c]. Структура используется в трёх местах класса — в snapshot популяции (snap_c[]), в буфере цветовой памяти (CMtempC[]) и в полях points структуры S_CHA_CMSet.

//+------------------------------------------------------------------+
struct S_CHA_Vector
 {
  double  v          [];

  void               Init(int dim)
   {
    ArrayResize(v, dim);
   }
 };
//+------------------------------------------------------------------+

S_CHA_NewColor. Запись для одного кандидата фазы концентрации. Помимо координат и фитнеса, в структуре сохраняются индексы секторов круга тонов, в которых сидели родители комбинации (sec_a и sec_b). Эти сектора нужны на этапе размещения кандидата в слот популяции: по правилу второго типа из статьи Заэйми и Годдосиана, новый цвет должен попадать на сектор одного из своих компонентов. Метод Init инициализирует массив координат, фитнес выставляется в -DBL_MAX (минимум для задачи максимизации), индексы секторов помечаются как -1 (не назначен).

//+------------------------------------------------------------------+
//| Запись для одного кандидата фазы концентрации:                   |
//| координаты + индексы родителей по секторам круга тонов.          |
//+------------------------------------------------------------------+
struct S_CHA_NewColor
 {
  double  c          [];      // координаты кандидата
  double             f;       // фитнес (заполняется ВНЕ Moving — извне)
  int                sec_a;   // сектор первого  родителя (0..99)
  int                sec_b;   // сектор второго  родителя (0..99)

  void               Init(int dim)
   {
    ArrayResize(c, dim);
    f = -DBL_MAX;
    sec_a = -1;
    sec_b = -1;
   }
 };

S_CHA_CMSet. Один набор плохих, но разнообразных цветов в основной памяти алгоритма. Поле points — массив из Ns обёрток S_CHA_Vector, каждая длины равной размерности задачи. Поле diversity хранит заранее вычисленное разнообразие набора относительно центра пространства поиска — это позволяет в фазе рассеивания быстро находить самый бедный по разнообразию набор для замены, не пересчитывая метрику каждый раз. Метод Init принимает число точек в наборе и размерность задачи, выделяет память и обнуляет diversity.

//+------------------------------------------------------------------+
//| Один набор «плохих, но разнообразных» цветов в основной памяти.  |
//+------------------------------------------------------------------+
struct S_CHA_CMSet
 {
  S_CHA_Vector points [];   // [Ns] обёрток, каждая длины dim
  double             diversity;   // разнообразие набора относительно центра поиска

  void               Init(int n_pts, int dim)
   {
    ArrayResize(points, n_pts);
    for(int i = 0; i < n_pts; i++)
      points [i].Init(dim);
    diversity = 0.0;
   }
 };

Класс C_AO_CHA наследуется от стандартного C_AO и реализует интерфейс Init / Moving / Revision. Сам алгоритм разбит на шесть базовых подпроцедур, каждая из которых отвечает за свой блок логики: размещение на круге тонов после первой оценки фитнеса, расчёт Power of Agent, генерацию и применение фаз концентрации и рассеивания, обновление лидеров групп. Эти подпроцедуры приватны и вызываются из методов-диспетчеров Moving и Revision.

Внутреннее состояние. Счётчики iter_cp и iter_dp отслеживают число итераций фазы концентрации (с момента последнего рассеивания) и общее число выполненных фаз рассеивания соответственно, iter_cp сбрасывается в ноль каждый раз, когда выполняется фаза рассеивания — это важно для корректного расчёта Power of Agent. Параметры Dthi (начальный порог) и Dth_cur (текущий порог) определяют момент переключения между фазами; N_Dth — оценка ожидаемого числа фаз рассеивания за весь прогон, нужная для формулы Power of Agent. Поле PA хранит текущее значение этого параметра, перерасчитываемое перед каждой фазой концентрации. Флаг nextIsDispersion решает, какая фаза будет запущена в следующем вызове Moving.

Геометрия круга тонов хранится в двух массивах, sectorOf[] для каждой особи популяции содержит её сектор от 0 до 99; группа определяется как sectorOf[i] / 10. agentIdx[10] для каждой группы хранит индекс её агента в массиве a[], или −1, если агент в группе не назначен.

Буферы фазы концентрации. Кандидаты фазы концентрации хранятся отдельно от популяции, в массиве candidates[] размера 10·Ncomb. В момент генерации кандидаты ещё не привязаны к слотам популяции — только после построения и записи родительских секторов алгоритм решает, в какой слот их разместить для оценки FF. Флаг candidateUsed[] помечает кандидатов, принятых в популяцию по результатам greedy селекции; те, кто остался непринятым, отправляются во временную цветовую память CMtempC[].

Snapshot популяции (snap_c, snap_f, snap_sectorOf) делается в начале ConcentrationPhase_Generate и используется в двух целях. Во-первых, он сохраняет старые координаты слотов на случай, если кандидат, временно помещённый в слот для оценки фитнеса, окажется хуже старого содержимого слота — тогда слот откатывается из snapshot. Во-вторых, snapshot фитнесов и секторов используется во всех правилах круга, чтобы не допустить путаницы между «состоянием до Moving» и «состоянием с временно помещённым кандидатом».

Связь между слотом популяции и индексом кандидата хранится в slotCandidate[]: если в слоте сидит кандидат, там его индекс в candidates[], иначе −1. Параллельный булев массив isCandidate[] нужен для быстрой проверки «занят ли слот кандидатом» без сравнения с −1.

Цветовая память. Основная память CM[] имеет ёмкость Ksets наборов, каждый по Ns точек. Поле CMcount показывает, сколько наборов реально записано — пока CMcount < Ksets, новые наборы добавляются; после заполнения наименее разнообразный набор перезаписывается новым, если у того разнообразие больше. Временная память CMtempC[] — это плоский кольцевой буфер ёмкости Ksets·Ns, в который складываются непринятые кандидаты фазы концентрации. Счётчик CMtempCnt инкрементируется при каждой записи; при достижении удвоенной ёмкости он зажимается обратно в CMtempCap, чтобы не разрастался без ограничений. В фазе рассеивания, если CM ещё не заполнена, источником разнообразных точек служит именно CMtempC.

//+------------------------------------------------------------------+
//|                       Color Harmony Algorithm                    |
//+------------------------------------------------------------------+
class C_AO_CHA : public C_AO
 {
public:
                    ~C_AO_CHA() {}
                     C_AO_CHA()
   {
    ao_name = "CHA";
    ao_desc = "Color Harmony Algorithm";
    ao_link = "https://www.mql5.com/ru/articles/18132";

    popSize = 50;
    Ncomb   = 4;       // комбинаций на шаблон (на агента)
    Ns      = 10;      // замен в фазе рассеивания
    Ksets   = 4;       // число наборов в CM
    damp    = 0.5;     // коэффициент сжатия порога разнообразия
    Dthf    = 0.01;    // финальный порог разнообразия
    rcm0    = 0.7;     // нижняя граница доли вклада CM в фазе рассеивания

    ArrayResize(params, 7);
    params [0].name = "popSize"; params [0].val = popSize;
    params [1].name = "Ncomb";   params [1].val = Ncomb;
    params [2].name = "Ns";      params [2].val = Ns;
    params [3].name = "Ksets";   params [3].val = Ksets;
    params [4].name = "damp";    params [4].val = damp;
    params [5].name = "Dthf";    params [5].val = Dthf;
    params [6].name = "rcm0";    params [6].val = rcm0;
   }

  void               SetParams()
   {
    popSize = (int)params [0].val;
    Ncomb   = (int)params [1].val;
    Ns      = (int)params [2].val;
    Ksets   = (int)params [3].val;
    damp    = params      [4].val;
    Dthf    = params      [5].val;
    rcm0    = params      [6].val;

    //--- предохранители 
    if(popSize < 20)
      popSize = 20;          // минимум 10 агентов + 10 не-агентов
    if(Ncomb   < 1)
      Ncomb   = 1;
    if(Ncomb * 10 > popSize - 10)
      Ncomb   = (popSize - 10) / 10;
    if(Ns      < 1)
      Ns      = 1;
    if(Ns      > popSize - 10)
      Ns      = popSize - 10;
    if(Ksets   < 1)
      Ksets   = 1;
    if(damp    <= 0 || damp >= 1)
      damp    = 0.5;
    if(Dthf    <= 0)
      Dthf    = 0.01;
    if(rcm0    < 0 || rcm0 >= 1)
      rcm0    = 0.7;
   }

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

  void               Moving();
  void               Revision();

  //--- видимые параметры 
  int                Ncomb;  // комбинаций на шаблон (на агента)
  int                Ns;     // замен в фазе рассеивания
  int                Ksets;  // число наборов в CM
  double             damp;   // коэффициент сжатия порога разнообразия
  double             Dthf;   // финальный порог разнообразия
  double             rcm0;   // нижняя граница доли вклада CM в фазе рассеивания

private: //---
  //--- глобальное состояние алгоритма 
  int                iter_cp;       // итераций фазы концентрации подряд
  int                iter_dp;       // итераций фазы рассеивания (= число обновлений Dth)
  int                N_Dth;         // floor(log(Dthf/Dthi)/log(damp))
  double             Dthi;          // начальный порог разнообразия
  double             Dth_cur;       // текущий порог
  int                PA;            // power of agent на текущей итерации концентрации
  bool               nextIsDispersion; // флаг: следующее Moving — фаза рассеивания

  //--- круг тонов 
  // sectorOf [i] ∈ [0..99] — сектор круга для i-й особи в a[].
  // Сектора 5,15,25,...,95 — «чистейшие», на них размещаются агенты.
  // Группа определяется как sectorOf [i] / 10 (0..9).
  int    sectorOf    [];          // [popSize]

  // agentIdx [g] — индекс в a[] агента группы g (0..9). −1 если не задан.
  int    agentIdx    [10];

  //--- буфер кандидатов фазы концентрации 
  // 10·Ncomb кандидатов от всех 10 агентов. Хранятся ОТДЕЛЬНО от a[],
  // потому что на этапе генерации мы ещё не знаем, в какой слот
  // каждого посадить — это решается в Revision по правилам круга.
  // На момент Moving() кандидаты "временно" размещаются в слотах
  // не-агентов для оценки FF; в Revision разбираются по правилам.
  S_CHA_NewColor candidates [];     // [10·Ncomb]
  bool           candidateUsed [];  // [10·Ncomb], true = принят на круг

  //--- snapshot текущей популяции на время фазы концентрации 
  // Нужен потому, что Moving затирает координаты не-агентов
  // кандидатами. Если кандидат отвергнут — слот восстанавливается
  // из snapshot.
  S_CHA_Vector snap_c        [];   // [popSize] обёрток, каждая длины coords
  double       snap_f        [];   // [popSize]
  int          snap_sectorOf [];   // [popSize]

  //--- связь "слот в a[] → индекс кандидата" во время фазы
  // -1 если слот не занят кандидатом (агент или свободный неагент).
  int     slotCandidate [];         // [popSize]
  bool    isCandidate   [];         // [popSize]

  //--- цветовая память 
  S_CHA_CMSet  CM     [];       // [<= Ksets]
  int                CMcount;         // фактический размер CM

  // Временная память — плоский буфер кандидатов, не попавших в круг
  S_CHA_Vector CMtempC [];      // [<= Ksets*Ns] обёрток, каждая длины coords
  int                CMtempCnt;
  int                CMtempCap;

  //--- внутренние подпроцедуры 
  bool               InitialPopulationWithDiversity();
  void               PlaceOnHueCircle();                     // после первичной оценки FF

  void               ConcentrationPhase_Generate();          // в Moving()
  void               ConcentrationPhase_UpdateCircle();      // в Revision()

  void               DispersionPhase_Generate();             // в Moving()
  void               DispersionPhase_UpdateCircle();         // в Revision()

  void               ComputePA();                            // PA_t по формулам (8)–(10)
  void               UpdateAgentsByGroup();                  // лучший в группе → агент

  //--- утилиты 
  double             DiversityPopulation();
  void               PopulationCenter(double &out []);

  // Шаблоны: для каждого агента возвращает массив секторов
  // (0..99), попадающих в серые дуги шаблона при повороте под этого агента.
  // templateType: 0=X, 1=V, 2=Y, 3=T, 4=L
  void               GreySectorsForTemplate(int templateType,
      int anchorSector,
      int &out []);

  int                GroupOf(int sector) { return sector / 10; }
  int                PurestSector(int group)  { return group * 10 + 5; }
 };
//+------------------------------------------------------------------+

Init— главная подготовительная процедура. После стандартной инициализации базового класса вычисляется максимальный диапазон по координатам и устанавливается начальный порог разнообразия Dthi = 0.5 · max(rangeMax − rangeMin). Затем по формуле N_Dth = floor(log(Dthf/Dthi) / log(damp)) оценивается ожидаемое число фаз рассеивания за весь прогон — это число используется в формулах Power of Agent. Все счётчики обнуляются, флаг nextIsDispersion сбрасывается в false. Затем выделяется память под все буферы: круг тонов, кандидаты фазы концентрации, snapshot популяции, связи слот-кандидат, основная и временная цветовая память. В завершение вызывается InitialPopulationWithDiversity для генерации начальной популяции.

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

//--- начальный порог разнообразия Dthi = 0.5 · max(rangeMax − rangeMin) 
  double maxRange = 0.0;
  for(int c = 0; c < coords; c++)
   {
    double r = rangeMax [c] - rangeMin [c];
    if(r > maxRange)
      maxRange = r;
   }
  Dthi    = 0.5 * maxRange;
  Dth_cur = Dthi;

//--- N_Dth = floor( log(Dthf/Dthi) / log(damp) )
  if(Dthi > 0.0 && Dthf > 0.0 && damp > 0.0 && damp != 1.0)
    N_Dth = (int)MathFloor(MathLog(Dthf / Dthi) / MathLog(damp));
  else
    N_Dth = 1;
  if(N_Dth < 1)
    N_Dth = 1;

//--- счётчики и флаги 
  iter_cp = 0;
  iter_dp = 0;
  PA      = 0;
  nextIsDispersion = false;

//--- круг тонов
  ArrayResize(sectorOf, popSize);
  ArrayInitialize(sectorOf, -1);
  for(int g = 0; g < 10; g++)
    agentIdx [g] = -1;

//--- буфер кандидатов: 10 агентов · Ncomb комбинаций 
  int Kcand = 10 * Ncomb;
  ArrayResize(candidates, Kcand);
  for(int i = 0; i < Kcand; i++)
    candidates [i].Init(coords);

  ArrayResize(candidateUsed, Kcand);
  ArrayInitialize(candidateUsed, false);

//--- snapshot и связи слот↔кандидат 
  ArrayResize(snap_c, popSize);
  for(int i = 0; i < popSize; i++)
    snap_c [i].Init(coords);
  ArrayResize(snap_f,        popSize);
  ArrayResize(snap_sectorOf, popSize);

  ArrayResize(slotCandidate, popSize);
  ArrayResize(isCandidate,   popSize);
  ArrayInitialize(slotCandidate, -1);
  ArrayInitialize(isCandidate,   false);

//--- цветовая память 
  ArrayResize(CM, Ksets);
  for(int s = 0; s < Ksets; s++)
    CM [s].Init(Ns, coords);
  CMcount   = 0;

  CMtempCap = Ksets * Ns;
  ArrayResize(CMtempC, CMtempCap);
  for(int i = 0; i < CMtempCap; i++)
    CMtempC [i].Init(coords);
  CMtempCnt = 0;

//--- начальная популяция с проверкой Dth_i 
  if(!InitialPopulationWithDiversity())
    return false;

  return true;
 }
//+------------------------------------------------------------------+

InitialPopulationWithDiversity генерирует начальную популяцию с проверкой разнообразия. На каждой попытке (до 50) равномерно сэмплируется популяция в диапазонах [rangeMin, rangeMax], затем рассчитывается её разнообразие. Если оно достигает или превышает Dthi, попытки прекращаются. На практике при равномерном распределении в больших диапазонах достаточно одной попытки. Если за все 50 попыток разнообразие не достигло порога, оставляется последняя сгенерированная популяция — это сценарий для очень узких или специфических диапазонов поиска, где геометрически невозможно достичь высокого разнообразия.

//+------------------------------------------------------------------+
//| Генерация начальной популяции с разнообразием ≥ Dthi.            |
//| Записывает координаты в a[i].cP[] (попадут в a[i].c через Moving)|
//+------------------------------------------------------------------+
bool C_AO_CHA::InitialPopulationWithDiversity()
 {
// Цикл регенерации; на практике сходится за 1-2 попытки при
// равномерном распределении в [rangeMin, rangeMax].
  const int maxAttempts = 50;
  for(int attempt = 0; attempt < maxAttempts; attempt++)
   {
    for(int i = 0; i < popSize; i++)
      for(int c = 0; c < coords; c++)
        a [i].cP [c] = u.RNDfromCI(rangeMin [c], rangeMax [c]);

    // Расчёт разнообразия по cP[]
    double D = 0.0;
    for(int c = 0; c < coords; c++)
     {
      double mean = 0.0;
      for(int i = 0; i < popSize; i++)
        mean += a [i].cP [c];
      mean /= popSize;
      double dj = 0.0;
      for(int i = 0; i < popSize; i++)
        dj += MathAbs(a [i].cP [c] - mean);
      dj /= popSize;
      D += dj;
     }
    D /= coords;

    if(D >= Dthi)
      return true;
   }

// Если за maxAttempts не сошлось — оставляем последнюю популяцию.
  return true;
 }
//+------------------------------------------------------------------+

Moving— главный диспетчер. На первом вызове (когда флаг revision ещё false) метод просто переносит начальную популяцию из cP[] в c[] через SeInDiSp для дискретизации — это нужно, чтобы внешний цикл тестового стенда смог посчитать FF для стартовой популяции. Размещение на круге тонов и назначение агентов делается в первом Revision, не здесь. Во всех последующих вызовах метод смотрит на флаг nextIsDispersion и вызывает либо DispersionPhase_Generate, либо комбинацию ComputePA + ConcentrationPhase_Generate. Сам метод не делает никакой содержательной работы — только маршрутизация.

//+------------------------------------------------------------------+
//|                            Moving                                |
//+------------------------------------------------------------------+
//  Логика диспетчера:
//   • Первый вызов (revision == false): копируем cP → c, чтобы внешний
//     цикл посчитал FF для начальной популяции. Размещение на круге
//     тонов и назначение агентов — в первом Revision().
//   • Дальнейшие вызовы: либо фаза концентрации (генерация
//     10·Ncomb_eff кандидатов в a[].c), либо фаза рассеивания
//     (генерация Ns кандидатов на замену ближайших к центру).
//+------------------------------------------------------------------+
void C_AO_CHA::Moving()
 {
  if(!revision)
   {
    // первый прогон — просто переносим начальную популяцию в c[]
    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]);
    return;
   }

  if(nextIsDispersion)
   {
    DispersionPhase_Generate();
   }
  else
   {
    ComputePA();
    ConcentrationPhase_Generate();
   }
 }
//+------------------------------------------------------------------+

Revision— второй главный диспетчер, вызываемый сразу после оценки FF в каждом слоте. Сначала идёт стандартный цикл обновления личных лучших каждой особи и глобального лучшего по всей популяции. Затем, если это первый вызов Revision, вызывается PlaceOnHueCircle для размещения отсортированной по фитнесу популяции на круге тонов и назначения 10 агентов — после этого флаг revision поднимается в true и метод завершается.

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

//+------------------------------------------------------------------+
//|                           Revision                               |
//+------------------------------------------------------------------+
//  Логика диспетчера:
//   • Если revision == false: это первая оценка популяции — обновляем
//     fB/cB, переносим f→fP в каждом слоте, размещаем особей на круге
//     тонов, назначаем агентов. revision = true.
//   • Иначе: применяем правила обновления круга для последней фазы
//     (концентрация или рассеивание), переключаем фазы по порогу
//     разнообразия, обновляем Dth_cur при необходимости.
//+------------------------------------------------------------------+
void C_AO_CHA::Revision()
 {
//--- обновление личных и глобального лучших 
  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);
     }
   }

  if(!revision)
   {
    // Первый Revision: просто разместить популяцию на круге тонов
    PlaceOnHueCircle();
    revision = true;
    return;
   }

//--- обновление круга по результатам только что оценённой фазы 
  if(nextIsDispersion)
   {
    DispersionPhase_UpdateCircle();
    iter_dp ++;
    Dth_cur *= damp;
    iter_cp = 0;                  // сброс — начинается новая серия концентраций
    nextIsDispersion = false;
   }
  else
   {
    ConcentrationPhase_UpdateCircle();
    iter_cp ++;
   }

//--- решение: какая фаза дальше 
  if(DiversityPopulation() < Dth_cur)
    nextIsDispersion = true;
 }
//+------------------------------------------------------------------+

DiversityPopulation— реализация формул (2)–(4) из оригинальной статьи. Для каждой координаты вычисляется среднее значение по популяции, затем среднее абсолютное отклонение от этого среднего. Финальное разнообразие — это среднее отклонений по всем координатам. Метрика устойчива к выбросам и хорошо работает в широком диапазоне размерностей задачи.

//+------------------------------------------------------------------+
//|              Утилиты: разнообразие и центр популяции             |
//+------------------------------------------------------------------+
double C_AO_CHA::DiversityPopulation()
 {
  double D = 0.0;
  for(int c = 0; c < coords; c++)
   {
    double mean = 0.0;
    for(int i = 0; i < popSize; i++)
      mean += a [i].c [c];
    mean /= popSize;

    double dj = 0.0;
    for(int i = 0; i < popSize; i++)
      dj += MathAbs(a [i].c [c] - mean);
    dj /= popSize;

    D += dj;
   }
  return D / coords;
 }
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
void C_AO_CHA::PopulationCenter(double &out [])
 {
  ArrayResize(out, coords);
  for(int c = 0; c < coords; c++)
   {
    double s = 0.0;
    for(int i = 0; i < popSize; i++)
      s += a [i].c [c];
    out [c] = s / popSize;
   }
 }
//+------------------------------------------------------------------+

ComputePA — расчёт параметра Power of Agent по формулам (8)–(10) статьи. Сначала вычисляется Ni = floor(iter_dp · Ncomb / N_Dth) с зажимом сверху на Ncomb — это «давление эксплуатации», которое со временем растёт. Затем step = (Ncomb + 1) / (Ncomb + Ni) — шаг, с которым iter_cp приближается к насыщению. Финальное значение PA = floor(iter_cp / step), тоже зажимается сверху на Ncomb. Семантика: чем больше PA, тем больше комбинаций фазы концентрации делается с агентом в качестве одного из родителей — то есть тем сильнее эксплуатация лидеров групп.

//+------------------------------------------------------------------+
//|       Power of Agent: формулы (8)–(10) статьи (Zaeimi 2020)      |
//+------------------------------------------------------------------+
void C_AO_CHA::ComputePA()
 {
  int Ni;
  if(N_Dth > 0)
   {
    double denom = (double)N_Dth / (double)Ncomb;
    Ni = (int)MathFloor((double)iter_dp / denom);
    if(Ni > Ncomb)
      Ni = Ncomb;
   }
  else
    Ni = 0;
  if(Ni < 0)
    Ni = 0;

  double step = (double)(Ncomb + 1) / (double)(Ncomb + Ni);
  if(step <= 0)
    step = 1.0;

  PA = (int)MathFloor((double)iter_cp / step);
  if(PA > Ncomb)
    PA = Ncomb;
  if(PA < 0)
    PA = 0;
 }
//+------------------------------------------------------------------+

UpdateAgentsByGroup — вызывается после применения правил круга в обеих фазах. Для каждой из 10 групп ищется лучшая по фитнесу особь среди тех, чьи секторы относятся к группе. Если найденный лидер не совпадает со старым агентом группы, происходит обмен секторов — старый агент попадает на сектор бывшего лидера, новый агент занимает чистейший сектор группы. Это реализует правило статьи «if replaced colors have better fitness value than their related agents, their positions on the hue circle will be exchanged».

//+------------------------------------------------------------------+
//|  Агент группы = лучший по фитнесу. Если новый лучший не сидит    |
//|  на чистейшем секторе — обмениваем секторы старого и нового.     |
//+------------------------------------------------------------------+
void C_AO_CHA::UpdateAgentsByGroup()
 {
  for(int g = 0; g < 10; g++)
   {
    int    oldAg  = agentIdx [g];
    int    bestI  = oldAg;
    double bestF  = (oldAg >= 0) ? a [oldAg].f : -DBL_MAX;

    for(int i = 0; i < popSize; i++)
     {
      if(i == oldAg)
        continue;
      if(sectorOf [i] / 10 != g)
        continue;
      if(a [i].f > bestF)
       {
        bestF = a [i].f;
        bestI = i;
       }
     }

    if(bestI >= 0 && bestI != oldAg)
     {
      // обмен секторов: новый агент → чистейший сектор группы
      if(oldAg >= 0)
       {
        int tmp = sectorOf [bestI];
        sectorOf [bestI] = sectorOf [oldAg];
        sectorOf [oldAg] = tmp;
       }
      else
       {
        sectorOf [bestI] = PurestSector(g);
       }
      agentIdx [g] = bestI;
     }
   }
 }
//+------------------------------------------------------------------+

PlaceOnHueCircle— первичное размещение популяции на круге тонов после стартовой оценки FF. Сначала особи сортируются по фитнесу по убыванию (пузырьковая сортировка по индексам, без перестановки самих структур a[]). Затем top-10 распределяются по чистейшим секторам в случайном порядке групп — порядок групп тасуется через классический Фишера-Йейтса. Оставшиеся popSize − 10 особей распределяются по группам максимально равномерно: сначала вычисляется базовое число особей на группу (popSize − 10) / 10, затем остаток (popSize − 10) % 10 распределяется по первым группам. Внутри каждой группы свободные секторы (все, кроме чистейшего) тасуются и назначаются особям по очереди. Если особей в группе больше девяти, секторы переиспользуются с повтором.

//+------------------------------------------------------------------+
//|     Первичное размещение популяции на круге тонов после          |
//|     стартовой оценки FF.                                         |
//+------------------------------------------------------------------+
void C_AO_CHA::PlaceOnHueCircle()
 {
// Сортировка индексов по фитнесу (по убыванию: лучший — первый)
  int idx [];
  ArrayResize(idx, popSize);
  for(int i = 0; i < popSize; i++)
    idx [i] = i;

  for(int i = 0; i < popSize - 1; i++)
    for(int j = i + 1; j < popSize; j++)
      if(a [idx [j]].f > a [idx [i]].f)
       {
        int t = idx [i];
        idx [i] = idx [j];
        idx [j] = t;
       }

// Top-10 → агенты на чистейших секторах в случайном порядке групп
  int groupOrder [10];
  for(int g = 0; g < 10; g++)
    groupOrder [g] = g;
  for(int g = 9; g > 0; g--)
   {
    int j = (int)u.RNDfromCI(0.0, (double)(g + 1));
    if(j > g)
      j = g;
    int t = groupOrder [g];
    groupOrder [g] = groupOrder [j];
    groupOrder [j] = t;
   }

  for(int k = 0; k < 10; k++)
   {
    int slot = idx [k];
    int g    = groupOrder [k];
    sectorOf [slot] = PurestSector(g);
    agentIdx [g]    = slot;
   }

// Остальные popSize-10 равномерно по группам
  int rem        = popSize - 10;
  int perGroup   = rem / 10;
  int extra      = rem % 10;

  int sectorList [];
  ArrayResize(sectorList, rem);
  int pos = 0;

  for(int g = 0; g < 10; g++)
   {
    int n = perGroup + (g < extra ? 1 : 0);

    // Свободные секторы группы (исключая чистейший)
    int freeSecs [9];
    int fsc = 0;
    for(int s = 0; s < 10; s++)
     {
      int sec = g * 10 + s;
      if(sec == PurestSector(g))
        continue;
      freeSecs [fsc++] = sec;
     }

    // Перемешать
    for(int s = fsc - 1; s > 0; s--)
     {
      int j = (int)u.RNDfromCI(0.0, (double)(s + 1));
      if(j > s)
        j = s;
      int t = freeSecs [s];
      freeSecs [s] = freeSecs [j];
      freeSecs [j] = t;
     }

    // Назначить n секторов (с повторами, если n > 9)
    for(int s = 0; s < n; s++)
      sectorList [pos++] = freeSecs [s % fsc];
   }

// Перемешать sectorList и раздать
  for(int i = ArraySize(sectorList) - 1; i > 0; i--)
   {
    int j = (int)u.RNDfromCI(0.0, (double)(i + 1));
    if(j > i)
      j = i;
    int t = sectorList [i];
    sectorList [i] = sectorList [j];
    sectorList [j] = t;
   }

  for(int k = 10; k < popSize; k++)
    sectorOf [idx [k]] = sectorList [k - 10];
 }
//+------------------------------------------------------------------+

GreySectorsForTemplate. Возвращает массив секторов, попадающих в «серые дуги» одного из пяти шаблонов гармонии при повороте под анкорный сектор anchor. Размеры дуг точно соответствуют Appendix A оригинальной статьи: X-тип — две дуги по 26% круга на 180° друг от друга, V-тип — одна дуга 26%, Y-тип — малая дуга 5% и большая 26% на 180°, T-тип — одна дуга 50%, L-тип — малая 5% и средняя 22% на 90°. Все смещения секторов модулируются по 100, чтобы обходить переход через границу круга. Метод используется в фазе концентрации для отбора кандидатов на роль родителей комбинации.

//+------------------------------------------------------------------+
//|  Серые секторы для шаблона при повороте под якорь anchor.        |
//|  Размеры дуг: X/V — 26%, T — 50%, Y — 5%+26%, L — 5%+22%.        |
//|  Углы между дугами: X/Y — 180°, L — 90°.                         |
//+------------------------------------------------------------------+
void C_AO_CHA::GreySectorsForTemplate(int templateType, int anchor, int &out [])
 {
  ArrayResize(out, 0);

// вспомогательная лямбда-имитация: добавление отрезка (offset±half) от anchor
// диапазон [center - low, center + high] включительно, по модулю 100
// чтобы не плодить вспомогательную функцию — пишем in-line.

  if(templateType == 0)
   {
    // X type: две дуги по 26% (по 26 секторов), угол между биссектрисами 180°
    for(int d = -13; d <= 12; d++)
     {
      int s1 = ((anchor + d) % 100 + 100) % 100;
      int s2 = ((anchor + 50 + d) % 100 + 100) % 100;
      int n = ArraySize(out);
      ArrayResize(out, n + 2);
      out [n]     = s1;
      out [n + 1] = s2;
     }
   }
  else
    if(templateType == 1)
     {
      // V type: одна дуга 26%
      for(int d = -13; d <= 12; d++)
       {
        int s = ((anchor + d) % 100 + 100) % 100;
        int n = ArraySize(out);
        ArrayResize(out, n + 1);
        out [n] = s;
       }
     }
    else
      if(templateType == 2)
       {
        // Y type: малая 5% на anchor + большая 26% на (anchor+50)
        for(int d = -2; d <= 2; d++)
         {
          int s = ((anchor + d) % 100 + 100) % 100;
          int n = ArraySize(out);
          ArrayResize(out, n + 1);
          out [n] = s;
         }
        for(int d = -13; d <= 12; d++)
         {
          int s = ((anchor + 50 + d) % 100 + 100) % 100;
          int n = ArraySize(out);
          ArrayResize(out, n + 1);
          out [n] = s;
         }
       }
      else
        if(templateType == 3)
         {
          // T type: одна дуга 50%
          for(int d = -25; d <= 24; d++)
           {
            int s = ((anchor + d) % 100 + 100) % 100;
            int n = ArraySize(out);
            ArrayResize(out, n + 1);
            out [n] = s;
           }
         }
        else
          if(templateType == 4)
           {
            // L type: малая 5% на anchor + большая 22% на (anchor+25)
            for(int d = -2; d <= 2; d++)
             {
              int s = ((anchor + d) % 100 + 100) % 100;
              int n = ArraySize(out);
              ArrayResize(out, n + 1);
              out [n] = s;
             }
            for(int d = -11; d <= 10; d++)
             {
              int s = ((anchor + 25 + d) % 100 + 100) % 100;
              int n = ArraySize(out);
              ArrayResize(out, n + 1);
              out [n] = s;
             }
           }
 }
//+------------------------------------------------------------------+

ConcentrationPhase_Generate— главная процедура построения кандидатов. Первый шаг — создание snapshot всей популяции. Второй — сброс связей слот↔кандидат. Третий, основной, — цикл по 10 агентам.

Для каждого агента перебираются все 5 шаблонов гармонии. Для каждого шаблона собирается множество H — индексы слотов, чьи секторы попали в серые дуги. Это множество разделяется на Hagents (агенты в серых дугах) и Hnon (не-агенты в серых дугах). Если Hnon пусто, шаблон пропускается — нет материала для построения комбинаций.

Перед генерацией комбинаций вычисляется адаптивная амплитуда возмущения perturbScale. Она завязана на текущий порог разнообразия Dth_cur: пока порог высок (популяция разрежена), амплитуда около 10% диапазона; по мере сжатия порога до Dthf амплитуда падает до 1%. Это не статическая константа из оригинальной формулировки, а критическое расширение алгоритма — без него все комбинации лежат строго в выпуклой оболочке родителей и не могут покинуть её.

Затем строится Ncomb комбинаций. Если индекс комбинации m < PA, она строится по типу (i) или (ii): с вероятностью 1/3 берётся комбинация «глобально лучший агент × якорный агент» (Type ii — мост между лидерами), иначе «якорный агент × случайный не-агент из серых дуг» (Type i — локальный эксплоит). Если m ≥ PA, строится комбинация типа (iii) — два различных не-агента из серых дуг.

Сама комбинация — линейная смесь координат двух родителей с коэффициентами r1, r2 = 1 − r1, где r1 берётся равномерно из [0.1, 0.9] (избегаем вырожденных случаев, когда кандидат почти совпадает с одним из родителей). К результату прибавляется гауссово возмущение амплитудой perturbScale. Финальное значение зажимается в границы диапазона [rangeMin, rangeMax].

После построения всех Ncomb комбинаций под текущий шаблон вычисляется их совокупное разнообразие относительно якорного агента. Шаблон с максимальным разнообразием выигрывает — именно его комбинации сохраняются в bestSet. Если ни один шаблон не дал материала (исключительная ситуация), используется fallback с возмущением координат якорного агента в радиусе 10% диапазона.

Лучший набор записывается в общий массив candidates[]. После завершения цикла по агентам кандидаты размещаются в слоты не-агентов для оценки FF. Размещение учитывает правило статьи: новый цвет ставится на сектор одного из своих родителей. Сначала ищется неагентский слот с подходящим сектором, если такого нет — fallback в любой свободный неагентский слот. Слоты агентов в фазе концентрации не трогаются — их координаты восстанавливаются из snapshot, чтобы при следующей оценке FF их фитнес не пересчитывался зря.

//+------------------------------------------------------------------+
//|                    ФАЗА КОНЦЕНТРАЦИИ — генерация                 |
//+------------------------------------------------------------------+
void C_AO_CHA::ConcentrationPhase_Generate()
 {
//--- 1. Сохранить snapshot популяции 
  for(int i = 0; i < popSize; i++)
   {
    for(int c = 0; c < coords; c++)
      snap_c [i].v [c] = a [i].c [c];
    snap_f        [i] = a [i].f;
    snap_sectorOf [i] = sectorOf [i];
   }

//--- 2. Сбросить связи слот↔кандидат и флаги используемости 
  ArrayInitialize(slotCandidate, -1);
  ArrayInitialize(isCandidate,   false);
  ArrayInitialize(candidateUsed, false);

//--- 3. По каждому агенту: 5 шаблонов → выбрать лучший набор 
  int Kcand   = 10 * Ncomb;
  int candPos = 0;

// временный буфер набора Ncomb комбинаций
  S_CHA_NewColor combo [];
  ArrayResize(combo, Ncomb);
  for(int i = 0; i < Ncomb; i++)
    combo [i].Init(coords);

  S_CHA_NewColor bestSet [];
  ArrayResize(bestSet, Ncomb);
  for(int i = 0; i < Ncomb; i++)
    bestSet [i].Init(coords);

  for(int g = 0; g < 10; g++)
   {
    int agSlot = agentIdx [g];
    if(agSlot < 0)
      continue;
    int anchorSec = snap_sectorOf [agSlot];

    double bestDiv = -1.0;

    for(int t = 0; t < 5; t++)
     {
      int greySectors [];
      GreySectorsForTemplate(t, anchorSec, greySectors);

      // Собрать H — индексы слотов, чьи секторы в серых дугах
      int H [];
      ArrayResize(H, 0);
      for(int i = 0; i < popSize; i++)
       {
        for(int s = 0; s < ArraySize(greySectors); s++)
         {
          if(snap_sectorOf [i] == greySectors [s])
           {
            int n = ArraySize(H);
            ArrayResize(H, n + 1);
            H [n] = i;
            break;
           }
         }
       }

      // Разделить H на агентов и не-агентов
      int Hagents [], Hnon [];
      ArrayResize(Hagents, 0);
      ArrayResize(Hnon,    0);
      for(int i = 0; i < ArraySize(H); i++)
       {
        bool ag = false;
        for(int gg = 0; gg < 10; gg++)
          if(agentIdx [gg] == H [i])
           {
            ag = true;
            break;
           }
        int n;
        if(ag)
         {
          n = ArraySize(Hagents);
          ArrayResize(Hagents, n + 1);
          Hagents [n] = H [i];
         }
        else
         {
          n = ArraySize(Hnon);
          ArrayResize(Hnon, n + 1);
          Hnon [n] = H [i];
         }
       }

      if(ArraySize(Hnon) == 0)
        continue;  // нет «материала» для комбинаций

      // Найти глобально лучшего агента — нужен для Type (ii)
      int    bestAg  = agSlot;
      double bestAgF = snap_f [agSlot];
      for(int gg = 0; gg < 10; gg++)
       {
        int s = agentIdx [gg];
        if(s < 0)
          continue;
        if(snap_f [s] > bestAgF)
         {
          bestAgF = snap_f [s];
          bestAg  = s;
         }
       }

      // Адаптивная амплитуда возмущения: завязана на текущий порог
      // разнообразия Dth_cur, а не на счётчик iter_dp. Логика:
      // когда популяция рассеяна (Dth_cur высок) — возмущаем сильно,
      // когда собрана (Dth_cur мал) — точная фокусировка.
      // 10% при Dth_cur = Dthi (старт), 1% при Dth_cur = Dthf (финиш).
      double ratio = (Dthi > 0.0)
                     ? MathMax(0.0, MathMin(1.0, (Dth_cur - Dthf) / (Dthi - Dthf)))
                     : 0.0;
      double perturbScale = 0.01 + 0.09 * ratio;

      // Сгенерировать Ncomb комбинаций
      for(int m = 0; m < Ncomb; m++)
       {
        int    ia = -1, ib = -1;

        if(m < PA)
         {
          // С вероятностью 1/3 — Type (ii): глобальный лучший × якорный.
          // Это сильный эксплоит, мост между лучшими лидерами групп.
          // Иначе — Type (i): якорный × не-агент (локальный эксплоит).
          if(bestAg != agSlot && u.RNDfromCI(0.0, 1.0) < 0.33)
           {
            ia = bestAg;
            ib = agSlot;
           }
          else
           {
            ia = agSlot;
            int kk = (int)u.RNDfromCI(0.0, (double)ArraySize(Hnon));
            if(kk >= ArraySize(Hnon))
              kk = ArraySize(Hnon) - 1;
            ib = Hnon [kk];
           }
         }
        else
         {
          // Type (iii): два не-агента, эксплорация
          if(ArraySize(Hnon) >= 2)
           {
            int k1 = (int)u.RNDfromCI(0.0, (double)ArraySize(Hnon));
            if(k1 >= ArraySize(Hnon))
              k1 = ArraySize(Hnon) - 1;
            int k2;
            int safety = 0;
            do
             {
              k2 = (int)u.RNDfromCI(0.0, (double)ArraySize(Hnon));
              if(k2 >= ArraySize(Hnon))
                k2 = ArraySize(Hnon) - 1;
              safety++;
              if(safety > 20)
               {
                k2 = (k1 + 1) % ArraySize(Hnon);
                break;
               }
             }
            while(k2 == k1);
            ia = Hnon [k1];
            ib = Hnon [k2];
           }
          else
           {
            ia = (ArraySize(Hagents) > 0) ? Hagents [0] : agSlot;
            ib = Hnon [0];
           }
         }

        // r1 в [0.1, 0.9] — исключаем вырожденные комбинации
        double r1 = u.RNDfromCI(0.1, 0.9);
        double r2 = 1.0 - r1;

        for(int c = 0; c < coords; c++)
         {
          double v = r1 * snap_c [ia].v [c] + r2 * snap_c [ib].v [c];

          // Возмущение с адаптивной амплитудой по cooling-графику
          double range = rangeMax [c] - rangeMin [c];
          v += u.RNDfromCI(-1.0, 1.0) * range * perturbScale;

          if(v < rangeMin [c])
            v = rangeMin [c];
          if(v > rangeMax [c])
            v = rangeMax [c];
          combo [m].c [c] = v;
         }
        combo [m].sec_a = snap_sectorOf [ia];
        combo [m].sec_b = snap_sectorOf [ib];
       }

      // Diversity комбинаций относительно агента (центр = координаты агента)
      double divAgg = 0.0;
      for(int c = 0; c < coords; c++)
       {
        double dj = 0.0;
        for(int m = 0; m < Ncomb; m++)
          dj += MathAbs(combo [m].c [c] - snap_c [agSlot].v [c]);
        dj /= Ncomb;
        divAgg += dj;
       }
      divAgg /= coords;

      if(divAgg > bestDiv)
       {
        bestDiv = divAgg;
        for(int m = 0; m < Ncomb; m++)
         {
          for(int c = 0; c < coords; c++)
            bestSet [m].c [c] = combo [m].c [c];
          bestSet [m].sec_a = combo [m].sec_a;
          bestSet [m].sec_b = combo [m].sec_b;
         }
       }
     } // конец цикла по шаблонам

    // Fallback: ни один шаблон не нашёл материала → возмущения вокруг агента
    if(bestDiv < 0)
     {
      for(int m = 0; m < Ncomb; m++)
       {
        for(int c = 0; c < coords; c++)
         {
          double range = rangeMax [c] - rangeMin [c];
          double v = snap_c [agSlot].v [c] + u.RNDfromCI(-1.0, 1.0) * range * 0.1;
          if(v < rangeMin [c])
            v = rangeMin [c];
          if(v > rangeMax [c])
            v = rangeMax [c];
          bestSet [m].c [c] = v;
         }
        bestSet [m].sec_a = anchorSec;
        bestSet [m].sec_b = anchorSec;
       }
     }

    // Записать bestSet в общий список candidates
    for(int m = 0; m < Ncomb; m++)
     {
      for(int c = 0; c < coords; c++)
        candidates [candPos].c [c] = bestSet [m].c [c];
      candidates [candPos].sec_a = bestSet [m].sec_a;
      candidates [candPos].sec_b = bestSet [m].sec_b;
      candidates [candPos].f     = -DBL_MAX;
      candPos++;
     }
   } // конец цикла по агентам

//--- 4. Разместить кандидатов в слоты не-агентов для оценки FF 
// По правилу (ii) статьи: новый цвет ставится на сектор одного из своих
// родителей. Сначала ищем слот с подходящим сектором; если занят или
// отсутствует — fallback в любой свободный неагентский слот.

  bool isAgentSlot [];
  ArrayResize(isAgentSlot, popSize);
  ArrayInitialize(isAgentSlot, false);
  for(int g = 0; g < 10; g++)
    if(agentIdx [g] >= 0)
      isAgentSlot [agentIdx [g]] = true;

  bool slotTaken [];
  ArrayResize(slotTaken, popSize);
  ArrayInitialize(slotTaken, false);

  for(int m = 0; m < Kcand; m++)
   {
    int sec_a = candidates [m].sec_a;
    int sec_b = candidates [m].sec_b;
    int chosenSlot = -1;

    // 1) попытка: неагентский слот с подходящим сектором
    for(int i = 0; i < popSize; i++)
     {
      if(isAgentSlot [i] || slotTaken [i])
        continue;
      if(snap_sectorOf [i] == sec_a || snap_sectorOf [i] == sec_b)
       {
        chosenSlot = i;
        break;
       }
     }

    // 2) fallback: любой свободный неагентский слот
    if(chosenSlot < 0)
     {
      for(int i = 0; i < popSize; i++)
       {
        if(isAgentSlot [i] || slotTaken [i])
          continue;
        chosenSlot = i;
        break;
       }
     }

    if(chosenSlot < 0)
      break; // на всякий случай — все слоты заняты

    slotTaken     [chosenSlot] = true;
    slotCandidate [chosenSlot] = m;
    isCandidate   [chosenSlot] = true;

    for(int c = 0; c < coords; c++)
      a [chosenSlot].c [c] = u.SeInDiSp(candidates [m].c [c],
                                        rangeMin  [c],
                                        rangeMax  [c],
                                        rangeStep [c]);
   }

//--- 5. Слоты агентов — без изменений (a[i].c = snap_c[i]) 
  for(int g = 0; g < 10; g++)
   {
    int s = agentIdx [g];
    if(s < 0)
      continue;
    for(int c = 0; c < coords; c++)
      a [s].c [c] = u.SeInDiSp(snap_c [s].v [c],
                               rangeMin  [c],
                               rangeMax  [c],
                               rangeStep [c]);
   }
 }
//+------------------------------------------------------------------+

ConcentrationPhase_UpdateCircle — применение результатов оценки FF. Сначала фитнесы кандидатов копируются из a[i].f обратно в candidates[m].f через связи slotCandidate[]. Затем проводится greedy-селекция: каждый слот, в который был помещён кандидат, сравнивает фитнес кандидата с фитнесом из snapshot. Если кандидат лучше — он остаётся в слоте, флаг candidateUsed[m] поднимается. Если хуже — слот откатывается из snapshot. Это упрощение оригинального правила круга, в котором перекрёстная фильтрация шагов 4–12 пропускала только малую долю кандидатов в популяцию; greedy-селекция даёт всем равные шансы и существенно ускоряет движение алгоритма.

После селекции слоты агентов восстанавливаются из snapshot полностью — это нужно на случай, если SeInDiSp в Moving слегка сместил координаты при дискретизации. Все непринятые кандидаты складываются в кольцевой буфер CMtempC[]. В конце вызывается UpdateAgentsByGroup для возможной смены лидеров групп.

//+------------------------------------------------------------------+
//|         ФАЗА КОНЦЕНТРАЦИИ — обновление круга (greedy + sector)   |
//+------------------------------------------------------------------+
void C_AO_CHA::ConcentrationPhase_UpdateCircle()
 {
  int Kcand = ArraySize(candidates);

//--- 1. Считать фитнесы кандидатов из их временных слотов в a[] 
  for(int i = 0; i < popSize; i++)
   {
    int m = slotCandidate [i];
    if(m >= 0)
      candidates [m].f = a [i].f;
   }

//--- 2. Greedy-селекция: для каждого слота с кандидатом сравнить
// его фитнес с фитнесом из snapshot. Лучший — остаётся, худший —
// откатывается. Это даёт всем 10·Ncomb кандидатам равный шанс на
// вход в популяцию (в отличие от строгих перекрёстных правил
// оригинальной статьи, которые отбрасывают большинство кандидатов
// и оставляют популяцию почти без движения).
  for(int i = 0; i < popSize; i++)
   {
    int m = slotCandidate [i];
    if(m < 0)
      continue;

    if(candidates [m].f > snap_f [i])
     {
      // принять — координаты уже в a[i].c, фитнес уже в a[i].f
      candidateUsed [m] = true;
      // sectorOf[i] не меняется (правило (ii) статьи)
     }
    else
     {
      // отвергнуть — откатить слот
      for(int c = 0; c < coords; c++)
        a [i].c [c] = snap_c [i].v [c];
      a [i].f = snap_f [i];
     }
   }

//--- 3. Слоты агентов — восстановить из snapshot на случай, если
// SeInDiSp в Moving слегка сместил координаты при дискретизации.
  for(int g = 0; g < 10; g++)
   {
    int s = agentIdx [g];
    if(s < 0)
      continue;
    for(int c = 0; c < coords; c++)
      a [s].c [c] = snap_c [s].v [c];
    a [s].f = snap_f [s];
   }

//--- 4. Непринятые кандидаты → CMtemp 
  for(int m = 0; m < Kcand; m++)
   {
    if(candidateUsed [m])
      continue;

    int dst = CMtempCnt % CMtempCap;
    for(int c = 0; c < coords; c++)
      CMtempC [dst].v [c] = candidates [m].c [c];
    CMtempCnt++;
    if(CMtempCnt >= 2 * CMtempCap)
      CMtempCnt = CMtempCap;
   }

//--- 5. Обновить агентов групп (обмен секторов при появлении 
//       нового лидера в группе)
  UpdateAgentsByGroup();
 }
//+------------------------------------------------------------------+
        

DispersionPhase_Generate— реализует формулу Манселла для рассеивания популяции. Сначала рассчитывается центр популяции, затем находятся Ns ближайших к центру не-агентов — именно они подлежат замене. Источник материала для замены выбирается так: если основная цветовая память CM уже заполнена (CMcount ≥ Ksets), берётся самый разнообразный набор из неё; иначе, если в CMtempC есть хоть какие-то накопленные кандидаты, делается случайная выборка из них; иначе — fallback на равномерную генерацию точек по всему пространству поиска.

Выбранный источник SCM оценивается по разнообразию относительно центра пространства поиска. Если основная память не заполнена, новый набор просто добавляется в неё; если заполнена, ищется существующий набор с минимальным разнообразием — и заменяется на новый, если у нового разнообразие больше.

Финальный шаг — генерация замен по формуле X_new = rcm·X_SCM + (1 − rcm)·X_old, где коэффициент rcm берётся равномерно из [rcm0, 1]. Параметр rcm0 (по умолчанию 0.7) гарантирует, что новый цвет в основном определяется точкой из памяти, но сохраняет некоторую связь со старой позицией. Замены пишутся непосредственно в слоты a[sxIdx[k]].c[] через SeInDiSp для дискретизации.

//+------------------------------------------------------------------+
//|                    ФАЗА РАССЕИВАНИЯ — генерация                  |
//+------------------------------------------------------------------+
void C_AO_CHA::DispersionPhase_Generate()
 {
//--- 1. Центр популяции 
  double center [];
  PopulationCenter(center);

//--- 2. SX = Ns ближайших к центру не-агентов 
  double dist [];
  ArrayResize(dist, popSize);
  for(int i = 0; i < popSize; i++)
   {
    bool ag = false;
    for(int g = 0; g < 10; g++)
      if(agentIdx [g] == i)
       {
        ag = true;
        break;
       }
    if(ag)
     {
      dist [i] = DBL_MAX;
      continue;
     }

    double d = 0.0;
    for(int c = 0; c < coords; c++)
      d += MathAbs(a [i].c [c] - center [c]);
    dist [i] = d;
   }

  int sxIdx [];
  ArrayResize(sxIdx, Ns);
  for(int k = 0; k < Ns; k++)
   {
    double mn = DBL_MAX;
    int    mi = -1;
    for(int i = 0; i < popSize; i++)
      if(dist [i] < mn)
       {
        mn = dist [i];
        mi = i;
       }
    sxIdx [k] = mi;
    if(mi >= 0)
      dist [mi] = DBL_MAX;
   }

//--- 3. Источник SCM: CM (если заполнен) или CMtemp 
  double centerSpace [];
  ArrayResize(centerSpace, coords);
  for(int c = 0; c < coords; c++)
    centerSpace [c] = (rangeMin [c] + rangeMax [c]) * 0.5;

  S_CHA_Vector scmPoints [];
  ArrayResize(scmPoints, Ns);
  for(int i = 0; i < Ns; i++)
    scmPoints [i].Init(coords);

  bool scmReady = false;

  if(CMcount >= Ksets)
   {
    // выбрать самый разнообразный набор из CM
    int    bi = 0;
    double bd = CM [0].diversity;
    for(int s = 1; s < CMcount; s++)
      if(CM [s].diversity > bd)
       {
        bd = CM [s].diversity;
        bi = s;
       }

    for(int i = 0; i < Ns; i++)
      for(int c = 0; c < coords; c++)
        scmPoints [i].v [c] = CM [bi].points [i].v [c];
    scmReady = true;
   }
  else
    if(CMtempCnt > 0)
     {
      int avail = MathMin(CMtempCnt, CMtempCap);
      if(avail >= Ns)
       {
        // случайный выбор Ns точек
        for(int k = 0; k < Ns; k++)
         {
          int idx = (int)u.RNDfromCI(0.0, (double)avail);
          if(idx >= avail)
            idx = avail - 1;
          for(int c = 0; c < coords; c++)
            scmPoints [k].v [c] = CMtempC [idx].v [c];
         }
       }
      else
       {
        for(int k = 0; k < Ns; k++)
         {
          int src = k % avail;
          for(int c = 0; c < coords; c++)
            scmPoints [k].v [c] = CMtempC [src].v [c];
         }
       }
      scmReady = true;
     }

  if(!scmReady)
   {
    // Fallback: случайные точки во всём поисковом пространстве
    for(int i = 0; i < Ns; i++)
      for(int c = 0; c < coords; c++)
        scmPoints [i].v [c] = u.RNDfromCI(rangeMin [c], rangeMax [c]);
   }

//--- 4. Сохранить SCM в CM (шаг 2 dispersion) 
// Diversity SCM относительно центра пространства поиска
  double scmDiv = 0.0;
  for(int c = 0; c < coords; c++)
   {
    double dj = 0.0;
    for(int i = 0; i < Ns; i++)
      dj += MathAbs(scmPoints [i].v [c] - centerSpace [c]);
    dj /= Ns;
    scmDiv += dj;
   }
  scmDiv /= coords;

  if(CMcount < Ksets)
   {
    int idx = CMcount;
    for(int i = 0; i < Ns; i++)
      for(int c = 0; c < coords; c++)
        CM [idx].points [i].v [c] = scmPoints [i].v [c];
    CM [idx].diversity = scmDiv;
    CMcount++;
   }
  else
   {
    int    mi = 0;
    double md = CM [0].diversity;
    for(int s = 1; s < CMcount; s++)
      if(CM [s].diversity < md)
       {
        md = CM [s].diversity;
        mi = s;
       }

    if(scmDiv > md)
     {
      for(int i = 0; i < Ns; i++)
        for(int c = 0; c < coords; c++)
          CM [mi].points [i].v [c] = scmPoints [i].v [c];
      CM [mi].diversity = scmDiv;
     }
   }

//--- 5. Сгенерировать новые цвета и записать в слоты SX 
  for(int k = 0; k < Ns; k++)
   {
    int slot = sxIdx [k];
    if(slot < 0)
      continue;
    double rcm = u.RNDfromCI(rcm0, 1.0);
    for(int c = 0; c < coords; c++)
     {
      double v = rcm * scmPoints [k].v [c] + (1.0 - rcm) * a [slot].c [c];
      a [slot].c [c] = u.SeInDiSp(v, rangeMin [c], rangeMax [c], rangeStep [c]);
     }
   }
 }
//+------------------------------------------------------------------+

DispersionPhase_UpdateCircle— самый короткий метод алгоритма: он всего лишь вызывает UpdateAgentsByGroup. По статье, после рассеивания «if replaced colors have better fitness value than their related agents, their positions on the hue circle will be exchanged» — UpdateAgentsByGroup именно это и делает: для каждой группы проверяет, не появился ли новый лидер, и при необходимости меняет местами их секторы.

//+------------------------------------------------------------------+
//|       ФАЗА РАССЕИВАНИЯ — обновление круга после оценки FF        |
//+------------------------------------------------------------------+
void C_AO_CHA::DispersionPhase_UpdateCircle()
 {
// По статье: «if replaced colors have better fitness value than their
// related agents, their positions on the hue circle will be exchanged».
// UpdateAgentsByGroup делает именно это (обмен секторов при смене лидера).
  UpdateAgentsByGroup();
 }
//+------------------------------------------------------------------+


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

Тестирование проводилось на стандартном бенчмарке C_AO: функции Hilly, Forest, Megacity на размерностях 10, 50, 1000 (5, 25, 500 функций), бюджет 10000 оценок FF, 10 повторений. Параметры алгоритма по умолчанию. Тест анти-чит алгоритм CHA проходит без проблем, набирая сопоставимое количество баллов на первых размерностях основного теста.

CHA|Color Harmony Algorithm|50.0|4.0|10.0|4.0|0.5|0.01|0.7|
=============================
5 Hilly's; Func runs: 10000; result: 0.5861221565962496
25 Hilly's; Func runs: 10000; result: 0.46909923573046725
500 Hilly's; Func runs: 10000; result: 0.30514641914713303
=============================
5 Forest's; Func runs: 10000; result: 0.4702465987009819
25 Forest's; Func runs: 10000; result: 0.21050099785621573
500 Forest's; Func runs: 10000; result: 0.07964154815476658
=============================
5 Megacity's; Func runs: 10000; result: 0.42000000000000004
25 Megacity's; Func runs: 10000; result: 0.2437333333333333
500 Megacity's; Func runs: 10000; result: 0.12732000000000107
=============================
All score: 2.91181 (32.35%)

Визуализация работы алгоритма на тестовом стенде и на некоторых функциях. 

Hilly

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

Forest

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

Megacity

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

Ackley

CHA на функции Ackley

Paraboloid

CHA на функции Paraboloid

В рейтинговой таблице алгоритм CHA представлен ознакомительно.

cc 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 1,00000 0,88228 0,40138 2,28366 1,00000 0,95281 0,28092 2,23373 0,94667 0,85733 0,22389 2,02789 6,545 72,72
2 AMOm animal migration optimization M 0,91624 0,83603 0,46790 2,22017 0,98482 0,92010 0,36391 2,26883 0,91733 0,81707 0,25177 1,98617 6,475 71,94
3 CLA code lock algorithm (joo) 0,95139 0,86199 0,37879 2,19217 0,99349 0,93500 0,26497 2,19346 0,93600 0,84267 0,24060 2,01927 6,405 71,17
4 (P+O)ES (P+O) evolution strategies 0,86571 0,89539 0,39740 2,15850 0,97761 0,89820 0,26878 2,14459 0,92133 0,80240 0,23952 1,96325 6,266 69,62
5 SDSm stochastic diffusion search M 0,95195 0,84944 0,36249 2,16388 0,98061 0,88457 0,22112 2,08630 0,92267 0,79013 0,21380 1,92660 6,177 68,63
6 AAm archery algorithm M 0,84685 0,73320 0,42590 2,00595 0,96709 0,77837 0,27789 2,02335 0,86133 0,77707 0,28712 1,92552 5,955 66,17
7 SIA simulated isotropic annealing (joo) 0,93543 0,86504 0,38483 2,18530 0,94069 0,80609 0,23835 1,98513 0,86400 0,66160 0,19536 1,72096 5,891 65,46
8 TETA time evolution travel algorithm (joo) 0,91452 0,86369 0,25579 2,03400 0,99654 0,91291 0,14394 2,05339 0,85467 0,82213 0,10443 1,78123 5,869 65,21
9 ESG evolution of social groups (joo) 0,98111 0,79857 0,31167 2,09135 0,98954 0,82270 0,15032 1,96256 0,92133 0,73440 0,15315 1,80888 5,863 65,14
10 CTA comet tail algorithm (joo) 0,92435 0,86786 0,27838 2,07059 0,99039 0,84571 0,19448 2,03058 0,95467 0,69680 0,11008 1,76155 5,863 65,14
11 ECBO enhanced colliding bodies optimization 0,94024 0,72363 0,32356 1,98743 0,99477 0,80291 0,13056 1,92824 0,87600 0,70160 0,17433 1,75193 5,668 62,98
12 DA dialectical algorithm 0,93117 0,75400 0,26205 1,94722 0,98925 0,81375 0,08662 1,88962 0,92667 0,68107 0,11315 1,72089 5,558 61,76
13 BBO biogeography based optimization 0,95876 0,70609 0,35752 2,02237 0,92981 0,70660 0,16970 1,80611 0,87467 0,63013 0,20813 1,71293 5,541 61,57
14 BHAm black hole algorithm M 0,79558 0,76207 0,34682 1,90447 0,99836 0,75798 0,13826 1,89460 0,85067 0,64427 0,17020 1,66514 5,464 60,71
15 HS harmony search 0,91420 0,69049 0,29924 1,90393 0,97627 0,73373 0,14193 1,85193 0,91733 0,62720 0,15364 1,69817 5,454 60,60
16 RFO royal flush optimization (joo) 0,80989 0,74481 0,34546 1,90016 0,95251 0,77926 0,15185 1,88362 0,80400 0,66427 0,19071 1,65898 5,443 60,48
17 BOAm billiards optimization algorithm M 0,76177 0,72421 0,25275 1,73873 0,90890 0,81960 0,28853 2,01703 0,83733 0,74613 0,09763 1,68109 5,437 60,41
18 ASO anarchy society optimization 0,73070 0,73713 0,31195 1,77978 0,99732 0,87700 0,17619 2,05051 0,72000 0,68773 0,18988 1,59761 5,428 60,31
19 EOm extremal optimization_M 0,76527 0,75205 0,31908 1,83640 0,99999 0,76426 0,12437 1,88862 0,84133 0,64133 0,15247 1,63513 5,360 59,56
20 ACS artificial cooperative search 0,75545 0,77162 0,31653 1,84360 1,00000 0,80488 0,10705 1,91193 0,76933 0,60800 0,14157 1,51890 5,274 58,60
21 SSG saplings sowing and growing 0,75436 0,63206 0,35935 1,74577 0,91907 0,69694 0,19755 1,81356 0,81867 0,60533 0,21347 1,63747 5,197 57,74
22 AOSm atomic orbital search M 0,76184 0,68435 0,31344 1,75963 0,90015 0,80044 0,11501 1,81560 0,82800 0,63280 0,15696 1,61776 5,193 57,70
23 TSEA turtle shell evolution algorithm (joo) 0,95809 0,64852 0,29571 1,90232 0,99522 0,58104 0,10542 1,68168 0,92133 0,52160 0,14567 1,58860 5,173 57,48
24 DE differential evolution 0,96398 0,62346 0,26089 1,84833 0,98482 0,77018 0,11459 1,86959 0,93067 0,36213 0,11000 1,40280 5,121 56,90
25 BIO blood inheritance optimization (joo) 0,72580 0,66522 0,31228 1,70330 0,99995 0,68125 0,11540 1,79660 0,85467 0,59333 0,15364 1,60164 5,102 56,69
26 (PO)ES (PO) evolution strategies 0,73972 0,58190 0,38896 1,71058 0,91199 0,59975 0,21262 1,72436 0,82400 0,56240 0,23432 1,62072 5,056 56,18
27 BO bonobo optimizer 0,75555 0,64366 0,32657 1,72578 0,94332 0,70442 0,13999 1,78773 0,73467 0,61440 0,16728 1,51635 5,030 55,89
28 SRA successful restaurateur algorithm (joo) 0,89010 0,63359 0,29115 1,81484 0,96634 0,55285 0,08914 1,60833 0,89333 0,52800 0,13911 1,56044 4,984 55,38
29 CRO chemical reaction optimisation 0,91281 0,65681 0,29866 1,86828 0,90513 0,56020 0,10939 1,57472 0,82800 0,50133 0,14149 1,47082 4,914 54,60
30 BCOm bacterial chemotaxis optimization M 0,82589 0,61733 0,31584 1,75906 0,95296 0,63718 0,11984 1,70998 0,76533 0,51653 0,15800 1,43986 4,909 54,54
31 DOA dream optimization algorithm 0,78522 0,78121 0,36036 1,92679 0,61584 0,42117 0,12254 1,15955 0,86667 0,72587 0,21127 1,80381 4,890 54,33
32 ABO african buffalo optimization 0,92295 0,62528 0,29885 1,84708 0,92992 0,57468 0,09372 1,59832 0,73333 0,51333 0,14324 1,38990 4,835 53,72
33 BSA bird swarm algorithm 0,94432 0,67941 0,26401 1,88774 0,91649 0,65619 0,12054 1,69322 0,80933 0,33547 0,10652 1,25132 4,832 53,69
34 TSm tabu search M 0,87806 0,61040 0,28993 1,77839 0,98116 0,52165 0,08544 1,58825 0,82667 0,49547 0,13552 1,45766 4,824 53,60
35 BSA backtracking search algorithm 0,87128 0,53190 0,28675 1,68993 0,92408 0,51602 0,09153 1,53163 0,96000 0,47253 0,13760 1,57013 4,792 53,24
36 WOAm whale optimization algorithm M 0,93893 0,59477 0,26695 1,80065 0,98036 0,53873 0,07112 1,59021 0,78667 0,47600 0,11892 1,38159 4,772 53,02
37 CSO competitive swarm optimizer 0,85151 0,60786 0,29896 1,75833 0,84085 0,58491 0,11974 1,54550 0,80000 0,48560 0,14184 1,42744 4,731 52,57
38 FBA fractal-based algorithm 0,69419 0,64267 0,28955 1,62641 0,99812 0,54905 0,08705 1,63422 0,76133 0,51253 0,13689 1,41075 4,671 51,90
39 ECOi eco-inspired evolutionary algorithm 0,78817 0,54402 0,29360 1,62579 0,88996 0,46592 0,09747 1,45335 0,78533 0,45173 0,14295 1,38001 4,459 49,54
40 BSO brain storm optimization 0,92207 0,57625 0,29732 1,79564 0,80764 0,42508 0,09448 1,32720 0,77200 0,36533 0,13065 1,26798 4,391 48,79
41 CAm camel algorithm M 0,71534 0,56917 0,35985 1,64436 0,84094 0,47174 0,10850 1,42118 0,70400 0,41947 0,19563 1,31910 4,385 48,72
42 ACOm ant colony optimization_M 0,71885 0,48410 0,30990 1,51285 0,75792 0,48639 0,11871 1,36302 0,83600 0,48667 0,16148 1,48415 4,360 48,44
43 BSO brain storm optimization 0,91331 0,55813 0,29705 1,76849 0,85329 0,44038 0,09447 1,38814 0,72267 0,32880 0,13325 1,18472 4,341 48,23
44 AEFA artificial electric field algorithm 0,88798 0,65769 0,25276 1,79843 0,95550 0,63602 0,03946 1,63098 0,60800 0,16000 0,09845 0,86645 4,296 47,73
45 SOA simple optimization algorithm 0,91664 0,47040 0,27095 1,65799 0,88264 0,31546 0,06044 1,25854 0,82933 0,31733 0,11579 1,26245 4,179 46,43
CHA color_harmony_algorithm 0,58612 0,46909 0,30514 1,36035 0,47024 0,21050 0,07964 0,76038 0,42000 0,24373 0,12732 0,79105 2,912 32,35
RW random walk 0,49970 0,32333 0,25791 1,08094 0,30754 0,11470 0,04400 0,46624 0,36133 0,17013 0,10244 0,63390 2,181 24,23


Выводы

CHA — пример того, как привлекательная метафора и как это математическая природа алгоритма могут расходиться, и проявляется это в процессе практической реализации. Структура круга тонов и правила гармоничного сочетания цветов выглядят содержательно, но базовый оператор кроссовера (линейная смесь) сводит весь алгоритм к движению внутри выпуклой оболочки начальной популяции. Без явного механизма, выводящего потомков за пределы этой оболочки, CHA не может оптимизировать функции, у которых оптимум не попал в стартовое облако. Мы добавили такой механизм — гауссово возмущение с cooling-графиком — и алгоритм начал работать. Но нужно понимать, что это не часть оригинальной формулировки.

В процессе доводки реализации до работоспособного состояния пришлось отступить от точного описания статьи в нескольких местах. В оригинале правило обновления круга включает пятнадцать шагов с перекрёстной фильтрацией кандидатов между группами. На практике эти фильтры в начальной фазе работы блокируют большинство кандидатов и сильно замедляют движение популяции. Шаг 6 («агент группы другого родителя должен быть лучше агента группы текущего слота») удалён полностью; шаг 8 («группа с минимальным средним фитнесом») заменён на простой выбор кандидата с максимальным фитнесом из подходящих.

Оригинальные шаги 4-12 правил круга строят глобальное соответствие между кандидатами и слотами. Это даёт примерно 5-10 принятых кандидатов из 40 — три четверти оценок FF тратятся впустую. Заменено на greedy: каждый слот сравнивает помещённого в него кандидата со своим snapshot до Moving, лучший побеждает. Все 40 кандидатов имеют шанс на вход в популяцию. Главное расширение, амплитуда возмущения, которая адаптивно меняется.

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

Слабые стороны — низкая эффективность на функциях с сильно неравномерным ландшафтом. Для задач глобальной оптимизации в высоких размерностях с нерегулярными целевыми функциями стоит выбирать алгоритмы из верхней части рейтинговой таблицы.

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

tab

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

chart


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


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

Плюсы:

  1. Решает простые задачи.

Минусы:

  1. Большое количество внешних параметров.
  2. Склонность к застреванию.
  3. Высокая сложность настройки и отладки алгоритма. 

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



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

# Имя Тип Описание
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 Test_AO_All.mq5
Скрипт Единый испытательный стенд для всех популяционных алгоритмов оптимизации
9 Test_AO_AntiCheat Скрипт Тест на читерство алгоритмов оптимизации
10 Simple use of population optimization algorithms.mq5
Скрипт
Простой пример использования популяционных алгоритмов оптимизации без визуализации
11 Test_AO_CHA.mq5
Скрипт Испытательный стенд для CHA
Прикрепленные файлы |
CHA.zip (412.89 KB)
Разработка инструментария для анализа Price Action (Часть 38): VWAP на основе тикового буфера и модуль расчета дисбаланса на коротком окне Разработка инструментария для анализа Price Action (Часть 38): VWAP на основе тикового буфера и модуль расчета дисбаланса на коротком окне
В Части 38 мы создаем для MT5 панель мониторинга промышленного уровня, которая преобразует необработанные тики в практические торговые сигналы. Советник накапливает тиковые данные для расчета тиковой VWAP (Volume Weighted Average Price, средневзвешенной по объему цены), метрики дисбаланса (индикатора потока Flow) на коротком окне и размера позиции на основе ATR. Затем он отображает спред, ATR и индикатор потока в виде столбиков с минимальным мерцанием. Система рассчитывает рекомендуемый размер лота и стоп 1R, а также выдает настраиваемые алерты для узкого спреда, сильного потока и ситуаций с торговым преимуществом. Автоматическая торговля намеренно отключена; основное внимание уделяется надежной генерации сигналов и удобству использования.
Как обучить MLP на признаках марковской цепи в MQL5 Как обучить MLP на признаках марковской цепи в MQL5
Статья описывает двухуровневый индикатор MarkovMLPOscillator: трехсостоянная марковская цепь на истории строит матрицу переходов и формирует 15 вероятностных признаков для каждого бара, а MLP обучается на них и прогнозирует направление через заданный горизонт. Рассмотрены генерация признаков, схема валидации на отложенной выборке и настройки параметров. Результат — интерпретируемый осциллятор с цветовой гистограммой, сглаженным сигналом и отображением текущей матрицы переходов.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Нейросети в трейдинге: Оценка риска по несогласованности представлений (Окончание) Нейросети в трейдинге: Оценка риска по несогласованности представлений (Окончание)
В статье представлена инженерная реализация ReGEN-TAD для онлайн-обработки: единый вычислительный конвейер с магистралью (backbone) и универсальной генеративной головой прогнозирования/уточнения/реконструкции. Разобрана организация прямого и обратного прохода с запаздывающей обратной связью и контроль согласованности представлений. Тестирование в потоковом режиме иллюстрирует поведение системы и ограничения по риску; читатель получает готовую схему интеграции в торговый конвейер.