preview
Алгоритм андского кондора — Andean Condor Algorithm (ACA)

Алгоритм андского кондора — Andean Condor Algorithm (ACA)

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

Содержание

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


Введение

В предыдущих статьях этой серии за алгоритмами оптимизации сложилась простая иерархия: сверху — методы, которые на стандартных стендах уверенно превышают 60% общего балла; в середине — те, кто держится в диапазоне 45–55% и пригодны для прикладных задач; внизу — алгоритмы, тестировавшиеся авторами на одной-двух простых функциях и разваливающиеся при переходе на сложный или высокоразмерный ландшафт. Поиск перспективных алгоритмов поэтому всегда идёт по двум критериям сразу: насколько алгоритм воспроизводим из опубликованных источников и насколько он способен показать честные числа на жёстком наборе тестов.

Каждый, кто когда-либо подбирал параметры советника, хорошо знает, зачем это нужно. Пространство параметров большое, ландшафт фитнес-функции рваный и многомодальный. Логичный шаг — обратиться к специализированным метаэвристикам, которые умеют балансировать разведку и эксплуатацию пространства решений. Но новых алгоритмов выходит много, и большинство сопровождается уверенными заявлениями об эффективности, выведенными из тестов на одной-двух унимодальных функциях. На сфере отлично выглядят алгоритмы, которые на реальных задачах рассыпаются — поэтому в этой серии статей каждый кандидат проходит проверку на трёх стендах (Hilly, Forest, Megacity) и на трёх размерностях (10, 50 и 1000 переменных).

Сегодняшний кандидат — Andean Condor Algorithm, или ACA, предложенный в 2018 году Boris Almonacid и Ricardo Soto для задачи формирования производственных ячеек, и расширенный самим Almonacid в 2019 году в статье PeerJ для непрерывных доменов. Метафора — паттерн перемещения андского кондора в поисках пищи, который меняется от сезона к сезону: то широкий радиус полёта над предгорьями, то локальные облёты вокруг найденного источника пищи. Это типичная двухфазная структура exploration / intensification, но с любопытным механизмом баланса: performance indicator, опирающийся на среднее значение фитнеса популяции. Кондоры с фитнесом выше среднего переходят к интенсификации, остальные продолжают разведку.

Главная сложность реализации обнаружилась в источниках. Статья автора алгоритма от 2019 года описывает только операторы перемещения, а ключевые детали механизма баланса — что именно делает performance indicator, как из него вычисляется статус каждого кондора, какую роль играют параметры DP и PC — отсылают к статье 2018 года. Поэтому часть пути этой статьи занимает реконструкция: как из аннотации, описаний операторов и общего понимания метаэвристик собрать рабочий вариант ACA. Затем сверить его поведение с теми числами, которые автор всё-таки опубликовал на тестовой задаче.

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


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

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

Структура агента. Каждый кондор хранит три блока информации: вектор переменных x=(x1,x2,,xn)x = (x_1, x_2, \ldots, x_n) , представляющий точку в пространстве поиска и ограниченный коробкой [LB,UB][LB, UB]  по каждой координате; статус S{T,F}S \in \{T, F\} , указывающий текущий режим (интенсификация или разведка); и приспособленность fRf \in \mathbb{R}  — значение целевой функции в этой точке.

Инициализация популяции. На старте каждая координата каждого кондора получает равномерно распределённое случайное значение в пределах своих границ:

xi(0)U(LBi, UBi),i=1,,n

Стартовые значения приспособленности рассчитываются однократно, статусы при первом запуске можно инициализировать произвольно — на следующей же итерации они будут переназначены.

Performance indicator и присвоение статуса. Это самая интересная часть алгоритма. На каждой итерации популяция сортируется по убыванию приспособленности, и кондорам с верхнего конца ранжированного списка присваивается статус интенсификации, остальным — статус разведки. Граница между группами задаётся параметром DP[0,1]DP \in [0, 1]  (Distribution Parameter) — доля популяции, попадающая в группу интенсификации.

Формально, пусть r(i){0,1,,N1}r(i) \in \{0, 1, \ldots, N-1\}  — ранг агента ii  после сортировки по убыванию (лучший имеет ранг 0), NN  — размер популяции. Тогда:

Si={T (intensification),если r(i)/N<DPF (exploration),иначеS_i = \begin{cases} T \text{ (intensification)}, & \text{если } r(i)/N < DP \\ F \text{ (exploration)}, & \text{иначе} \end{cases}

После этого с вероятностью PC[0,1]PC \in [0, 1]  (Perturbation Coefficient) статус каждого агента инвертируется. Эта стохастическая поправка добавляет небольшой шум к жёсткому ранговому разделению — позволяет иногда худшим агентам перейти в режим доводки, а лучшим — в режим разведки, что помогает алгоритму избежать преждевременной сходимости.

Здесь важная оговорка. В статье 2019 года описание (performance indicator) не приведено напрямую — автор ссылается на статью 2018 года. Изложенная схема ранжирования и порога DPDP  со стохастической инверсией PCPC  — это реконструкция, опирающаяся на аннотацию основной статьи (где упоминаются ranking и average fitness) и общую логику метаэвристик группы. Альтернативные интерпретации механизма (например, прямое сравнение каждого агента со средним фитнесом популяции) на тестах дают близкие, но более шумные результаты.

Пример. Пусть популяция из 4 кондоров имеет приспособленности f=(3,7,1,5)f = (3, 7, 1, 5)  по индексам, DP=0.5DP = 0.5 , PC=0.2PC = 0.2 . После сортировки по убыванию ранги получаются: агент 1 (f=7) — ранг 0, агент 3 (f=5) — ранг 1, агент 0 (f=3) — ранг 2, агент 2 (f=1) — ранг 3. Проверяем условие r/N<0.5r/N < 0.5 : агенты 1 и 3 (нормированные ранги 0.0 и 0.25) получают статус TT , агенты 0 и 2 (0.5 и 0.75) — статус FF . Затем с вероятностью 0.2 у каждого статус может инвертироваться — в среднем половина популяции в каждой итерации работает на интенсификации, половина на разведке, но конкретные кондоры в этих группах меняются.

Оператор разведки (exploration). Применяется к агентам со статусом FF . У выбранного кондора случайным образом отбирается половина координат — индексы J{1,,n}J \subset \{1, \ldots, n\} , J=n/2|J| = \lfloor n/2 \rfloor . Для каждой iJi \in J  применяется:

xi(t+1)=xi(t)+δi,δiU(LBixi(t), UBixi(t))x_i^{(t+1)} = x_i^{(t)} + \delta_i, \quad \delta_i \sim U(LB_i - x_i^{(t)},\ UB_i - x_i^{(t)})

Если развернуть это выражение, получаем xi(t+1)[LBi,UBi]x_i^{(t+1)} \in [LB_i, UB_i] , что эквивалентно полной переинициализации этой координаты равномерным распределением во всём допустимом диапазоне. Остальные половины координат остаются без изменений. С точки зрения геометрии — это прыжок кондора в случайную точку гиперплоскости, ортогональной к подпространству оставшихся координат.

Пример. Кондор в 4-мерной задаче с [LB,UB]=[100,100][LB, UB] = [-100, 100]  занимает положение x=(50,30,80,10)x = (-50, 30, 80, -10) . Случайно выбираются координаты 1 и 3. Для координаты 1: δ1U(10030, 10030)=U(130,70)\delta_1 \sim U(-100 - 30,\ 100 - 30) = U(-130, 70) , новое значение x1x_1  лежит в (100,100)(-100, 100) . Для координаты 3: δ3U(100+10, 100+10)=U(90,110)\delta_3 \sim U(-100 + 10,\ 100 + 10) = U(-90, 110) , новое значение x3x_3 ​ также в (100,100)(-100, 100) . Координаты 0 и 2 не меняются. Например, при δ1=55\delta_1 = -55  и δ3=75\delta_3 = 75  новое положение кондора: xnew=(50,25,80,65)x_{new} = (-50, -25, 80, 65) .

Оператор интенсификации (intensification). Применяется к агентам со статусом TT . Случайно выбирается одна координата j{1,,n}j \in \{1, \ldots, n\}  и случайно выбирается тип воздействия θ{1,2,3,4}\theta \in \{1, 2, 3, 4\}  с равными вероятностями. Изменение производится по правилу:

xj(t+1)=xj(t)+{α,θ=1,αU(0.3LBj, 0.3UBj)β,θ=2,βU(0.1LBj, 0.1UBj)γ,θ=3,γU(1, 1)ε,θ=4,εU(0.05, 0.05)x_j^{(t+1)} = x_j^{(t)} + \begin{cases} \alpha, & \theta = 1, \quad \alpha \sim U(0.3 \cdot LB_j,\ 0.3 \cdot UB_j) \\ \beta, & \theta = 2, \quad \beta \sim U(0.1 \cdot LB_j,\ 0.1 \cdot UB_j) \\ \gamma, & \theta = 3, \quad \gamma \sim U(-1,\ 1) \\ \varepsilon, & \theta = 4, \quad \varepsilon \sim U(-0.05,\ 0.05) \end{cases}

Это четыре масштаба возмущения в одно действие. Для типичного симметричного домена [100,100][-100, 100]  оператор α\alpha  даёт скачок до ±30\pm 30  (крупный, фактически прыжок в новую часть пространства), β\beta  — до ±10\pm 10  (средний), γ\gamma  — до ±1\pm 1  (точная доводка), ε\varepsilon  — до ±0.05\pm 0.05  (микро-доводка). Случайный равновероятный выбор между ними даёт алгоритму встроенный механизм многомасштабного поиска: одна и та же популяция в один и тот же момент может нащупывать удалённые перспективные области через α\alpha  и шлифовать найденное оптимальное значение через ε\varepsilon .

Пример. Тот же кондор x=(50,30,80,10)x = (-50, 30, 80, -10) . Выпало j=2j = 2 , θ=3\theta = 3  (то есть γ\gamma ): γU(1,1)\gamma \sim U(-1, 1) , допустим γ=0.4\gamma = 0.4 . Новое значение x2=80+0.4=80.4x_2 = 80 + 0.4 = 80.4 . Остальные координаты не изменяются. Если бы вместо этого выпало θ=1\theta = 1 : αU(30,30)\alpha \sim U(-30, 30) , допустим α=25\alpha = -25 , тогда x2=55x_2 = 55 x2​=55 — это уже существенный скачок, переносящий кондора в принципиально другую область по этой координате.

Greedy acceptance. После того как все кондоры сгенерировали свои новые положения, целевая функция вычисляется во всех новых точках. Принятие нового положения — жадное: если приспособленность новой позиции не хуже старой, кондор остаётся в новой; иначе откатывается в положение перед итерацией.

x(t+1)={xnew,если f(xnew)f(x(t))x(t),иначеx^{(t+1)} = \begin{cases} x_{\text{new}}, & \text{если } f(x_{\text{new}}) \geq f(x^{(t)}) \\ x^{(t)}, & \text{иначе} \end{cases}

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

ACA

Рисунок 1. Схема работы алгоритма ACA

Иллюстрация показывает три шага одной итерации:

Step 1. Ранжирование четырёх кондоров по фитнесу с пунктирной линией порога DP=0.5: верхние два (A₁, A₂) — синие, intensification; нижние (A₃, A₄) — оранжевые, exploration. Справа отдельный блок показывает PC-механизм стохастической инверсии статуса.

Step 2a(синяя панель). Вектор координат с подсвеченной случайно выбранной координатой x₃, и четыре масштаба возмущения α/β/γ/ε визуализированы прямоугольниками-полосами, чьи размеры передают разницу в порядках амплитуды: от крупного прыжка ±30 до микро-доводки ±0.05.

Step 2b(оранжевая панель). Вектор с тремя случайно выбранными координатами и шкала диапазона [LB, UB] с точками, показывающими равномерное распределение новых значений.

Step 3. Жадная проверка с двумя выходами: зелёная ветка Keep при fnewfsnapshotf_{\text{new}} \geq f_{\text{snapshot}}  и красная Rollback в противном случае, плюс указатель на следующую итерацию.

Псевдокод одной итерации алгоритма выглядит следующим образом:

Вход:  популяция кондоров {x_1, ..., x_N} с приспособленностями {f_1, ..., f_N}
       параметры DP, PC
       границы LB, UB

1. Сохранить снимок текущей популяции (для возможного отката)

2. Ранжирование:
   Отсортировать кондоров по убыванию f
   r(i) ← ранг агента i

3. Присвоение статуса:
   для каждого i = 1..N:
       S_i ← T, если r(i)/N < DP, иначе F
       с вероятностью PC: S_i ← !S_i

4. Перемещение:
   для каждого i = 1..N:
       если S_i = T:                                   // intensification
           j  ← случайная координата из {1, ..., n}
           θ  ← случайный выбор из {1, 2, 3, 4}
           ξ  ← сэмпл из распределения α / β / γ / ε по θ
           x_{i,j} ← x_{i,j} + ξ
           
       если S_i = F:                                   // exploration
           J ← случайные ⌊n/2⌋ координат
           для каждой j из J:
               δ ← U(LB_j − x_{i,j}, UB_j − x_{i,j})
               x_{i,j} ← x_{i,j} + δ

5. Вычислить приспособленности новых позиций

6. Greedy acceptance:
   для каждого i = 1..N:
       если f(x_new_i) < f(snapshot_i):
           откатить x_i в snapshot

7. Обновить глобальный рекорд x_B, f_B

Цикл повторяется до достижения заданного лимита вычислений целевой функции (в наших тестах — 10 000).

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

Можем перейти к разбору реализации. Класс C_AO_ACA наследуется от базового класса C_AO нашего фреймворка и подключается к стандартному жизненному циклу метода оптимизации через переопределение двух виртуальных функций — Moving() и Revision(). Снаружи он доступен через три параметра: размер популяции popSize, долю популяции под интенсификацию DP и вероятность стохастической инверсии статуса PC. Значения по умолчанию выставлены под авторскую рекомендацию (popSize = 25, DP = 0.5, PC = 0.2), однако, как будет показано в следующем разделе, эти настройки далеко не оптимальны на сложных тестовых функциях, и реальный пик эффективности достигается при кардинально меньшей популяции.

В конструкторе задаются строковое имя алгоритма (ao_name), его текстовое описание (ao_desc) и ссылка на статью. Помимо этого инициализируется массив params[], через который параметры алгоритма прокидываются во внешний тестовый стенд и его input-параметры. Это стандартная процедура для всех алгоритмов фреймворка.

Метод SetParams() считывает значения параметров из params[] обратно во внутренние поля класса и накладывает предохранители: значения DP и PC ограничены диапазоном [0, 1]. Эта обработка нужна на случай, если внешний тестер передаст некорректные значения.

//+------------------------------------------------------------------+
class C_AO_ACA : public C_AO
 {
public:
                    ~C_AO_ACA() {}
                     C_AO_ACA()
   {
    ao_name = "ACA";
    ao_desc = "Andean Condor Algorithm";
    ao_link = "https://www.mql5.com/ru/articles/22525";

    popSize = 25;
    DP      = 0.5;
    PC      = 0.2;

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

  void               SetParams()
   {
    popSize = (int)params [0].val;
    DP      = params      [1].val;
    PC      = params      [2].val;

    //--- предохранители

    if(DP < 0.0)
      DP = 0.0;
    if(DP > 1.0)
      DP = 1.0;
    if(PC < 0.0)
      PC = 0.0;
    if(PC > 1.0)
      PC = 1.0;
   }

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

  void               Moving();
  void               Revision();

  //--- видимые параметры
  double             DP;     // доля популяции под intensification (0..1)
  double             PC;     // вероятность инверсии статуса (0..1)

private:
  //--- snapshot для greedy acceptance
  double             snap_c  [];   // [popSize * coords] — плоский буфер
  double             snap_f  [];   // [popSize]

  //--- ранжирование и статусы
  int                rankIdx [];   // [popSize]
  bool               status  [];   // [popSize] true = intensification

  //--- буфер выбора координат для exploration (Fisher–Yates partial)
  int                shuf    [];   // [coords]
 };
//+------------------------------------------------------------------+

Метод Init() выполняет одноразовую подготовку алгоритма к работе. Сначала вызывается StandardInit() базового класса — он аллоцирует массив агентов a[], нормирует входные диапазоны и инициализирует служебные поля. Затем заполняется начальная популяция: каждой координате каждого кондора присваивается случайное значение, равномерно распределённое в её допустимом диапазоне [LB_c, UB_c].

Эти значения записываются в массив cP[], а не в c[] — это конвенция фреймворка, по которой cP[] хранит предыдущее положение агента, c[] — текущее, а cB[] — лучшее за всю историю работы агента. На самом первом обращении к Moving() значения cP[] будут перенесены в c[] для оценки целевой функции.

После этого аллоцируются внутренние буферы класса: snap_c (плоский одномерный массив размера popSize × coords для сохранения снимков положений), snap_f (массив фитнесов снимка), rankIdx (перестановка индексов после сортировки), status (булев массив статусов агентов) и shuf (вспомогательный буфер для частичной перетасовки Фишера-Йейтса при выборе координат в exploration).

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

//--- начальная случайная популяция в cP[] (попадёт в c[] на первом Moving)
  for(int i = 0; i < popSize; i++)
    for(int c = 0; c < coords; c++)
      a [i].cP [c] = u.RNDfromCI(rangeMin [c], rangeMax [c]);

//--- внутренние буферы
  ArrayResize(snap_c,  popSize * coords);
  ArrayResize(snap_f,  popSize);
  ArrayResize(rankIdx, popSize);
  ArrayResize(status,  popSize);
  ArrayResize(shuf,    coords);

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

Метод Moving() — рабочая лошадка алгоритма, генерирующая новые положения агентов в каждой итерации. Первый вызов обрабатывается особо: если флаг revision ещё не поднят (то есть это самый старт работы), то c[] заполняется из cP[] через утилиту u.SeInDiSp(), которая попутно дискретизирует значение с учётом заданного шага и подгоняет под границы. Дальше внешний цикл оценит начальную популяцию, и управление вернётся к Revision().

На втором и всех последующих вызовах Moving() выполняется собственно итерация. Прежде всего, текущее положение всех агентов и их фитнесы сохраняются в snap_c и snap_f — этот снимок понадобится позже для greedy-отката. Затем популяция ранжируется по убыванию приспособленности обычной пузырьковой сортировкой (при popSize ≤ 25 этого более чем достаточно по производительности, и нет смысла подключать что-то более изощрённое).

Для каждого агента вычисляется нормированный ранг r/N, и по сравнению его с порогом DP присваивается статус: T для верхней DP-доли популяции, F для остальных. После этого выполняется стохастическая инверсия — с вероятностью PC статус агента переворачивается, что добавляет шум к жёсткому ранговому разделению.

Когда статусы расставлены, начинается генерация новых положений. Агенты со статусом T проходят через ветку интенсификации: случайно выбирается одна координата j из coords, случайно выбирается тип воздействия θ из четырёх вариантов (0..3), и к значению snap_c[i × coords + j] прибавляется соответствующее случайное приращение — выборка из U(0.3·LB, 0.3·UB) для θ=0 (α-оператор), U(0.1·LB, 0.1·UB) для θ=1 (β), U(−1, 1) для θ=2 (γ) или U(−0.05, 0.05) для θ=3 (ε). Остальные координаты агента остаются без изменений.

Агенты со статусом F идут через ветку exploration: половина координат выбирается без повторений через частичный shuffle Фишера-Йейтса (нам не нужно перемешивать весь массив shuf, достаточно прогнать алгоритм для первых ⌊n/2⌋ позиций), и для каждой выбранной значение полностью переинициализируется случайным выбором в её допустимом диапазоне. Результат во всех ветках записывается в a[i].c с попутной дискретизацией через SeInDiSp().

//+------------------------------------------------------------------+
//|                            Moving                                |
//|                                                                  |
//|  Структура итерации:                                             |
//|    1. snapshot текущей популяции;                                |
//|    2. ранжирование по фитнесу (по убыванию — maximization);      |
//|    3. присвоение Status: верхняя DP-доля — intensification,      |
//|       нижняя — exploration;                                      |
//|    4. PC-инверсия каждого статуса;                               |
//|    5. для каждого агента — генерация нового кандидата в a[i].c.  |
//+------------------------------------------------------------------+
void C_AO_ACA::Moving()
 {
//--- первый прогон: cP → c, чтобы внешний цикл оценил FF
  if(!revision)
   {
    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;
   }

//--- 1. snapshot
  for(int i = 0; i < popSize; i++)
   {
    for(int c = 0; c < coords; c++)
      snap_c [i * coords + c] = a [i].c [c];
    snap_f [i] = a [i].f;
   }

//--- 2. ранжирование (по убыванию f)
  for(int i = 0; i < popSize; i++)
    rankIdx [i] = i;

  for(int i = 0; i < popSize - 1; i++)
    for(int j = i + 1; j < popSize; j++)
      if(snap_f [rankIdx [j]] > snap_f [rankIdx [i]])
       {
        int tmp = rankIdx [i];
        rankIdx [i] = rankIdx [j];
        rankIdx [j] = tmp;
       }

//--- 3-4. status + PC-инверсия
  for(int k = 0; k < popSize; k++)
   {
    int    slot     = rankIdx [k];
    double normRank = (double)k / (double)popSize;
    status [slot]   = (normRank < DP);

    if(u.RNDfromCI(0.0, 1.0) < PC)
      status [slot] = !status [slot];
   }

//--- 5. движение
  for(int i = 0; i < popSize; i++)
   {
    if(status [i])
     {
      //--- INTENSIFICATION: одна координата, один θ ∈ {1..4}
      int j = (int)u.RNDfromCI(0.0, (double)coords);
      if(j >= coords)
        j = coords - 1;
      if(j < 0)
        j = 0;

      int theta = (int)u.RNDfromCI(0.0, 4.0);
      if(theta > 3)
        theta = 3;
      if(theta < 0)
        theta = 0;

      double xi = 0.0;
      double LB = rangeMin [j];
      double UB = rangeMax [j];

      switch(theta)
       {
        case 0:
          xi = u.RNDfromCI(0.3 * LB, 0.3 * UB);
          break;  // α
        case 1:
          xi = u.RNDfromCI(0.1 * LB, 0.1 * UB);
          break;  // β
        case 2:
          xi = u.RNDfromCI(-1.0, 1.0);
          break;  // γ
        default:
          xi = u.RNDfromCI(-0.05, 0.05);                 // ε
       }

      double v = snap_c [i * coords + j] + xi;
      a [i].c [j] = u.SeInDiSp(v, rangeMin [j], rangeMax [j], rangeStep [j]);
     }
    else
     {
      //--- EXPLORATION: половина координат, δ ~ U[LB-x, UB-x]
      int half = coords / 2;
      if(half < 1)
        half = 1;

      // Fisher–Yates partial shuffle: первые half индексов — случайные без повторов
      for(int k = 0; k < coords; k++)
        shuf [k] = k;
      for(int k = 0; k < half; k++)
       {
        int r = k + (int)u.RNDfromCI(0.0, (double)(coords - k));
        if(r >= coords)
          r = coords - 1;
        if(r < k)
          r = k;
        int tmp  = shuf [k];
        shuf [k] = shuf [r];
        shuf [r] = tmp;
       }

      for(int kk = 0; kk < half; kk++)
       {
        int    j     = shuf [kk];
        double x_old = snap_c [i * coords + j];
        double LB    = rangeMin [j];
        double UB    = rangeMax [j];

        double delta = u.RNDfromCI(LB - x_old, UB - x_old);
        double v     = x_old + delta;
        a [i].c [j]  = u.SeInDiSp(v, rangeMin [j], rangeMax [j], rangeStep [j]);
       }
     }
   }
 }
//+------------------------------------------------------------------+

Метод Revision() вызывается тестовым стендом после того, как внешний цикл уже вычислил фитнес во всех новых положениях. Логика делится на три части. Первая — обновление личного и глобального рекордов: для каждого агента, если новая приспособленность лучше его лучшей за всю историю (a[i].fB), то текущее положение и фитнес запоминаются как новый личный рекорд; параллельно, если новое значение лучше глобального рекорда (fB класса), обновляется и он вместе с cB. Этот блок выполняется до greedy acceptance потому, что нас интересуют все случаи рекордного значения, даже если новое положение в итоге будет отброшено по жадному правилу.

Вторая часть срабатывает только на первом вызове Revision(): при revision == false устанавливается флаг revision = true, и метод выходит, оставляя начальную популяцию как есть — она уже оценена и зафиксирована. С третьего обращения к методу и далее начинает работать третья часть — собственно greedy acceptance. Для каждого агента сравниваются текущая приспособленность a[i].f с сохранённой snap_f[i]. Если новая хуже снимка — выполняется откат: координаты восстанавливаются из snap_c[i × coords + c], фитнес — из snap_f[i]. Если новая не хуже — изменения принимаются как есть. Так формируется монотонная траектория каждого агента: его приспособленность за всё время работы не убывает.

Несколько технических примечаний об устройстве буферов. Снимок snap_c хранится как плоский одномерный массив длиной popSize × coords с row-major индексацией: snap_c[i × coords + c] — это значение координаты c у агента i. В классах фреймворка чаще используется обёртка вида S_AO_Vector с массивом v[] внутри, но в случае ACA нужен ровно один такой буфер, и заводить ради него отдельную структуру избыточно — плоского массива достаточно, а индексная арифметика умещается в одну строку. Буфер rankIdx[] заполняется в Moving() начальной перестановкой [0, 1, ..., N−1] и переставляется по убыванию snap_f[]. Буфер shuf[] переиспользуется на каждой итерации внутри ветки exploration: он содержит все индексы координат от 0 до n−1, и алгоритм частично перемешивает его — этого хватает, чтобы получить случайные ⌊n/2⌋ индексов без повторений за линейное время.

//+------------------------------------------------------------------+
//|                           Revision                               |
//|                                                                  |
//|  Стандартное обновление fB/cB + greedy acceptance: если новый    |
//|  фитнес хуже снапшота — откат координат и фитнеса агента.        |
//+------------------------------------------------------------------+
void C_AO_ACA::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 = true;
    return;
   }

//--- greedy acceptance: rollback если стало хуже
  for(int i = 0; i < popSize; i++)
   {
    if(a [i].f < snap_f [i])
     {
      for(int c = 0; c < coords; c++)
        a [i].c [c] = snap_c [i * coords + c];
      a [i].f = snap_f [i];
     }
   }
 }
//+------------------------------------------------------------------+


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

Автор статьи рекомендует размер популяции в 25 кондоров — и при этом значении алгоритм показывает 41,42% общего балла, что заметно ниже порога эффективности около 48%, и выходит за пределы рейтинга. 

ACA|Andean Condor Algorithm|25.0|0.5|0.2|
=============================
5 Hilly's; Func runs: 10000; result: 0.8052284199791254
25 Hilly's; Func runs: 10000; result: 0.4537792998744926
500 Hilly's; Func runs: 10000; result: 0.2666586996118199
=============================
5 Forest's; Func runs: 10000; result: 0.6651158271894628
25 Forest's; Func runs: 10000; result: 0.2311713783192359
500 Forest's; Func runs: 10000; result: 0.052890991104486404
=============================
5 Megacity's; Func runs: 10000; result: 0.8093333333333333
25 Megacity's; Func runs: 10000; result: 0.33359999999999995
500 Megacity's; Func runs: 10000; result: 0.11008000000000095
=============================
All score: 3.72786 (41.42%)

Однако при радикальном сокращении популяции эффективность алгоритма резко возрастает — общий балл доходит до 53.02%, и ACA уверенно входит в таблицу рейтинга. Эта инверсия — материал для отдельного разговора.

ACA|Andean Condor Algorithm|2.0|0.5|0.2|
=============================
5 Hilly's; Func runs: 10000; result: 0.7844401742263607
25 Hilly's; Func runs: 10000; result: 0.5326017349427434
500 Hilly's; Func runs: 10000; result: 0.33108785513596034
=============================
5 Forest's; Func runs: 10000; result: 0.7907177664055441
25 Forest's; Func runs: 10000; result: 0.44960020772191805
500 Forest's; Func runs: 10000; result: 0.10685016826475134
=============================
5 Megacity's; Func runs: 10000; result: 0.9226666666666667
25 Megacity's; Func runs: 10000; result: 0.6773333333333332
500 Megacity's; Func runs: 10000; result: 0.17613600000000043
=============================
All score: 4.77143 (53.02%)

Тест анти-чит показывает сближенную динамику первых показателей функций предыдущего основного теста. Честный алгоритм.

ACA|Andean Condor Algorithm|2.0|0.5|0.2|
=============================
Composite test: Hilly + Forest + Megacity + Peaks + Skin
Coordinates: 10; Epochs: 10000; Repeats: 10
=============================
Run 1/10: 0.8549286580531176
Run 2/10: 0.7878587377860654
Run 3/10: 0.7878587377860654
Run 4/10: 0.7878587377860654
Run 5/10: 0.7878587377860654
Run 6/10: 0.7878587377860654
Run 7/10: 0.7878587377860654
Run 8/10: 0.786583205099778
Run 9/10: 0.786583205099778
Run 10/10: 0.786583205099778
=============================
Average result: 0.7941830700 (79.42%)
=============================

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

Hilly

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

Forest

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

Megacity

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

Ackley

ACA на функции Ackley

GoldsteinPrice

ACA на функции GoldsteinPrice

По итогам тестирования алгоритм ACA занимает 37-ю позицию в рейтинге.

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)
1ANSacross neighbourhood search1,000000,882280,401382,283661,000000,952810,280922,233730,946670,857330,223892,027896,54572,72
2AMOmanimal migration optimization M0,916240,836030,467902,220170,984820,920100,363912,268830,917330,817070,251771,986176,47571,94
3CLAcode lock algorithm (joo)0,951390,861990,378792,192170,993490,935000,264972,193460,936000,842670,240602,019276,40571,17
4(P+O)ES(P+O) evolution strategies0,865710,895390,397402,158500,977610,898200,268782,144590,921330,802400,239521,963256,26669,62
5SDSmstochastic diffusion search M0,951950,849440,362492,163880,980610,884570,221122,086300,922670,790130,213801,926606,17768,63
6AAmarchery algorithm M0,846850,733200,425902,005950,967090,778370,277892,023350,861330,777070,287121,925525,95566,17
7SIAsimulated isotropic annealing (joo)0,935430,865040,384832,185300,940690,806090,238351,985130,864000,661600,195361,720965,89165,46
8TETAtime evolution travel algorithm (joo)0,914520,863690,255792,034000,996540,912910,143942,053390,854670,822130,104431,781235,86965,21
9ESGevolution of social groups (joo)0,981110,798570,311672,091350,989540,822700,150321,962560,921330,734400,153151,808885,86365,14
10CTAcomet tail algorithm (joo)0,924350,867860,278382,070590,990390,845710,194482,030580,954670,696800,110081,761555,86365,14
11ECBOenhanced colliding bodies optimization0,940240,723630,323561,987430,994770,802910,130561,928240,876000,701600,174331,751935,66862,98
12DAdialectical algorithm0,931170,754000,262051,947220,989250,813750,086621,889620,926670,681070,113151,720895,55861,76
13BBObiogeography based optimization0,958760,706090,357522,022370,929810,706600,169701,806110,874670,630130,208131,712935,54161,57
14BHAmblack hole algorithm M0,795580,762070,346821,904470,998360,757980,138261,894600,850670,644270,170201,665145,46460,71
15HSharmony search0,914200,690490,299241,903930,976270,733730,141931,851930,917330,627200,153641,698175,45460,60
16RFOroyal flush optimization (joo)0,809890,744810,345461,900160,952510,779260,151851,883620,804000,664270,190711,658985,44360,48
17BOAmbilliards optimization algorithm M0,761770,724210,252751,738730,908900,819600,288532,017030,837330,746130,097631,681095,43760,41
18ASOanarchy society optimization0,730700,737130,311951,779780,997320,877000,176192,050510,720000,687730,189881,597615,42860,31
19EOmextremal optimization_M0,765270,752050,319081,836400,999990,764260,124371,888620,841330,641330,152471,635135,36059,56
20ACSartificial cooperative search0,755450,771620,316531,843601,000000,804880,107051,911930,769330,608000,141571,518905,27458,60
21SSGsaplings sowing and growing0,754360,632060,359351,745770,919070,696940,197551,813560,818670,605330,213471,637475,19757,74
22AOSmatomic orbital search M0,761840,684350,313441,759630,900150,800440,115011,815600,828000,632800,156961,617765,19357,70
23TSEAturtle shell evolution algorithm (joo)0,958090,648520,295711,902320,995220,581040,105421,681680,921330,521600,145671,588605,17357,48
24DEdifferential evolution0,963980,623460,260891,848330,984820,770180,114591,869590,930670,362130,110001,402805,12156,90
25BIOblood inheritance optimization (joo)0,725800,665220,312281,703300,999950,681250,115401,796600,854670,593330,153641,601645,10256,69
26(PO)ES(PO) evolution strategies0,739720,581900,388961,710580,911990,599750,212621,724360,824000,562400,234321,620725,05656,18
27BObonobo optimizer0,755550,643660,326571,725780,943320,704420,139991,787730,734670,614400,167281,516355,03055,89
28SRAsuccessful restaurateur algorithm (joo)0,890100,633590,291151,814840,966340,552850,089141,608330,893330,528000,139111,560444,98455,38
29CROchemical reaction optimisation0,912810,656810,298661,868280,905130,560200,109391,574720,828000,501330,141491,470824,91454,60
30BCOmbacterial chemotaxis optimization M0,825890,617330,315841,759060,952960,637180,119841,709980,765330,516530,158001,439864,90954,54
31DOAdream optimization algorithm0,785220,781210,360361,926790,615840,421170,122541,159550,866670,725870,211271,803814,89054,33
32ABOafrican buffalo optimization0,922950,625280,298851,847080,929920,574680,093721,598320,733330,513330,143241,389904,83553,72
33BSAbird swarm algorithm0,944320,679410,264011,887740,916490,656190,120541,693220,809330,335470,106521,251324,83253,69
34TSmtabu search M0,878060,610400,289931,778390,981160,521650,085441,588250,826670,495470,135521,457664,82453,60
35BSAbacktracking search algorithm0,871280,531900,286751,689930,924080,516020,091531,531630,960000,472530,137601,570134,79253,24
36WOAmwhale optimization algorithm M0,938930,594770,266951,800650,980360,538730,071121,590210,786670,476000,118921,381594,77253,02
37ACAandean_condor_algorithm0,784440,532600,331081,648120,790710,449600,106851,347160,922660,677330,176131,776124,77153,02
38CSOcompetitive swarm optimizer0,851510,607860,298961,758330,840850,584910,119741,545500,800000,485600,141841,427444,73152,57
39FBAfractal-based algorithm0,694190,642670,289551,626410,998120,549050,087051,634220,761330,512530,136891,410754,67151,90
40ECOieco-inspired evolutionary algorithm0,788170,544020,293601,625790,889960,465920,097471,453350,785330,451730,142951,380014,45949,54
41BSObrain storm optimization0,922070,576250,297321,795640,807640,425080,094481,327200,772000,365330,130651,267984,39148,79
42CAmcamel algorithm M0,715340,569170,359851,644360,840940,471740,108501,421180,704000,419470,195631,319104,38548,72
43ACOmant colony optimization_M0,718850,484100,309901,512850,757920,486390,118711,363020,836000,486670,161481,484154,36048,44
44BSObrain storm optimization 0,913310,558130,297051,768490,853290,440380,094471,388140,722670,328800,133251,184724,34148,23
45AEFAartificial electric field algorithm0,887980,657690,252761,798430,955500,636020,039461,630980,608000,160000,098450,866454,29647,73
RWrandom walk0,499700,323330,257911,080940,307540,114700,044000,466240,361330,170130,102440,633902,18124,23


Выводы

В проведённом исследовании Andean Condor Algorithm показал себя как алгоритм со своим характером и со своими сильными сторонами. При настройке по авторской рекомендации (популяция 25, DP = 0.5, PC = 0.2) общий балл составил 41.42%. Однако при сокращении популяции до двух кондоров — алгоритм поднимается до 53.02% и уверенно входит в топ-45. На расширенном композите из пяти функций при размерности 10 он достигает 79.42%, что снимает подозрение на оверфит под три стандартные функции и подтверждает реальную силу при низкой размерности.

Особо стоит отметить поведение алгоритма на функции Megacity. Это дискретный ступенчатый ландшафт с плоскими плато между уровнями, и многие метаэвристики справляются с такой геометрией с трудом, на плато попросту не за что зацепиться, направление улучшения не считывается. У ACA здесь обнаруживается структурное преимущество. Многомасштабный intensification-оператор с четырьмя категориями возмущения позволяет одновременно искать правильную «плитку» большими α- и β-скачками и шлифовать положение внутри неё мелкими γ- и ε-шагами, а greedy-принятие естественно работает в условиях ступенчатого улучшения целевой функции. 

Внутреннее устройство ACA примечательно несколькими особенностями. Во-первых, это компактный алгоритм с минимальным набором параметров — три штуки, две из которых (DP и PC) задают баланс между интенсификацией и разведкой, а третий — размер популяции. Во-вторых, оператор exploration, переинициализирующий половину координат полностью случайным выбором в их полном диапазоне, — нечастый приём среди метаэвристик: большинство алгоритмов разведки делает плавные смещения с убывающим во времени радиусом, а здесь по выбранным осям происходит полный сброс. Это даёт стохастическую комбинацию из локальной доводки и принципиально нелокальных прыжков, причём пропорция между ними регулируется автоматически через ранжирование по фитнесу. В-третьих, отсутствие внутренней памяти между итерациями делает алгоритм лёгким в реализации, дешёвым по памяти и предсказуемым в поведении — каждая итерация локальна по времени и не зависит от долгой предыстории.

Уменьшение популяции улучшает качество работы алгоритма, оптимум приходится на два-четыре агента. Этому есть прозрачное объяснение в рамках фиксированного бюджета вычислений: при 10 000 FF-оценок меньшая популяция означает больше итераций на агента и, соответственно, больше касаний каждой координаты в режиме intensification. Поэтому авторская рекомендация в 25 кондоров оказывается на нашем тестовом наборе не оптимальной. 

Важная общая оговорка по всем результатам. Поскольку механизм performance indicator восстанавливался по аннотации основной статьи 2018 года, часть деталей реализации является интерпретацией: точная семантика параметров DP и PC, схема ранжирования, способ вычисления порога. Альтернативные интерпретации этих элементов на тестах дают близкие, но иные по характеру результаты. Это значит, что приведённые числа характеризуют нашу реконструированную реализацию ACA, а не оригинальную авторскую, которая, возможно, ведёт себя в каких-то нюансах иначе. 

С практической точки зрения трейдеру имеет смысл рассматривать ACA прежде всего для задач с дискретным или ступенчатым откликом целевой функции. Особенно — при размерности в пределах 20–30 параметров. Здесь алгоритм будет работать предсказуемо, быстро и с минимумом ручной настройки. Для задач с гладким непрерывным ландшафтом большой размерности подбор не настолько естественен, и стоит сравнивать ACA с алгоритмами, рассчитанными на такие условия.

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

tab

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

chart

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


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

Плюсы:

  1. Небольшое количество внешних параметров.
  2. Хорошие показатели на дискретной Megacity.

Минусы:

  1. Склонность к застреванию.

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



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

#ИмяТипОписание
1#C_AO.mqh
Включаемый файл
Родительский класс популяционных алгоритмов оптимизации
2#C_AO_enum.mqh
Включаемый файл
Перечисление популяционных алгоритмов оптимизации
3TestFunctions.mqh
Включаемый файл
Библиотека тестовых функций
4
TestStandFunctions.mqh
Включаемый файл
Библиотека функций тестового стенда
5TestStand3D.mqhВключаемый файл3D-панель визуализации для тестового стенда 
6Utilities.mqh
Включаемый файл
Библиотека вспомогательных функций
7CalculationTestResults.mqh
Включаемый файл
Скрипт для расчёта результатов в сравнительную таблицу
8Test_AO_All.mq5
СкриптЕдиный испытательный стенд для всех популяционных алгоритмов оптимизации
9Test_AO_AntiCheatСкриптТест на читерство алгоритмов оптимизации
10Simple use of population optimization algorithms.mq5
Скрипт
Простой пример использования популяционных алгоритмов оптимизации без визуализации
11Test_AO_ACA.mq5
СкриптИспытательный стенд для ACA
Прикрепленные файлы |
ACA.zip (407.18 KB)
MetaTrader 5: конструируйте рынок под стратегию — Renko/Range/Volume, синтетика и стресс-тесты на пользовательских символах MetaTrader 5: конструируйте рынок под стратегию — Renko/Range/Volume, синтетика и стресс-тесты на пользовательских символах
Показываем, как с помощью API пользовательских символов MetaTrader 5 превратить терминал в конструктор данных: генерировать вне‑временные графики Renko, Range и Equal‑Volume и собирать синтетические инструменты. Разбираем агрегацию тиков и модификацию истории для стресс‑тестов (расширение спреда, изменение стоп‑уровней) с учетом ограничений платформы. Даем практику работы с CiCustomSymbol и маршрутизацией приказов на реальный символ через обертку CustomOrder, с готовыми фрагментами кода.
Тестер стратегий для Python и MetaTrader 5 (Часть 05): Тестер стратегий для нескольких символов и таймфреймов Тестер стратегий для Python и MetaTrader 5 (Часть 05): Тестер стратегий для нескольких символов и таймфреймов
В этой статье представлен совместимый с MetaTrader 5 рабочий процесс бэктестинга, масштабируемый на разные символы и таймфреймы. Мы используем HistoryManager для параллельного сбора данных, синхронизации баров и тиков со всех таймфреймов и запуска изолированных по символам обработчиков OnTick в потоках. Вы узнаете, как режимы моделирования влияют на скорость и точность, когда стоит полагаться на данные терминала, как уменьшить операции ввода-вывода с помощью событийных обновлений и как собрать полноценного мультивалютного торгового робота.
Двумерные копулы в MQL5 (Часть 3): Реализация и настройка смешанных моделей копул Двумерные копулы в MQL5 (Часть 3): Реализация и настройка смешанных моделей копул
В статье наш набор инструментов для работы с копулами расширяется смешанными копулами, реализованными непосредственно в MQL5. Мы строим смеси Клейтона–Франка–Гумбеля и Клейтона–Стьюдента-t–Гумбеля, оцениваем их с помощью EM и вводим управление разреженностью через SCAD с кросс-валидацией. Предоставленные скрипты настраивают гиперпараметры, сравнивают смеси с использованием информационных критериев и сохраняют обученные модели. Практики могут применять эти компоненты для учета асимметричной хвостовой зависимости и встраивать выбранную модель в индикаторы или советники.
Автоматизация торговых стратегий в MQL5 (Часть 28): Создание гармонического паттерна "Летучая мышь" на основе Price Action с визуальной обратной связью Автоматизация торговых стратегий в MQL5 (Часть 28): Создание гармонического паттерна "Летучая мышь" на основе Price Action с визуальной обратной связью
В этой статье мы разработаем систему распознавания гармонических паттернов "Летучая мышь" на языке MQL5, которая определяет бычьи и медвежьи гармонические паттерны "Летучая мышь" с использованием пивотных точек и коэффициентов Фибоначчи, запускает сделки с точными уровнями входа, стоп-лосса и тейк-профита. Система также визуализирует паттерны с помощью графических объектов.