preview
Алгоритм оптимизации Архимеда — Archimedes Optimization Algorithm (AOA)

Алгоритм оптимизации Архимеда — Archimedes Optimization Algorithm (AOA)

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

Содержание

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


Введение

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

Задержим этот образ. Теперь представим, что водяной столб — это пространство поиска, а нужная плотность — решение некоторой задачи оптимизации. Тогда предмет, нашедший нейтральную плавучесть, в самом буквальном смысле эту задачу решил: он свёл действующую на себя силу к нулю. Именно эта идея лежит в основе Archimedes Optimization Algorithm (AOA), предложенного Hashim и соавторами в 2021 году, и именно она необычна.

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

Из такой постановки даром достаётся то, ради чего другие алгоритмы городят отдельные механизмы. Бросьте горсть объектов в неспокойную воду — поначалу они хаотично сталкиваются друг с другом. Это и есть разведка: широкий, ничем не направленный обзор пространства. Турбулентность затихает — объекты перестают сталкиваться и начинают плавно дрейфовать к своим положениям равновесия. Это эксплуатация: аккуратное дожатие найденного. Переход от первого режима ко второму в AOA не задаётся убывающим коэффициентом, навешенным поверх алгоритма, — он возникает сам, по мере того как затихает турбулентность. Баланс explore/exploit становится внутренним свойством модели, а не внешней надстройкой, которую приходится подгонять руками.

Стакан воды — это, конечно, игрушка. Настоящий водяной столб, ради которого всё затевается, — это пространство параметров торговой системы: периоды индикаторов, пороги, множители риска на дискретной сетке. Ландшафт фитнес-функции изломан и зашумлён, а каждый её вызов — дорогой прогон на истории. Бюджет таких вызовов измеряется тысячами, не миллионами. Требование к оптимизатору одно: при фиксированном бюджете (на стенде — 10 000 вызовов фитнеса) стабильно выдавать высокий результат от запуска к запуску, а не эпизодически. Связку высокое среднее по повторам плюс малый разброс при заданном бюджете мы и принимаем за решение. Это и измеряет рейтинговая таблица на функциях Hilly, Forest и Megacity в трёх размерностях.

Дальше — по делу. Разберём идею и математику AOA, переведём её в псевдокод, реализуем алгоритм в фреймворке C_AO, прогоним на стандартном стенде и посмотрим, какое место он займёт в рейтинге рядом с уже разобранными алгоритмами.


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

Мысленный эксперимент из введения AOA превращает в строгий алгоритм. Каждый объект популяции — это кандидат-решение, то есть точка X в пространстве поиска. Но, в отличие от обычного роевого агента, он несёт с собой ещё три физических атрибута: плотность den, объём vol и ускорение acc. Алгоритм итеративно подстраивает все четыре величины так, чтобы популяция сходилась к равновесию, — а конфигурация равновесия и есть искомый оптимум.

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

Инициализация. Создаётся популяция из N N  объектов. Каждый получает случайную стартовую позицию — формула (4):

X i = l b + rand ( u b l b ) X_i = lb + \text{rand} \cdot (ub - lb)

где l b lb , u b ub  — границы пространства поиска, rand \text{rand}  — равномерное случайное число из [ 0 , 1 ] [0,1] . Плотность и объём задаются случайно в диапазоне [ 0 , 1 ] [0,1]  — формула (5):

d e n i = rand , v o l i = rand den_i = \text{rand}, \qquad vol_i = \text{rand}

а ускорение — снова в границах поиска, формула (6):

a c c i = l b + rand ( u b l b ) acc_i = lb + \text{rand} \cdot (ub - lb)

Вся популяция оценивается, выбирается лучший объект, и его атрибуты запоминаются как X b e s t X_{best} , d e n b e s t den_{best} , v o l b e s t vol_{best} , a c c b e s t acc_{best} ​. В нашем фреймворке C_AO семантика максимизации, поэтому лучший — это объект с наибольшим значением фитнес-функции.

Обновление плотности и объёма. На каждой итерации t t  каждый объект подтягивает свою плотность и объём к плотности и объёму текущего лучшего — формула (7):

d e n i = d e n i + r ( d e n b e s t d e n i ) den_i = den_i + r \cdot (den_{best} - den_i)
v o l i = v o l i + r ( v o l b e s t v o l i ) vol_i = vol_i + r \cdot (vol_{best} - vol_i)

Здесь r [ 0 , 1 ] r \in [0,1]  — одно случайное число на всю популяцию за итерацию. Поскольку r r  и оба конца отрезка лежат в [ 0 , 1 ] [0,1] , это выпуклое смешивание: плотность и объём остаются в [ 0 , 1 ] [0,1]  и постепенно выравниваются по популяции — жидкость приходит к однородному состоянию.

Операторы перехода: TF и d. Переходом разведка → эксплуатация управляют две скалярные величины, зависящие только от номера итерации t t  и максимума t m a x t_{max} .

Оператор переноса (transfer operator) — формула (8):

T F = exp ( t t m a x t m a x ) , T F 1 TF = \exp\!\left(\frac{t - t_{max}}{t_{max}}\right), \qquad TF \le 1

Он монотонно растёт примерно от exp ( 1 ) 0,37 \exp(-1) \approx 0{,}37  в начале до 1 1  в конце. Это часы затихающей турбулентности из введения: малое T F TF  — вода неспокойна, объекты сталкиваются (разведка); большое T F TF  — вода успокоилась, объекты оседают (эксплуатация).

Фактор уменьшения плотности (density decreasing factor) — формула (9):

d = exp ( t m a x t t m a x ) t t m a x d = \exp\!\left(\frac{t_{max} - t}{t_{max}}\right) - \frac{t}{t_{max}}

Он убывает примерно от e 2,72 e \approx 2{,}72  до нуля. Если T F TF  выбирает режим, то d d  задаёт масштаб шага: широкие прыжки в начале, всё более мелкая подстройка к концу.

Обновление ускорения. Ускорение пересчитывается каждую итерацию, и формула зависит от режима, заданного T F TF .

Фаза столкновений (малое T F TF ) — формула (10). Объект i i  считается столкнувшимся со случайно выбранным объектом m r mr , и его новое ускорение следует из баланса выталкивающих сил для этой пары:

a c c i = d e n m r + v o l m r a c c m r rand d e n i v o l i acc_i = \frac{den_{mr} + vol_{mr} \cdot acc_{mr}}{\text{rand} \cdot den_i \cdot vol_i}​​

Ускорение диктуется случайным соседом — источник хаотичной, ненаправленной разведки.

Фаза без столкновений (большое T F TF ) — формула (11). Столкновения затихли, и ускорение объекта диктуется уже лучшим объектом:

a c c i = d e n b e s t + v o l b e s t a c c b e s t rand d e n i v o l i acc_i = \frac{den_{best} + vol_{best} \cdot acc_{best}}{\text{rand} \cdot den_i \cdot vol_i}​​

Теперь тяга каждого объекта направлена к конфигурации лидера — это направленная эксплуатация.

Замечание о пороге. В тексте оригинальной статьи граница между фазами описана значением T F = 0,5 TF = 0{,}5 . В C_AO — для ускорения используется порог  0,45 0{,}45 , а для обновления позиций ниже  0,4 0{,}4 0, для нашей реализации. 

Нормализация ускорения. Сырое ускорение из формул (10)–(11) может принимать значения в широком диапазоне, поэтому оно нормализуется по всей популяции в фиксированную полосу — формула (12):

a c c i n o r m = u a c c i a c c m i n a c c m a x a c c m i n + l acc_i^{\,norm} = u \cdot \frac{acc_i - acc_{min}}{acc_{max} - acc_{min}} + l

где u = 0,9 u = 0{,}9 , l = 0,1 l = 0{,}1 , а a c c m i n acc_{min}  и a c c m a x acc_{max}  берутся по всем объектам и всем координатам сразу. Результат лежит в [ 0,1 ; 1,0 ] [0{,}1;\,1{,}0] . Это и есть механизм самодиагностики, упомянутый во введении: нормализованное ускорение работает как индивидуальный регулятор длины шага. Объект далеко от оптимума получает высокое значение — ему ещё нужно много двигаться; объект рядом с оптимумом получает низкое — ему пора лишь дошлифовывать. Переход explore/exploit оказывается не только глобальным (через T F TF  и d d ), но и локальным, у каждого объекта своим.

Обновление позиций. Наконец объект перемещается, и режим снова выбирает T F TF .

Фаза разведки (малое T F TF ) — формула (13), применяется покоординатно:

X i = X i + C 1 rand a c c i n o r m ( X r a n d X i ) d X_i = X_i + C_1 \cdot \text{rand} \cdot acc_i^{\,norm} \cdot (X_{rand} - X_i) \cdot d

Объект шагает в сторону случайно выбранного объекта X r a n d X_{rand} Xrand​; C 1 = 2 C_1 = 2  — фиксированный масштаб. Шаг модулируется нормализованным ускорением (насколько двигаться) и фактором d d  (общее сжатие). Случайная цель плюс большое d d  в начале дают широкий охват пространства.

Фаза эксплуатации (большое T F TF ) — формула (14), покоординатно:

X i = X b e s t ± C 2 rand a c c i n o r m ( T X b e s t X i ) d X_i = X_{best} \pm C_2 \cdot \text{rand} \cdot acc_i^{\,norm} \cdot (T \cdot X_{best} - X_i) \cdot d

Теперь объект движется относительно глобально лучшего X b e s t X_{best} , C 2 = 6 C_2 = 6 . Добавляются две величины — формула (15):

p = 2 rand C 4 , T = C 3 T F ( T 1 ) p = 2 \cdot \text{rand} - C_4, \qquad T = C_3 \cdot TF \;\; (T \le 1)

Множитель T T  растёт вместе с T F TF  и задаёт, насколько сильно X b e s t X_{best}  притягивает объект. Знак шага выбирает p p : при p < 0,5 p < 0{,}5  объект приближается к лидеру с одной стороны (знак +), иначе — с другой (знак −). Этот выбор знака позволяет объектам прощупывать обе стороны от лидера, а не схлопываться все в одну точку, — защита от преждевременной сходимости. C 3 C_3  и C 4 C_4  — две настраиваемые ручки алгоритма (по умолчанию C 3 = 2 C_3 = 2 , C 4 = 0,5 C_4 = 0{,}5 ); C 1 C_1 , C 2 C_2 ​, u u , l l  зафиксированы.

Завершение итерации. Новые позиции загоняются в границы поиска и оцениваются. AOA применяет жадный отбор: объект принимает новую позицию только если она улучшила фитнес, иначе остаётся на месте. При этом плотность, объём и ускорение продолжают эволюционировать независимо от того, принялась позиция или нет, — это память объекта о физическом процессе. Лучший объект обновляется, и цикл повторяется, пока не исчерпан бюджет вызовов фитнеса. По мере роста t t  оператор T F стремится к   1, а фактор d d  — к нулю: столкновения прекращаются, шаги сжимаются, популяция оседает. Жидкость приходит в покой — и её равновесная конфигурация и есть ответ.

AOA_algorithm

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

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

Составим псевдокод алгоритма AOA.

ВХОД:
  N            — число объектов (Materials_no)
  T_max        — максимум итераций
  dim, lb, ub  — размерность и границы поиска
  C1 = 2, C2 = 6           — фиксированные константы
  C3, C4                   — настраиваемые (обычно C3 ∈ {1,2}, C4 ≈ 0.5)
  u = 0.9, l = 0.1         — диапазон нормализации ускорения, Eq.(12)

──────────── ИНИЦИАЛИЗАЦИЯ ────────────
для каждого объекта i = 1..N:
    X[i]   = lb + rand(dim) · (ub − lb)       // позиции,    Eq.(4)
    den[i] = rand(dim)                        // плотность,  Eq.(5)
    vol[i] = rand(dim)                        // объём,      Eq.(5)
    acc[i] = lb + rand(dim) · (ub − lb)       // ускорение,  Eq.(6)
    Y[i]   = f(X[i])

(Scorebest, idx) = min(Y)
Xbest = X[idx];  den_best = den[idx];  vol_best = vol[idx];  acc_best = acc[idx]
acc_norm = acc

──────────── ОСНОВНОЙ ЦИКЛ ────────────
для t = 1..T_max:

    //--- глобальные коэффициенты перехода
    TF = exp((t − T_max) / T_max)             // оператор переноса, Eq.(8)
    если TF > 1: TF = 1
    d  = exp((T_max − t)/T_max) − (t/T_max)   // фактор плотности,  Eq.(9)

    acc = acc_norm                            // ускорение с прошлой итерации
    r   = rand()                              // одно общее число на итерацию

    // --- обновление плотности, объёма, ускорения 
    для каждого объекта i = 1..N:
        den[i] = den[i] + r · (den_best − den[i])       // Eq.(7)
        vol[i] = vol[i] + r · (vol_best − vol[i])       // Eq.(7)

        если TF ≤ 0.45:                                 // СТОЛКНОВЕНИЯ (разведка)
            mr = случайный индекс 1..N
            acc_tmp[i] = (den[mr] + vol[mr]·acc[mr]) /
                         (rand · den[i]·vol[i])         // Eq.(10)
        иначе:                                          // БЕЗ СТОЛКНОВЕНИЙ (эксплуатация)
            acc_tmp[i] = (den_best + vol_best·acc_best) /
                         (rand · den[i]·vol[i])         // Eq.(11)

    // --- нормализация ускорения по ВСЕЙ популяции 
    a_min = min(acc_tmp по всем i и j)
    a_max = max(acc_tmp по всем i и j)
    для каждого i, j:
        acc_norm[i,j] = u · (acc_tmp[i,j] − a_min)/(a_max − a_min) + l   // Eq.(12)

    // --- обновление позиций 
    для каждого объекта i = 1..N:
        если TF ≤ 0.4:                                 // РАЗВЕДКА
            для каждого измерения j:
                mrand = случайный индекс 1..N
                Xnew[i,j] = X[i,j] + C1·rand·acc_norm[i,j]·
                            (X[mrand,j] − X[i,j])·d              // Eq.(13)
        иначе:                                         // ЭКСПЛУАТАЦИЯ
            для каждого измерения j:
                p = 2·rand − C4                                  // Eq.(15)
                T = C3 · TF;  если T > 1: T = 1
                если p ≤ 0.5:
                    Xnew[i,j] = Xbest[j] + C2·rand·acc_norm[i,j]·
                                (T·Xbest[j] − X[i,j])·d          // Eq.(14)
                иначе:
                    Xnew[i,j] = Xbest[j] − C2·rand·acc_norm[i,j]·
                                (T·Xbest[j] − X[i,j])·d

    // --- контроль границ 
    Xnew = clamp(Xnew, lb, ub)

    // --- жадный отбор 
    для каждого объекта i = 1..N:
        v = f(Xnew[i])
        если v < Y[i]:
            X[i] = Xnew[i];  Y[i] = v

    // --- обновление лучшего решения 
    (Ybest, idx) = min(Y)
    если Ybest < Scorebest:
        Scorebest = Ybest
        Xbest = X[idx]
        den_best = den[idx];  vol_best = vol[idx];  acc_best = acc_norm[idx]

ВЫХОД: Xbest, Scorebest

Можем перейти к рассмотрению реализации алгоритма AOA. 

Поля класса. Агент фреймворка ( S_AO_Agent ) несёт только координаты ( c, cP, cB, cW) и значения фитнеса. Но объекту AOA нужны ещё три физических атрибута — плотность, объём, ускорение, — и у каждого своя динамика. Поэтому они вынесены в отдельные приватные буферы класса: den , vol , accNorm и промежуточный accTemp. Все четыре — плоские одномерные массивы размером popSize * coords, элемент объекта i по координате c адресуется как [i * coords + c]. Плоская раскладка вместо двумерной выбрана ради простоты и предсказуемого доступа к памяти.

Ещё три буфера размером coords — denBest, volBest, accBest — хранят атрибуты лучшего объекта; они нужны в формуле (11). Пара snap_c и snap_f — снимок популяции (о нём ниже). Наконец, t и Tmax — счётчик и горизонт расписания итераций. Публичными остаются только две настраиваемые ручки — C3 и C4.

Конструктор задаёт паспорт алгоритма — имя AOA, описание, ссылку на статью — и значения по умолчанию: popSize = 25, C3 = 2.0, C4 = 0.5. Затем массив params расширяется до трёх элементов, и в каждый записывается имя и значение параметра. Через этот массив тестовый стенд видит и выставляет настройки алгоритма.

SetParams(). Метод считывает значения из params[] обратно в поля popSize, C3, C4 и ставит предохранители: размер популяции не меньше 2 (фазе столкновений нужен хотя бы один сосед), коэффициенты C3 и C4 не отрицательны. SetParams() вызывается до Init(), поэтому к моменту выделения памяти popSize уже корректен.

//+------------------------------------------------------------------+
class C_AO_AOAarch : public C_AO
 {
public:
                    ~C_AO_AOAarch() {}
                     C_AO_AOAarch()
   {
    ao_name = "AOA";
    ao_desc = "Archimedes Optimization Algorithm";
    ao_link = "https://www.mql5.com/ru/articles/22648";

    popSize = 25;
    C3      = 2.0;
    C4      = 0.5;

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

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

    //--- предохранители
    if(popSize < 2)            // столкновениям нужен хотя бы один сосед
      popSize = 2;
    if(C3 < 0.0)
      C3 = 0.0;
    if(C4 < 0.0)
      C4 = 0.0;
   }

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

  void               Moving();
  void               Revision();

  //--- видимые параметры
  double             C3;     // масштаб влияния лучшего: T = C3*TF,   Eq.(15)
  double             C4;     // смещение знака шага: p = 2*rand - C4, Eq.(15)

private:
  //--- физические атрибуты объектов (плоские буферы popSize*coords)
  double             den     [];   // плотность,                   Eq.(5),(7)
  double             vol     [];   // объём,                       Eq.(5),(7)
  double             accNorm [];   // ускорение (нормализованное), Eq.(12)
  double             accTemp [];   // ускорение до нормализации,   Eq.(10),(11)

  //--- атрибуты лучшего объекта (буферы coords)
  double             denBest [];
  double             volBest [];
  double             accBest [];

  //--- snapshot для генерации кандидатов и greedy acceptance
  double             snap_c  [];   // popSize*coords
  double             snap_f  [];   // popSize

  //--- расписание итераций
  int                t;            // номер текущей рабочей итерации (1..Tmax)
  int                Tmax;         // полное число рабочих итераций
 };
//+------------------------------------------------------------------+

Init(). Инициализация начинается с StandardInit() — базовый класс пересоздаёт массив агентов, копирует диапазоны поиска, сбрасывает fB и флаг revision. Дальше идёт специфика AOA.

Сначала настраивается расписание: Tmax = epochsP - 1. Вычитание единицы здесь принципиально — первая эпоха внешнего цикла уходит на оценку стартовой популяции, рабочих итераций остаётся epochsP - 1, и именно на этот горизонт должны быть растянуты операторы TF и d. Счётчик t обнуляется, Tmax страхуется снизу единицей.

Затем выделяются все буферы и заполняется стартовое состояние популяции: координаты — случайно в границах поиска (формула 4, кладутся в cP), плотность и объём — случайно из [0,1] (формула 5), ускорение — случайно в границах поиска (формула 6). Тонкость: стартовое ускорение пишется сразу в accNorm, а не в отдельный буфер. Так сделано потому, что на первой рабочей итерации ускорение acc совпадает с acc_norm, и хранить их раздельно нет смысла.

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

//--- расписание: первая эпоха уходит на оценку стартовой популяции,
//--- поэтому рабочих итераций epochsP - 1
  Tmax = epochsP - 1;
  if(Tmax < 1)
    Tmax = 1;
  t = 0;

//--- буферы
  ArrayResize(den,     popSize * coords);
  ArrayResize(vol,     popSize * coords);
  ArrayResize(accNorm, popSize * coords);
  ArrayResize(accTemp, popSize * coords);
  ArrayResize(denBest, coords);
  ArrayResize(volBest, coords);
  ArrayResize(accBest, coords);
  ArrayResize(snap_c,  popSize * coords);
  ArrayResize(snap_f,  popSize);

//--- стартовая популяция и физические атрибуты
  for(int i = 0; i < popSize; i++)
   {
    for(int c = 0; c < coords; c++)
     {
      //--- позиции, Eq.(4) — в cP[], попадут в c[] на первом Moving
      a [i].cP [c] = u.RNDfromCI(rangeMin [c], rangeMax [c]);

      //--- плотность и объём ~ U[0,1], Eq.(5)
      den [i * coords + c] = u.RNDfromCI(0.0, 1.0);
      vol [i * coords + c] = u.RNDfromCI(0.0, 1.0);

      //--- ускорение ~ U[lb,ub], Eq.(6).
      //--- Кладём в accNorm: на первой итерации acc = acc_norm = это значение
      accNorm [i * coords + c] = u.RNDfromCI(rangeMin [c], rangeMax [c]);
     }
   }

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

Moving(). Это сердце алгоритма. Первый вызов — особый: пока revision равен false, метод просто переносит стартовые координаты из cP в c (через u.SeInDiSp, с приведением к сетке и границам) и выходит. Это даёт внешнему циклу оценить стартовую популяцию.

На рабочих вызовах сначала фиксируются константы алгоритма — C1 = 2, C2 = 6, пределы нормализации uN = 0.9, lN = 0.1, пороги фаз TFc = 0.45 и TFx = 0.40, а также EPS для защиты знаменателей. Дальше итерация идёт по этапам.

Этап 0 — снимок. Координаты и фитнес всех агентов копируются в snap_c и snap_f. Снимок нужен по двум причинам. Во-первых, формулы (13) и (14) читают позиции на начало итерации, а метод тут же перезаписывает a[i].c новыми кандидатами — без снимка обработанный раньше агент исказил бы вход для обработанного позже. Во-вторых, снимок — основа жадного отбора в Revision().

Этап 1 — расписание. Счётчик t увеличивается, считаются оператор переноса TF (формула 8, с потолком 1), фактор плотности d (формула 9) и общее на всю итерацию случайное число r (формула 7).

Этап 2 — атрибуты. В одном цикле по агентам сначала обновляются плотность и объём — они подтягиваются к лучшему прямо на месте, in-place (формула 7), — затем считается сырое ускорение accTemp. Если TF ниже порога TFc, это фаза столкновений: выбирается случайный объект mr, ускорение берётся от него (формула 10). Иначе — фаза без столкновений, ускорение берётся от лучшего (формула 11). Обновление den / vol на месте означает, что объект с меньшим индексом уже обновлён к моменту, когда его читает объект с большим, — это сознательно повторяет порядок эталонного кода. Проверка EPS в знаменателе не даёт получить inf или nan, если плотность, объём или случайный множитель окажутся нулевыми.

Этап 3 — нормализация. По всему буферу accTemp (все объекты, все координаты) находятся глобальные минимум и максимум, и ускорение линейно отображается в полосу [lN, uN] (формула 12), результат пишется в accNorm. Вырожденный случай, когда все ускорения равны, обрабатывается отдельно.

Этап 4 — позиции. Считается множитель T = C3 · TF с потолком 1 (формула 15). Затем по агентам: если TF ниже TFx — фаза разведки, шаг в сторону случайного объекта (формула 13); иначе — фаза эксплуатации, шаг относительно глобально лучшего cB , причём знак шага на каждой координате выбирается величиной p (формулы 14–15). Все новые координаты пропускаются через u.SeInDiSp — это покоординатный контроль границ и шага сетки, заменивший дефектную проверку fun_checkpositions из оригинала.

//+------------------------------------------------------------------+
//|                            Moving                                |
//|                                                                  |
//|  Структура рабочей итерации:                                     |
//|    0. snapshot текущей популяции;                                |
//|    1. коэффициенты перехода TF (Eq.8) и d (Eq.9);                |
//|    2. обновление den/vol (Eq.7) + расчёт accTemp:                |
//|         TF < 0.45 — столкновения, Eq.(10);                       |
//|         иначе      — без столкновений, Eq.(11);                  |
//|    3. нормализация ускорения по всей популяции, Eq.(12);         |
//|    4. обновление позиций:                                        |
//|         TF < 0.4  — разведка, Eq.(13);                           |
//|         иначе      — эксплуатация, Eq.(14)-(15).                 |
//+------------------------------------------------------------------+
void C_AO_AOAarch::Moving()
 {
//--- первый прогон: cP -> c для оценки стартовой популяции
  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;
   }

//--- константы алгоритма (Hashim et al., 2021)
  const double C1   = 2.0;     // масштаб шага разведки,       Eq.(13)
  const double C2   = 6.0;     // масштаб шага эксплуатации,   Eq.(14)
  const double uN   = 0.9;     // верхний предел нормализации, Eq.(12)
  const double lN   = 0.1;     // нижний предел нормализации,  Eq.(12)
  const double TFc  = 0.45;    // порог фазы столкновений
  const double TFx  = 0.40;    // порог фазы разведки позиций
  const double EPS  = 1e-300;  // защита знаменателей от inf/nan

//--- 0. 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;
   }

//--- 1. коэффициенты перехода
  t++;
  double tt = 1; //(double)t;
  double Tm = (double)Tmax;

  double TF = MathExp((tt - Tm) / Tm);              // оператор переноса, Eq.(8)
  if(TF > 1.0)
    TF = 1.0;

  double d  = MathExp((Tm - tt) / Tm) - (tt / Tm);  // фактор плотности,  Eq.(9)

  double r  = u.RNDfromCI(0.0, 1.0);                // общий на итерацию, Eq.(7)

//--- 2. обновление den/vol + расчёт accTemp
  for(int i = 0; i < popSize; i++)
   {
    //--- плотность и объём тянутся к лучшему (in-place), Eq.(7)
    for(int c = 0; c < coords; c++)
     {
      int idx = i * coords + c;
      den [idx] += r * (denBest [c] - den [idx]);
      vol [idx] += r * (volBest [c] - vol [idx]);
     }

    double rnd = u.RNDfromCI(0.0, 1.0);             // свежий скаляр на объект

    if(TF < TFc)
     {
      //--- ФАЗА СТОЛКНОВЕНИЙ (разведка), Eq.(10)
      int mr = (int)u.RNDfromCI(0.0, (double)popSize);
      if(mr >= popSize)
        mr = popSize - 1;
      if(mr < 0)
        mr = 0;

      for(int c = 0; c < coords; c++)
       {
        int idx  = i  * coords + c;
        int idxR = mr * coords + c;

        double denom = rnd * den [idx] * vol [idx];
        if(MathAbs(denom) < EPS)
          denom = (denom < 0.0 ? -EPS : EPS);

        accTemp [idx] = (den [idxR] + vol [idxR] * accNorm [idxR]) / denom;
       }
     }
    else
     {
      //--- БЕЗ СТОЛКНОВЕНИЙ (эксплуатация), Eq.(11)
      for(int c = 0; c < coords; c++)
       {
        int idx = i * coords + c;

        double denom = rnd * den [idx] * vol [idx];
        if(MathAbs(denom) < EPS)
          denom = (denom < 0.0 ? -EPS : EPS);

        accTemp [idx] = (denBest [c] + volBest [c] * accBest [c]) / denom;
       }
     }
   }

//--- 3. нормализация ускорения по всей популяции, Eq.(12)
  int    total = popSize * coords;
  double gMin  = accTemp [0];
  double gMax  = accTemp [0];

  for(int k = 1; k < total; k++)
   {
    double v = accTemp [k];
    if(v < gMin)
      gMin = v;
    if(v > gMax)
      gMax = v;
   }

  double span = gMax - gMin;

  for(int k = 0; k < total; k++)
   {
    if(span > EPS)
      accNorm [k] = uN * (accTemp [k] - gMin) / span + lN;
    else
      accNorm [k] = lN;     // вырожденный случай: все ускорения равны
   }

//--- 4. обновление позиций
  double T = C3 * TF;                               // Eq.(15)
  if(T > 1.0)
    T = 1.0;

  for(int i = 0; i < popSize; i++)
   {
    if(TF < TFx)
     {

      //--- ФАЗА РАЗВЕДКИ, Eq.(13)
      for(int c = 0; c < coords; c++)
       {
        int mrand = (int)u.RNDfromCI(0.0, (double)popSize);
        if(mrand >= popSize)
          mrand = popSize - 1;
        if(mrand < 0)
          mrand = 0;

        int    idx   = i * coords + c;
        double x_old = snap_c [idx];
        double x_rnd = snap_c [mrand * coords + c];

        double v = x_old + C1 * u.RNDfromCI(0.0, 1.0) * accNorm [idx] *
                   (x_rnd - x_old) * d;

        a [i].c [c] = u.SeInDiSp(v, rangeMin [c], rangeMax [c], rangeStep [c]);
       }
     }
    else
     {
      //--- ФАЗА ЭКСПЛУАТАЦИИ, Eq.(14)
      for(int c = 0; c < coords; c++)
       {
        int    idx  = i * coords + c;
        double p    = 2.0 * u.RNDfromCI(0.0, 1.0) - C4;    // Eq.(15)

        double step = C2 * u.RNDfromCI(0.0, 1.0) * accNorm [idx] *
                      (T * cB [c] - snap_c [idx]) * d;

        double v;
        if(p < 0.5)
          v = cB [c] + step;
        else
          v = cB [c] - step;

        a [i].c [c] = u.SeInDiSp(v, rangeMin [c], rangeMax [c], rangeStep [c]);
       }
     }
   }
 }
//+------------------------------------------------------------------+

Revision(). Метод завершает итерацию. Первый вызов снова особый: стартовая популяция только что оценена, поэтому метод обновляет личные лучшие значения агентов и глобальное fB / cB, запоминает индекс лучшего объекта и снимает с него атрибуты в denBest, volBest, accBest, после чего ставит revision = true и выходит. Жадного отбора здесь нет — снимка ещё не существует.

На рабочих вызовах первым делом идёт жадный отбор: если новый фитнес агента хуже значения из снимка, координаты и фитнес откатываются к snap_c / snap_f. Важно, что атрибуты den, vol, acc откату не подлежат — они эволюционируют независимо. После отбора обновляются личные и глобальные лучшие, и, если глобальный оптимум улучшился, с нового лучшего объекта снимаются den / vol / accNorm в буферы лучшего. Порядок — сначала отбор, потом выбор лучшего — повторяет эталонную реализацию: лучший выбирается уже из принятой популяции.

//+------------------------------------------------------------------+
//|                           Revision                               |
//|                                                                  |
//|  Greedy acceptance: позиция/фитнес агента откатываются к         |
//|  снапшоту, если новый кандидат оказался хуже. Атрибуты           |
//|  den/vol/acc откату НЕ подлежат — они эволюционируют независимо. |
//|  Затем обновляются лучшие решения и атрибуты лучшего объекта.    |
//+------------------------------------------------------------------+
void C_AO_AOAarch::Revision()
 {
//--- первый прогон: оценка стартовой популяции, выбор лучшего
  if(!revision)
   {
    int bestIdx = -1;

    for(int i = 0; i < popSize; i++)
     {
      if(a [i].f > a [i].fB)
       {
        a [i].fB = a [i].f;
        ArrayCopy(a [i].cB, a [i].c, 0, 0, coords);
       }
      if(a [i].f > fB)
       {
        fB = a [i].f;
        ArrayCopy(cB, a [i].c, 0, 0, coords);
        bestIdx = i;
       }
     }

    //--- атрибуты лучшего объекта стартовой популяции
    if(bestIdx >= 0)
     {
      for(int c = 0; c < coords; c++)
       {
        denBest [c] = den     [bestIdx * coords + c];
        volBest [c] = vol     [bestIdx * coords + c];
        accBest [c] = accNorm [bestIdx * coords + c]; // сырое ускорение Eq.(6)
       }
     }

    revision = true;
    return;
   }

//--- greedy acceptance: откат, если стало хуже
  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];
     }
   }

//--- обновление личных/глобального лучших + захват атрибутов лучшего
  int bestIdx = -1;

  for(int i = 0; i < popSize; i++)
   {
    if(a [i].f > a [i].fB)
     {
      a [i].fB = a [i].f;
      ArrayCopy(a [i].cB, a [i].c, 0, 0, coords);
     }
    if(a [i].f > fB)
     {
      fB = a [i].f;
      ArrayCopy(cB, a [i].c, 0, 0, coords);
      bestIdx = i;
     }
   }

//--- den/vol/acc лучшего обновляются только при улучшении глобума
  if(bestIdx >= 0)
   {
    for(int c = 0; c < coords; c++)
     {
      denBest [c] = den     [bestIdx * coords + c];
      volBest [c] = vol     [bestIdx * coords + c];
      accBest [c] = accNorm [bestIdx * coords + c];
     }
   }
 }
//+------------------------------------------------------------------+


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

Прогон AOA на стандартном стенде даёт общий результат 39,66%. Этого, к сожалению, недостаточно, чтобы войти в рейтинговую таблицу. Но интереснее не сама цифра, а то, как она складывается, об этом подробнее в выводах.

AOA|Archimedes Optimization Algorithm|25.0|2.0|0.5|
=============================
5 Hilly's; Func runs: 10000; result: 0.8206488162634686
25 Hilly's; Func runs: 10000; result: 0.4924635010346886
500 Hilly's; Func runs: 10000; result: 0.2768438671570971
=============================
5 Forest's; Func runs: 10000; result: 0.6788879303684001
25 Forest's; Func runs: 10000; result: 0.24788382890806288
500 Forest's; Func runs: 10000; result: 0.06499823135837557
=============================
5 Megacity's; Func runs: 10000; result: 0.5906666666666667
25 Megacity's; Func runs: 10000; result: 0.27759999999999996
500 Megacity's; Func runs: 10000; result: 0.11920000000000104
=============================
All score: 3.56919 (39.66%)

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

AOA|Archimedes Optimization Algorithm|25.0|2.0|0.5|
=============================
Composite anti-cheat test: Hilly + Forest + Megacity + Peaks + Skin
Coordinates: 10; Epochs: 400; Repeats: 10
=============================
Run 1/10: 0.6677793808019812
Run 2/10: 0.6355386750189551
Run 3/10: 0.5813048402935594
Run 4/10: 0.7378557003396822
Run 5/10: 0.7012834397505456
Run 6/10: 0.6202804626691847
Run 7/10: 0.8256544601079605
Run 8/10: 0.7089113881121764
Run 9/10: 0.9011830945159774
Run 10/10: 0.9514789578735151
=============================
Average result: 0.7331270399 (73.31%)
=============================

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

Hilly

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

Forest

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

Megacity

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

Rosenbrock

AOA на функции Rosenbrock

Ackley

AOA на функции Ackley

По результатам тестирования алгоритм AOA представлен в рейтинговой таблице ознакомительно.

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 ACA andean_condor_algorithm 0,78444 0,53260 0,33108 1,64812 0,79071 0,44960 0,10685 1,34716 0,92266 0,67733 0,17613 1,77612 4,771 53,02
38 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
39 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
40 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
41 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
42 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
43 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
44 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
45 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
AOA archimedes_optimization_algorithm 0,82064 0,49246 0,27684 1,58994 0,67888 0,24788 0,06500 0,99176 0,59066 0,27759 0,11920 0,98745 3,569 39,66
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


Выводы

Прогон AOA на стандартном стенде дал общий результат 39,66%. Этого оказалось недостаточно, чтобы войти в рейтинговую таблицу, — алгоритм остаётся за чертой уже разобранных лидеров. Но интереснее не сама цифра, а то, как она складывается.

Посмотрим на результаты по размерности. Усреднённо по трём функциям AOA набирает около 70% на задачах с 10 координатами, около 34% на 50 и лишь около 15% на 1000. С каждым шагом вверх по размерности оценка/результат падает примерно вдвое. На Forest это видно резче всего: 68% → 25% → 6,5%. Картина одинакова на всех трёх ландшафтах — Hilly, Forest, Megacity — и это не разброс, а устойчивая закономерность.

Такой профиль — характерная подпись преждевременной сходимости. На низкоразмерных задачах AOA работает вполне достойно: пространство тесное, заблудиться негде, и алгоритм аккуратно находит хорошую область. Но чем больше координат, тем раньше популяция схлопывается. И здесь стоит вернуться к устройству алгоритма. Разведка в AOA включена лишь пока TF мал, а пороги (0,45 и 0,4) держат эту фазу всего в первых ~8–20% итераций; дальше — сплошная эксплуатация, движение к лучшему. В пятимерной задаче этого хватает. В пятисотмерной — нет: турбулентность затихает быстрее, чем рой успевает обследовать пространство, и объекты оседают у первого же приличного оптимума, случайно найденного в начале. Жидкость приходит в покой слишком рано — и застывает не там.

Композитный анти-чит тест подтверждает, что числам можно верить. На пяти функциях с непохожей геометрией оптимума AOA набрал 73,31%, и это согласуется с его честной силой на низких размерностях — каждая функция в композите получает лишь двумерный срез. Будь у AOA геометрический чит вроде диагонального выравнивания, композит обрушил бы счёт; этого не произошло. То есть и сильная сторона, и слабая — настоящие, без артефактов.

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

И всё же записывать AOA в неудачники не хочется. Идея — переопределить оптимизацию как поиск равновесия, а баланс explore/exploit отдать физике — красива и работоспособна; на низких размерностях она себя оправдывает. Слабое место не сама модель, а её настройка по умолчанию, унаследованная из эталонной статьи: расписание перекошено в эксплуатацию. Очевидный путь доработки лежит ровно там, где мы наметили его при реализации: вынести пороги фаз в параметры и сместить их, удлинив разведку, а возможно — поставить долю разведки в зависимость от размерности задачи. С такой переработкой AOA вполне мог бы подняться значительно выше нынешних 39,66%. 

Весь инструментарий: класс C_AO_AOA, стандартный стенд и композитный анти-чит тест — у читателя на руках. Приглашаю не останавливаться на этих выводах, а проверить и поискать самому: возможно, кому-то удастся больше, чем вышло у меня. AOA в нынешнем виде — не финал, а открытая задача.

tab

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

chart

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


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

Плюсы:

  1. Небольшое количество внешних параметров.

Минусы:

  1. Склонность к застреванию.
  2. Внутренние параметры необходимо настраивать.

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



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

# Имя Тип Описание
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_AOA.mq5
Скрипт Испытательный стенд для AOA
Прикрепленные файлы |
AOAarch.zip (421.67 KB)
От "лучшего прохода" к устойчивым решениям: исследование поверхности оптимизации в MetaTrader 5 От "лучшего прохода" к устойчивым решениям: исследование поверхности оптимизации в MetaTrader 5
В статье рассмотрен инженерный подход к оптимизации советника в MetaTrader 5: от сбора пользовательских метрик через Optimization Frames до анализа поверхности параметров. На простой событийной EMA/RSI‑модели показаны экспорт в CSV, сглаживание и оценка локальной стабильности в Python. Цель — находить устойчивые области конфигураций и подтверждать их форвард‑оптимизацией для надёжного внедрения.
Разработка инструментария для анализа Price Action (Часть 44): Создание в MQL5 сигнального советника на основе пересечений VWMA Разработка инструментария для анализа Price Action (Часть 44): Создание в MQL5 сигнального советника на основе пересечений VWMA
В этой статье представлен инструмент для MetaTrader 5, сигнализирующий о пересечениях VWMA, который помогает трейдерам выявлять потенциальные бычьи и медвежьи развороты, сочетая анализ движения цены и торгового объема. Советник генерирует четкие сигналы на покупку и продажу прямо на графике, оснащен информативной панелью и позволяет гибко настраивать входные параметры, что делает его практичным дополнением к торговой стратегии.
Разработка инструментария для анализа Price Action (Часть 45): Создание динамической панели для анализа уровней в MQL5 Разработка инструментария для анализа Price Action (Часть 45): Создание динамической панели для анализа уровней в MQL5
В этой статье мы рассмотрим мощный инструмент на MQL5, который позволяет тестировать любой ценовой уровень одним кликом. Просто введите нужный уровень и нажмите Analyze – советник мгновенно сканирует исторические данные, выделяет на графике все касания и пробои и выводит статистику в аккуратной информационной панели. Вы увидите, как часто цена отрабатывала этот уровень или пробивала его, а также выступал ли уровень чаще как поддержка или как сопротивление. Читайте дальше, чтобы подробнее ознакомиться с процедурой.
Создание торговой системы (Часть 3): Определение минимального уровня риска для достижения реалистичных целей по прибыли Создание торговой системы (Часть 3): Определение минимального уровня риска для достижения реалистичных целей по прибыли
Конечной целью каждого трейдера является прибыльность, именно поэтому многие устанавливают конкретные цели по прибыли, которых необходимо достичь в течение определенного периода торговли. В этой статье мы будем использовать моделирование методом Монте-Карло, чтобы определить оптимальный процент риска на сделку, необходимый для достижения торговых целей. Полученные результаты помогут трейдерам оценить, являются ли их целевые показатели прибыли реалистичными или чрезмерно амбициозными. Наконец, мы обсудим, какие параметры можно скорректировать, чтобы установить реалистичный уровень риска на сделку, соответствующий торговым целям.