preview
Роевой оптимизатор с иерархией суброев — Flock by Leader

Роевой оптимизатор с иерархией суброев — Flock by Leader

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

Содержание

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


Введение

Если вы оптимизируете параметры торговой системы или другую высокоразмерную модель с помощью роевых методов, вы часто сталкиваетесь с двумя конфликтующими проблемами: популяция быстро «схлопывается» к одной точке и прекращает разведку, а попытка удержать разнообразие резко увеличивает вычислительные затраты и разброс результатов. При фиксированном бюджете вычислений хотелось бы, чтобы алгоритм одновременно: (1) глубоко прорабатывал перспективные области — exploitation, (2) сохранял несколько независимых «очагов» поиска — exploration, (3) не терял глобальную разведку.

В этой работе мы проверяем, решает ли такую задачу архитектура Flock by Leader (FBL). Идея проста и воспроизводима: популяцию делят на суброи по плотности (ARF), внутри каждого суброя лидера назначают по личному лучшему рекорду (fB), а для трёх ролей — лидера, последователя и выброса — заданы разные правила движения. В статье мы формализуем метрику ARF и правила обновления, даём псевдокод, реализуем компонент "C_AO_FBL" для MQL5, фиксируем управляющие параметры (popSize, k, phi, wSep) и задаём чёткие критерии успеха: качество найденных решений, стабильность результатов и вычислительная стоимость при заданном бюджете вычислений.


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

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

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

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

В начале каждой итерации алгоритм смотрит на текущее расположение всех агентов и спрашивает: кто находится в центре плотного скопления, а кто — на периферии или в одиночестве? Для ответа вычисляется метрика ARF — Agent Role Factor. Идея ARF опирается на два множества для каждого агента. Первое — прямая окрестность: все агенты, находящиеся в радиусе "d_max" от данного агента, где d_max — расстояние до k-го ближайшего соседа. Это личное пространство агента, те, кого он считает своими соседями. Второе — обратная окрестность: все агенты, в чьё личное пространство попадает данный агент. Это те, кто считает его своим соседом.

ARF — это отношение размера обратной окрестности к сумме обеих: ARF = |обратная окрестность| / (|обратная окрестность| + |прямая окрестность|).

Разберём, что это означает на практике. Если агент стоит в центре плотной группы, многие агенты вокруг него включают его в своё личное пространство — обратная окрестность велика. При этом его собственный радиус "d_max" невелик, потому что "k" соседей совсем рядом, и его прямая окрестность тоже не очень большая. ARF приближается к единице. Если агент стоит на периферии или совсем один, его никто не включает в своё личное пространство — обратная окрестность мала или пуста. Зато, чтобы найти "k" соседей, ему нужно тянуться далеко, его радиус "d_max" велик и прямая окрестность большая. ARF приближается к нулю.

Агенты с ARF >= 0.5 становятся центроидами — геометрическими ядрами суброев. Если ни один агент не достиг порога, берётся тот, у кого ARF максимален. Это страховка для первых итераций, когда агенты ещё рассредоточены равномерно. Каждый центроид притягивает к себе агентов в радиусе своего "d_max". Агент, попавший в радиус нескольких центроидов, достаётся тому, у кого выше ранг — специальная числовая мера значимости суброя. Ранг учитывает плотность скопления: чем больше агентов собрал вокруг себя центроид, тем выше его ранг. Число суброев сверху ограничено: если центроидов стало слишком много (при малом k), лишние с низкими рангами отбрасываются. Это не даёт популяции рассыпаться на десятки крошечных неэффективных групп. Здесь возникает ключевой вопрос: кто из членов суброя становится его лидером? Именно в ответе на этот вопрос алгоритм делает принципиальный выбор.

Центроид определяет геометрию суброя — где он находится в пространстве. Но центроид не обязательно стоит в хорошей точке ландшафта. Представьте: рой собрался на склоне холма. Центроид — агент примерно в середине этой группы — стоит на высоте 650 метров. Но один из других членов роя побывал на высоте 820 метров и запомнил это место в своём личном рекорде. Лидером роя становится именно он — тот, у кого лучший личный рекорд "fB" среди всех членов группы. Весь суброй будет двигаться в его сторону, а не в сторону геометрического центра.

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

Лидер суброя — лучший в своей группе на данный момент. Его задача: продолжать улучшать найденное решение, оставаясь точкой притяжения для своих последователей. На него действуют три силы. Инерция — он продолжает двигаться туда, куда двигался. Коэффициент 0.729 (стандартный коэффициент сужения Клерка–Кеннеди) плавно гасит скорость, не давая лидеру разогнаться до хаотичного блуждания. Тяга к личному рекорду — лидер помнит лучшее место, где побывал, и тяготеет к нему. Тяга к глобальному рекорду — лидер знает лучшее место во всей популяции и с силой "phi" притягивается к нему. Эти три силы складываются случайно взвешенным образом для каждой координаты отдельно. Независимость весов по координатам принципиально важна: агент может двигаться по диагонали ландшафта, усиленно продвигаясь по одним осям и осторожно по другим.

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

Последователь — член суброя, не являющийся его лидером. Он движется вместе с группой, тянется к лидеру, но при этом сохраняет собственную историю поиска. На него действуют четыре силы плюс отталкивание. Когезия тянет его к текущей позиции лидера: "иди туда, где стоит ведущий". Выравнивание (Alignment) согласовывает его скорость со скоростью лидера: "двигайся туда, куда движется ведущий". Когнитивный компонент тянет к собственному лучшему найденному месту: "помни о своих находках". Глобальный компонент притягивает ко всему лучшему, что нашла вся популяция. Separation — сила отталкивания от слишком близких соседей, о которой подробнее ниже.

Рассмотрим пример. Лидер суброя "L" нашёл хорошую точку на ландшафте и движется в её сторону со скоростью (1.8, 0.7) по двум координатам. Последователь "F" находится в 12 единицах к югу от "L". Его личный рекорд — хорошая точка в 5 единицах к востоку. Когезия тянет "F" на север (к "L"). Выравнивание добавляет компоненту в направлении (1.8, 0.7) — туда, куда идёт "L". Когнитивный компонент добавляет смещение на восток. В итоге "F" движется на северо-восток, сохраняя как направление к лидеру, так и память о собственной находке.

Важная техническая деталь в компоненте выравнивания. Для согласования скоростей используется не текущая скорость лидера после его обновления в этой итерации, а зафиксированное значение скорости — то значение, которое было до начала обновлений. Лидеры обновляются первыми (Pass 1), последователи — вторыми (Pass 2). Если использовать уже изменённую скорость лидера, последователь выравнивается по "будущему" направлению, которое лидер ещё не прошёл. Использование сохраненного значения скорости делает выравнивание физически корректным: агент согласовывает движение с тем, что лидер действительно делал, а не с тем, что только собирается сделать.

Без дополнительного механизма когезия со временем стянула бы всех членов суброя в одну точку — позицию лидера. Двадцать агентов в одном месте вычисляют фитнес двадцать раз в одинаковой точке — это полное расточительство бюджета вычислений. Separation — сила отталкивания, которая не даёт агентам слипнуться. Для каждого агента вычисляется зона дискомфорта: половина его радиуса окрестности "d_max". Если сосед попадает в эту зону, агент получает толчок в сторону от него. Сила толчка тем больше, чем ближе сосед: при расстоянии, равном половине "d_max", сила нулевая, при полном совпадении позиций — максимальная. Результат: члены суброя движутся к лидеру, но сохраняют между собой здоровые дистанции. Рой движется как плотная, но не слипшаяся группа, покрывая окрестность лидера и исследуя её более широко.

Вес "separation" задаётся параметром "wSep". При большом "wSep" агенты сильно расходятся, суброи рыхлые. При малом — плотные, с риском схлопывания. 

Выброс — агент, оказавшийся вне всех роев. Ни один центроид не покрыл его своим радиусом. Это изолированный искатель без группы и без лидера. Его поведение простое. Для каждой координаты независимо: с вероятностью 50% он перемещается в окрестность глобального рекорда, добавляя случайное смещение в пределах 20% диапазона — это локальный поиск вблизи лучшей известной точки. С вероятностью 50% он улетает в совершенно случайную точку пространства поиска — это глобальная диверсификация. Именно покоординатная независимость делает поведение выброса богатым.

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

FBL_illustration

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

На иллюстрации в левой части задано пространство поиска с тремя цветными суброями (синий, оранжевый, зелёный). Каждый рой показывает полную структуру: штриховая окружность — граница роя (радиус "d_max" центроида), ромб с буквой C — центроид (агент с ARF ≥ 0.5, геометрическое ядро), золотая звезда — лидер (агент с лучшим "fB" в группе, может не совпадать с центроидом), синие кружки со стрелками — последователи, двигающиеся к лидеру. Серые кружки за пределами всех роев — выбросы с хаотичными стрелками разведки. Белая звезда в кольце — gBest, к которому тянутся все три лидера золотыми стрелками. Фоновые цветные пятна намекают на рельеф фитнес-ландшафта. В левом верхнем углу — формула ARF с порогом 0.5. В правой части картинки легенда с объяснением каждого символа.

Переходим к написанию псевдокода алгоритма FBL.

Псевдокод FBL (Flock by Leader)
Обозначения

    N — размер популяции (popSize), D — размерность.
    kEff = min(k, N-1) — эффективное число соседей.
    x[i] — текущая позиция агента i, v[i] — скорость.
    pBest[i] — личный лучший (позиция), fBest[i] — его качество.
    gBest — глобальный лучший (позиция).
    dist(i,j) — нормированное расстояние между агентами.
    dMax[i] — расстояние до kEff-го ближайшего соседа агента i.
    Nout[i] — прямая k-окрестность (кого i считает соседями), Nin[i] — обратная (кто считает соседом i).
    ARF[i] = |Nin[i]| / (|Nin[i]| + |Nout[i]|).
    Роли: LEADER, FOLLOWER, OUTLIER.

Итерация t = 1..T
Шаг 0. Снимки (нужны для корректного alignment)

    xSnap[i] ← x[i] для всех i
    vSnap[i] ← v[i] для всех i

Шаг 1. Расстояния и k-ближайшие соседи

Для каждого агента i:

    Посчитать dist(i,j) для всех j ≠ i
    Отсортировать j по возрастанию dist(i,j)
    knnIdx[i] ← первые kEff индексов
    dMax[i] ← dist(i, knnIdx[i][kEff])
    Nout[i] ← { j | dist(i,j) ≤ dMax[i] } (по смыслу: «личная окрестность»)

Шаг 2. Обратные окрестности и ARF

    Обнулить Nin[i] (или счётчик |Nin[i]|) для всех i
    Для всех пар (i, j), где j ∈ Nout[i]: добавить i в Nin[j]
    Для каждого i:
    arf[i] ← |Nin[i]| / (|Nin[i]| + |Nout[i]|)

Шаг 3. Выбор центроидов (ядра суброев) и ранжирование

    centroids ← { i | arf[i] ≥ 0.5 }
    Если centroids пуст:
    centroids ← { argmax_i arf[i] } (страховка ранних итераций)
    Для каждого центроида c:
    Кандидатный радиус суброя: R[c] ← dMax[c]
    rank[c] ← 0 (будет заполнен числом присоединённых агентов)
    Если центроидов слишком много (ограничение сверху):
    оставить центроиды с наибольшим rank/плотностью (точное правило — как в реализации)

Шаг 4. Назначение агентов в суброи и роли OUTLIER

    subflockId[i] ← -1 для всех i
    Для каждого агента i:
    Найти множество подходящих центроидов:
    C(i) = { c ∈ centroids | dist(i,c) ≤ R[c] }
    Если C(i) пусто: role[i] ← OUTLIER, subflockId[i] ← -1
    Иначе:
    выбрать c* с максимальным rank[c] (или иным правилом при конфликте)
    subflockId[i] ← id(c*), role[i] пока не назначать
    rank[c*]++

Шаг 5. Выбор лидера в каждом суброе (по личному лучшему fBest)

Для каждого суброя s:

    members(s) ← { i | subflockId[i] = s }
    leader[s] ← argmax_{i ∈ members(s)} fBest[i]
    Для всех i ∈ members(s):
    если i = leader[s]: role[i] ← LEADER
    иначе role[i] ← FOLLOWER

Шаг 6. Обновление позиций (3 разных правила движения)
6A. Обновление лидеров (Pass 1)

Для каждого агента i с role[i]=LEADER:

    Если prevRole[i] ≠ LEADER: v[i] ← 0.5 * v[i]
    Обновить скорость (инерция + pBest + gBest):
    v[i] ← 0.729*v[i]
    v[i] ← v[i] + randVec()* (pBest[i] - xSnap[i])
    v[i] ← v[i] + phi*randVec()* (gBest - xSnap[i])
    x[i] ← xSnap[i] + v[i]

6B. Обновление последователей (Pass 2)

Для каждого агента i с role[i]=FOLLOWER:

    L ← leader[subflockId[i]]
    Посчитать sep ← Separation(i) (по knnIdx[i], зоне 0.5*dMax[i], вес wSep)
    Обновить скорость (cohesion + alignment + pBest + gBest + separation):
    v[i] ← v[i] + wCoh*randVec()*(xSnap[L] - xSnap[i])
    v[i] ← v[i] + wAli*randVec()*(vSnap[L] - vSnap[i]) (важно: берём vSnap!)
    v[i] ← v[i] + wCog*randVec()*(pBest[i] - xSnap[i])
    v[i] ← v[i] + phi*wGlob*randVec()*(gBest - xSnap[i])
    v[i] ← v[i] + sep
    x[i] ← xSnap[i] + v[i]

6C. Обновление выбросов (OUTLIER)

Для каждого агента i с role[i]=OUTLIER:

    Для каждой координаты d=1..D:
    если rand()<0.5:
    x[i][d] ← gBest[d] + uniform(-0.2*range[d], +0.2*range[d])
    иначе:
    x[i][d] ← uniform(min[d], max[d])
    v[i] ← 0

Шаг 7. Границы, оценка, обновление рекордов

    Ограничить x[i] в допустимых пределах (clamp/repair)
    Вычислить f[i] = Fitness(x[i])
    Если f[i] > fBest[i]: обновить fBest[i], pBest[i]
    Если f[i] > f(gBest): обновить gBest
    prevRole[i] ← role[i]


Теперь можем написать реализацию.

Класс "C_AO_FBL" реализует алгоритм Flock by Leader в виде стандартного компонента, наследующего от базового класса "C_AO" унифицированного тестового стенда. Архитектура наследования гарантирует совместимость с инфраструктурой тестирования: класс предоставляет те же три точки входа (Init, Moving, Revision) и опирается на тот же механизм параметров "S_AlgoParam".

Алгоритм управляется четырьмя внешними параметрами. Параметр "popSize" задаёт размер популяции и определяет соотношение числа вычислений функции к числу итераций. Параметр "k" устанавливает число ближайших соседей при построении k-окрестностей; он является главным регулятором топологической структуры: малое "k" порождает мелкие многочисленные суброи с активной разведкой, большое "k" — крупные и редкие суброи с доминирующей эксплуатацией. Рекомендуемое значение k ≈ popSize × 0.1. Параметр "phi" задаёт силу притяжения к глобальному рекорду как для лидеров, так и для последователей; при phi = 0 суброи работают независимо, при большом "phi" популяция схлопывается к одной точке. Параметр "wSep" управляет весом вектора "separation" и определяет, насколько сильно агенты отталкиваются от слишком близких соседей внутри суброя.

Внутреннее состояние алгоритма хранится в нескольких группах массивов. Массивы "V" и "Vsnap" хранят текущие скорости агентов и сохраненные, снятые в начале каждой итерации. Массив "cPsnap" фиксирует позиции агентов до начала обновлений. Матрица "distM" содержит нормированные попарные расстояния между всеми агентами. Массивы "dMax", "dknbSz", "drknbSz" и "arf" хранят радиусы окрестностей, размеры прямых и обратных окрестностей, и значения "ARF" для каждого агента. Массив "knnIdx" хранит индексы "k" ближайших соседей каждого агента и используется при вычислении "separation". Массивы "subflockId", "leaderIdx", "role", "prevRole" кодируют топологическую структуру популяции и ролевое состояние каждого агента. Массивы "centroidBuf", "subflockLeader", "subflockBestF" и "rankBuf" являются рабочими буферами для построения иерархии суброев и сортировки центроидов.

//────────────────────────────────────────────────────────────────────
class C_AO_FBL : public C_AO
{
public: //------------------------------------------------------------
  ~C_AO_FBL () { }
  C_AO_FBL ()
  {
    ao_name = "FBL";
    ao_desc = "Flock by Leader";
    ao_link = "https://www.mql5.com/ru/articles/21821";

    popSize = 50;
    k       = 5;
    phi     = 0.5;   // [F1]
    wSep    = 0.15;  // [F6]

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

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

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

  void Moving   ();
  void Revision ();

  //------------------------------------------------------------------
  int    k;    // k ближайших соседей для DkNB / DR-kNB / ARF
  double phi;  // коэффициент притяжения к gBest
  double wSep; // вес вектора separation                                [F6]

private: //---------------------------------------------------------
  double V              []; // скорости              [popSize * coords]
  double Vsnap          []; // снапшот скоростей     [popSize * coords]  [F4]
  double Vmax           []; // ограничение скорости  [coords]
  double cPsnap         []; // снапшот позиций       [popSize * coords]
  double sepVec         []; // вектор separation     [coords]
  double distM          []; // норм. матрица расст.  [popSize × popSize]
  double dMax           []; // d_max^i               [popSize]
  int    dknbSz         []; // |DkNB_t(x_i)|         [popSize]
  int    drknbSz        []; // |DR-kNB_t(x_i)|       [popSize]
  double arf            []; // ARF_t(A_i)             [popSize]
  int    knnIdx         []; // k-NN индексы           [popSize * kEff]
  int    subflockId     []; // суброй агента (-1=выброс) [popSize]
  int    leaderIdx      []; // индекс лидера суброя [popSize]
  int    role           []; // 0=лидер 1=послед. 2=выброс [popSize]
  int    prevRole       []; // роль предыдущей итерации [popSize]
  int    centroidBuf    []; // центроиды, sorted by ARF↓ [popSize]
  int    subflockLeader []; // лидер суброя s       [popSize]
  double subflockBestF  []; // лучший fB в суброе s [popSize]
  double rankBuf        []; // Rank_t(A_i) Formula 5  [popSize]          [F7]
  double tmpDist        []; // буфер k-NN сортировки  [popSize]
  int    tmpIdx_        []; // буфер k-NN индексов    [popSize]
  int    kEff;              // min(k, popSize-1)
  int    nCentroids;        // число суброев на итерации

  int    Idx   (int i, int c)
  {
    return i * coords + c;
  }
  double Clamp (double v, double lo, double hi)
  {
    return v < lo ? lo : v > hi ? hi : v;
  }

  void   CalcSep (int i); // вычислить sepVec[] для агента i
};
//————————————————————————————————————————————————————————————————————

Метод "Init" выполняет однократную инициализацию перед началом оптимизации. Он вызывает стандартный инициализатор базового класса "StandardInit", который распределяет массивы агентов и копирует диапазоны поиска. Затем вычисляется эффективное число соседей kEff = min(k, popSize − 1) — страховка от некорректных значений параметра "k". Все внутренние массивы размечаются в соответствии с размером популяции и числом координат. "Vmax" по каждой координате устанавливается равным половине диапазона этой оси, что ограничивает максимальный шаг агента за итерацию. Начальные позиции агентов задаются случайно равномерно в допустимом пространстве поиска, скорости обнуляются, личные рекорды инициализируются значением −DBL_MAX. Все агенты получают роль последователя (role = 1), счётчик суброев обнуляется.

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

  kEff = MathMin (k, popSize - 1);
  if (kEff < 1) kEff = 1;

  ArrayResize (V,              popSize * coords);
  ArrayResize (Vsnap,          popSize * coords);  // [F4]
  ArrayResize (Vmax,           coords);
  ArrayResize (cPsnap,         popSize * coords);
  ArrayResize (sepVec,         coords);
  ArrayResize (distM,          popSize * popSize);
  ArrayResize (dMax,           popSize);
  ArrayResize (dknbSz,         popSize);
  ArrayResize (drknbSz,        popSize);
  ArrayResize (arf,            popSize);
  ArrayResize (knnIdx,         popSize * kEff);
  ArrayResize (subflockId,     popSize);
  ArrayResize (leaderIdx,      popSize);
  ArrayResize (role,           popSize);
  ArrayResize (prevRole,       popSize);
  ArrayResize (centroidBuf,    popSize);
  ArrayResize (subflockLeader, popSize);
  ArrayResize (subflockBestF,  popSize);
  ArrayResize (rankBuf,        popSize);
  ArrayResize (tmpDist,        popSize);
  ArrayResize (tmpIdx_,        popSize);

  for (int c = 0; c < coords; c++)
    Vmax [c] = (rangeMax [c] - rangeMin [c]) * 0.5;

  for (int i = 0; i < popSize; i++)
  {
    for (int c = 0; c < coords; c++)
    {
      a [i].cP [c]    = u.RNDfromCI (rangeMin [c], rangeMax [c]);
      V    [Idx (i, c)] = 0.0;
      Vsnap[Idx (i, c)] = 0.0;  // [F4]
    }
    a [i].fB    = -DBL_MAX;
    role     [i] = 1;
    prevRole [i] = 1;
    leaderIdx[i] = -1;
  }
  nCentroids = 0;
  return true;
}
//————————————————————————————————————————————————————————————————————

Метод "Moving" выполняет единственную операцию: копирует непрерывные рабочие позиции "cP" в дискретизованные позиции "c" через функцию "SeInDiSp". Именно координаты "c" передаются внешнему коду для вычисления значения фитнес-функции. Разделение "cP" и "c" позволяет алгоритму работать во внутреннем непрерывном пространстве (где формулы скоростей дают дробные значения) независимо от того, является ли реальная задача дискретной или непрерывной — дискретизация происходит только в момент запроса фитнеса.

//————————————————————————————————————————————————————————————————————
void C_AO_FBL::Moving ()
{
  for (int i = 0; i < popSize; i++)
    for (int c = 0; c < coords; c++)
      a [i].c [c] = u.SeInDiSp (a [i].cP [c], rangeMin [c], rangeMax [c], rangeStep [c]);
}
//————————————————————————————————————————————————————————————————————

Метод "CalcSep" вычисляет вектор "separation" для агента "i" и записывает его в общий буфер "sepVec". Метод реализует первое правило Рейнольдса: агент должен держать минимальную дистанцию от своих ближайших соседей.

Зона "separation" определяется как половина радиуса окрестности: dSep = 0.5 × dMax [i]. Метод перебирает все "kEff" ближайших соседей агента "i", заранее найденных в шаге 3 метода "Revision" и сохранённых в "knnIdx". Если сосед "j" находится ближе "dSep", он вносит вклад в вектор отталкивания с весом, обратно пропорциональным расстоянию: чем ближе сосед, тем сильнее толчок. Вес вычисляется как (dSep − dist) / dSep, что даёт значение от 0 на границе зоны до 1 при совпадении позиций. Итоговый вектор масштабируется на параметр "wSep". Поскольку разности позиций "cPsnap" берутся в абсолютных единицах пространства поиска, а "Vmax" тоже задан в абсолютных единицах, вектор "sep" непосредственно сопоставим с другими компонентами скорости.

//————————————————————————————————————————————————————————————————————
// [O2][F6] Separation-вектор для агента i → sepVec[].
// Отталкивание от k-NN соседей ближе dSep = 0.5 * dMax[i].
// sepVec[] в абсолютных единицах позиций, масштаб задаёт wSep.
void C_AO_FBL::CalcSep (int i)
{
  ArrayInitialize (sepVec, 0.0);
  double dSep = 0.5 * dMax [i];
  if (dSep < 1e-14) return;

  for (int m = 0; m < kEff; m++)
  {
    int    j     = knnIdx [i * kEff + m];
    double normD = distM  [i * popSize + j];
    if (normD >= dSep) continue;
    double w = (normD > 1e-12) ? (dSep - normD) / dSep : 1.0;
    for (int c = 0; c < coords; c++)
      sepVec [c] += w * (cPsnap [Idx (i, c)] - cPsnap [Idx (j, c)]);
  }
  for (int c = 0; c < coords; c++) sepVec [c] *= wSep;  // [F6]
}
//————————————————————————————————————————————————————————————————————

Метод "Revision" содержит всю логику алгоритма и разделён на 14 шагов. Он вызывается после того, как внешний код заполнил поле a [i].f для всех агентов по результатам вызовов "Moving" и "CalcFunc".

Шаг 1 обновляет личные рекорды fB/cB и глобальный рекорд fB/cB. Личный рекорд фиксирует лучшую позицию, которую агент когда-либо посетил, и используется в когнитивном компоненте формул скоростей и — что принципиально важно — для выбора лидера суброя. Сравнение ведётся с дискретизованными координатами "c", а не с "cP", поскольку именно "c" оценивались функцией.

Шаги 2–5 формируют топологическую картину популяции. Матрица расстояний строится с нормировкой на диапазон каждой оси, что обеспечивает равный вклад всех координат независимо от их масштаба. Это критично для задач с несколькими сотнями координат, где оси могут иметь принципиально разные диапазоны. Частичная сортировка выбором для поиска "kEff" ближайших соседей работает за O(kEff × n) вместо O(n × log n) полной сортировки — при kEff << n это заметная экономия. "ARF" вычисляется точно по формуле 4 оригинальной статьи.

Шаг 6 отбирает центроиды и сортирует их по формуле ранга 5 из статьи, адаптированной для оптимизации. Ранг учитывает только число соседей (топологический вес), но не фитнес — это намеренно, поскольку ранг управляет приоритетом при конфликте членства двух роев, а не выбором лидера. Ограничение числа центроидов до popSize / kEff предотвращает фрагментацию: при малом "k" нет смысла иметь больше суброев, чем позволяет связность k-NN графа.

Шаг 7 назначает агентов в суброи по принципу «первый охватывающий центроид» в порядке убывания ранга: более значимый суброй захватывает пограничных агентов первым. Агент вне всех радиусов становится выбросом.

Шаг 8 выбирает лидера каждого суброя по наилучшему личному рекорду "fB" среди всех членов. Использование "fB" вместо текущего "f" делает выбор устойчивым к шуму: случайно хорошее текущее значение у слабого агента не сделает его лидером, если его исторический рекорд слабее. Страховка на случай первой итерации (когда fB = −DBL_MAX у всех) назначает лидером центроид суброя.

Шаг 10 снимает снапшоты "cPsnap" и "Vsnap" перед любыми обновлениями. Снапшот позиций необходим для устранения "ordering bias": все компоненты когезии и "separation" вычисляются относительно стабильных старых позиций, а не постепенно изменяющихся в ходе одного прохода цикла. Снапшот скоростей "Vsnap" необходим для корректной работы "Alignment" у последователей: они выравниваются по тому вектору скорости лидера, который существовал до начала итерации, а не по уже изменённому в Pass 1. Это делает выравнивание физически осмысленным — агент согласует направление с реальным движением лидера, а не с только что вычисленным новым импульсом.

Pass 1 (шаг 11) обновляет лидеров первыми. Коэффициент сужения 0.729 (Clerc–Kennedy) в компоненте инерции обеспечивает гарантированную сходимость скоростей при любых весах "r1" и "r2" без ручного подбора инерционного коэффициента. При смене роли с последователя на лидера старая скорость приглушается вдвое, а не обнуляется полностью: сохраняется направленность накопленного импульса, но его величина снижается до безопасного уровня.

Pass 2 (шаг 12) обновляет последователей с использованием обновлённых позиций лидеров "cPsnap" (снапшот — до изменений) и старых скоростей "Vsnap". Четыре компонента скорости соответствуют четырём силам, действующим на последователя: когезия тянет к лидеру суброя, "alignment" согласовывает направление движения, когнитивный компонент удерживает связь с личным рекордом агента, глобальный компонент обеспечивает медленную конвергенцию всей популяции к наилучшему найденному решению.

Pass 3 (шаг 13) обновляет выбросов. Выброс — агент, оказавшийся вне всех роев. В многомерном пространстве решение о прыжке принимается независимо для каждой координаты: это позволяет агенту перемещаться по диагонали в пространстве, изменяя одни координаты в окрестности "gBest" и другие случайно. Такое покоординатное решение обеспечивает значительно большее разнообразие паттернов поиска, чем единое решение для всего агента целиком.

//————————————————————————————————————————————————————————————————————
void C_AO_FBL::Revision ()
{
  //─── 1. Личные и глобальный лучшие ───────────────────────────────
  for (int i = 0; i < popSize; i++)
  {
    if (a [i].f > a [i].fB)
    {
      a [i].fB = a [i].f;
      ArrayCopy (a [i].cB, a [i].c, 0, 0, coords);
    }
    if (a [i].f > fB)
    {
      fB       = a [i].f;
      ArrayCopy (cB,       a [i].c, 0, 0, coords);
    }
  }

  //─── 2. Нормированная матрица расстояний ─────────────────────────
  for (int i = 0; i < popSize; i++)
  {
    distM [i * popSize + i] = 0.0;
    for (int j = i + 1; j < popSize; j++)
    {
      double d = 0.0;
      for (int c = 0; c < coords; c++)
      {
        double rng = rangeMax [c] - rangeMin [c];
        double df  = (rng > 0.0) ? (a [i].cP [c] - a [j].cP [c]) / rng : 0.0;
        d += df * df;
      }
      d = MathSqrt (d);
      distM [i * popSize + j] = d;
      distM [j * popSize + i] = d;
    }
  }

  //─── 3. k-NN → d_max → |DkNB| (Def 5, Formula 3) ───────────────
  for (int i = 0; i < popSize; i++)
  {
    int cnt = 0;
    for (int j = 0; j < popSize; j++)
    {
      if (j == i) continue;
      tmpDist [cnt] = distM [i * popSize + j];
      tmpIdx_ [cnt] = j;
      cnt++;
    }
    for (int m = 0; m < kEff; m++)
    {
      int minPos = m;
      for (int n = m + 1; n < cnt; n++)
        if (tmpDist [n] < tmpDist [minPos]) minPos = n;
      if (minPos != m)
      {
        double td = tmpDist [m];
        tmpDist [m] = tmpDist [minPos];
        tmpDist [minPos] = td;
        int    ti = tmpIdx_ [m];
        tmpIdx_ [m] = tmpIdx_ [minPos];
        tmpIdx_ [minPos] = ti;
      }
    }
    dMax [i] = tmpDist [kEff - 1];
    for (int m = 0; m < kEff; m++) knnIdx [i * kEff + m] = tmpIdx_ [m];

    dknbSz [i] = 0;
    for (int j = 0; j < popSize; j++)
    {
      if (j == i) continue;
      if (distM [i * popSize + j] <= dMax [i]) dknbSz [i]++;
    }
  }

  //─── 4. |DR-kNB| (Definition 6) ─────────────────────────────────
  ArrayInitialize (drknbSz, 0);
  for (int i = 0; i < popSize; i++)
    for (int j = 0; j < popSize; j++)
    {
      if (j == i) continue;
      if (distM [i * popSize + j] <= dMax [j]) drknbSz [i]++;
    }

  //─── 5. ARF (Formula 4) ──────────────────────────────────────────
  for (int i = 0; i < popSize; i++)
  {
    int denom = drknbSz [i] + dknbSz [i];
    arf [i] = (denom > 0) ? (double)drknbSz [i] / (double)denom : 0.5;
  }

  //─── 6. Центроиды: ARF >= 0.5, сортировка по Rank↓ (Formula 5) ──
  nCentroids = 0;
  for (int i = 0; i < popSize; i++)
    if (arf [i] >= 0.5) centroidBuf [nCentroids++] = i;

  if (nCentroids == 0)
  {
    int bestI = 0;
    for (int i = 1; i < popSize; i++) if (arf [i] > arf [bestI]) bestI = i;
    centroidBuf [0] = bestI;
    nCentroids = 1;
  }

  // [F7] Formula 5: Rank_t(A_i) = Log(|N_i,t| / |N_t| * 10) * ARF_t(A_i)
  // |N_i,t| = dknbSz[i] (число соседей агента i)
  // |N_t|   = popSize   (число всех агентов)
  // Аргумент Log защищён от нуля: если dknbSz == 0 → rank = 0.
  for (int i = 0; i < popSize; i++)
  {
    double ratio = (double)dknbSz [i] / (double)popSize * 10.0;
    rankBuf [i]  = (ratio > 1e-12) ? MathLog (ratio) * arf [i] : 0.0;
  }

  // Insertion sort центроидов по Rank↓
  for (int a_ = 1; a_ < nCentroids; a_++)
  {
    int    key  = centroidBuf [a_];
    double keyR = rankBuf [key];
    int    b_   = a_ - 1;
    while (b_ >= 0 && rankBuf [centroidBuf [b_]] < keyR)
    {
      centroidBuf [b_ + 1] = centroidBuf [b_];
      b_--;
    }
    centroidBuf [b_ + 1] = key;
  }

  // [F9] Обрезка: оставляем не более popSize/k центроидов.
  // Сортировка уже выполнена → в начале массива самые значимые лидеры.
  int maxCentroids = MathMax (1, popSize / kEff);
  if (nCentroids > maxCentroids) nCentroids = maxCentroids;

  //─── 7. Назначение агентов в суброи ────────────────────────────
  for (int i = 0; i < popSize; i++) subflockId [i] = -1;

  for (int s = 0; s < nCentroids; s++)
    subflockId [centroidBuf [s]] = s;

  for (int i = 0; i < popSize; i++)
  {
    if (subflockId [i] >= 0) continue;
    for (int s = 0; s < nCentroids; s++)
    {
      if (distM [i * popSize + centroidBuf [s]] <= dMax [centroidBuf [s]])
      {
        subflockId [i] = s;
        break;
      }
    }
  }

  //─── 8. Лидер суброя = агент с наилучшим fB в суброе ─────────
  // [F2] Используем fB (исторический лучший), а не f (текущий шумный).
  for (int s = 0; s < nCentroids; s++)
  {
    subflockLeader [s] = -1;
    subflockBestF  [s] = -DBL_MAX;
  }
  for (int i = 0; i < popSize; i++)
  {
    int s = subflockId [i];
    if (s < 0) continue;
    if (a [i].fB > subflockBestF [s])  // [F2] fB вместо f
    {
      subflockBestF  [s] = a [i].fB;
      subflockLeader [s] = i;
    }
  }

  // Страховка: если ни один агент суброя не имел fB > -DBL_MAX
  // (например, первая итерация), назначаем центроид лидером,
  // чтобы leaderIdx последователей никогда не был -1.
  for (int s = 0; s < nCentroids; s++)
    if (subflockLeader [s] < 0) subflockLeader [s] = centroidBuf [s];

  //─── 9. Роли агентов ─────────────────────────────────────────────
  for (int i = 0; i < popSize; i++)
  {
    int s = subflockId [i];
    if      (s < 0)
    {
      role      [i] = 2;
      leaderIdx [i] = -1;
    }
    else if (subflockLeader [s] == i)
    {
      role      [i] = 0;
      leaderIdx [i] = -1;
    }
    else
    {
      role      [i] = 1;
      leaderIdx [i] = subflockLeader [s];
    }
  }

  //─── 10. Снапшоты позиций и скоростей перед обновлением ──────────
  // [F4] Vsnap снимается здесь — Pass 2 использует согласованные
  //      старые скорости лидеров, а не уже изменённые в Pass 1.
  for (int i = 0; i < popSize; i++)
    for (int c = 0; c < coords; c++)
    {
      cPsnap [Idx (i, c)] = a [i].cP [c];
      Vsnap  [Idx (i, c)] = V  [Idx (i, c)];  // [F4]
    }

  //─── 11. Pass 1: ЛИДЕРЫ ──────────────────────────────────────────
  // v = w·v + r1·(cB_i − cP_i) + φ·r2·(gBest − cP_i) + sep
  // [F10] w=0.729 — коэффициент сужения Clerc-Kennedy (фиксированный).
  for (int i = 0; i < popSize; i++)
  {
    if (role [i] != 0) continue;

    // [F3] Смягчённый сброс скорости при первом появлении в роли лидера.
    //      Накопленный импульс приглушается вдвое, а не обнуляется.
    if (prevRole [i] != 0)
      for (int c = 0; c < coords; c++) V [Idx (i, c)] *= 0.5;  // [F3]

    CalcSep (i);

    for (int c = 0; c < coords; c++)
    {
      double r1   = u.RNDfromCI (0.0, 1.0);
      double r2   = u.RNDfromCI (0.0, 1.0);
      int    idx  = Idx (i, c);
      double vNew = 0.729 * V [idx]                              // [F10] инерция
                    + r1 * (a [i].cB [c] - cPsnap [idx])         // когнитивный
                    + phi * r2 * (cB [c]  - cPsnap [idx])        // глобальный
                    + sepVec [c];                                // separation

      vNew         = Clamp (vNew, -Vmax [c], Vmax [c]);
      a [i].cP [c] = Clamp (cPsnap [idx] + vNew, rangeMin [c], rangeMax [c]);
      V [idx]      = vNew;
    }
  }

  //─── 12. Pass 2: ПОСЛЕДОВАТЕЛИ ───────────────────────────────────
  // v = w·v + r1·(cP_L − cP_i) + r2·(Vsnap_L − Vsnap_i)
  //       + r3·(cB_i − cP_i) + φ·r4·(gBest − cP_i) + sep
  // [F4]  Выравнивание идёт по Vsnap лидера (старая скорость).
  // [F10] w=0.729 — коэффициент сужения Clerc-Kennedy (фиксированный).
  for (int i = 0; i < popSize; i++)
  {
    if (role [i] != 1) continue;
    int wi = leaderIdx [i];

    CalcSep (i);

    for (int c = 0; c < coords; c++)
    {
      double r1   = u.RNDfromCI (0.0, 1.0);
      double r2   = u.RNDfromCI (0.0, 1.0);
      double r3   = u.RNDfromCI (0.0, 1.0);
      double r4   = u.RNDfromCI (0.0, 1.0);
      int    idx  = Idx (i, c);
      int    idxL = Idx (wi, c);
      double vNew = 0.729 * V [idx]                             // [F10] инерция
                    + r1 * (cPsnap [idxL] - cPsnap [idx])       // Cohesion
                    + r2 * (Vsnap  [idxL] - Vsnap  [idx])       // Alignment [F4]
                    + r3 * (a [i].cB [c]  - cPsnap [idx])       // Когнитивный
                    + phi * r4 * (cB [c]  - cPsnap [idx])       // Глобальный
                    + sepVec [c];                               // Separation

      vNew         = Clamp (vNew, -Vmax [c], Vmax [c]);
      a [i].cP [c] = Clamp (cPsnap [idx] + vNew, rangeMin [c], rangeMax [c]);
      V [idx]      = vNew;
    }
  }

  //─── 13. Pass 3: ВЫБРОСЫ ─────────────────────────────────────────
  // [F5] r независим для каждой координаты — в многомерном
  //      пространстве координаты прыгают независимо.
  for (int i = 0; i < popSize; i++)
  {
    if (role [i] != 2) continue;

    for (int c = 0; c < coords; c++)
    {
      double r    = u.RNDfromCI (0.0, 1.0);  // [F5] per-coordinate
      double xNew = (r > 0.5)
                    ? cB [c] + u.RNDfromCI (-1.0, 1.0) * (rangeMax [c] - rangeMin [c]) * 0.2
                    : u.RNDfromCI (rangeMin [c], rangeMax [c]);

      a [i].cP [c]   = Clamp (xNew, rangeMin [c], rangeMax [c]);
      V [Idx (i, c)] = 0.0;
    }
  }

  //─── 14. Сохранение ролей ────────────────────────────────────────
  for (int i = 0; i < popSize; i++) prevRole [i] = role [i];
}
//————————————————————————————————————————————————————————————————————


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

Алгоритм тестировался на трёх стандартных функциях стенда — Hilly, Forest и Megacity — при трёх уровнях размерности: 10, 50 и 1000 координат соответственно, по 10000 вычислений на каждый прогон и 10 повторений. Это честный и однозначный ответ на исходный вопрос: в текущем виде алгоритм не является готовым инструментом для сложных задач подбора параметров торговой системы.

FBL|Flock by Leader|50.0|10.0|0.5|0.9|
=============================
5 Hilly's; Func runs: 10000; result: 0.6316210581453732
25 Hilly's; Func runs: 10000; result: 0.45555817277200505
500 Hilly's; Func runs: 10000; result: 0.2640456113290198
=============================
5 Forest's; Func runs: 10000; result: 0.6112393067788322
25 Forest's; Func runs: 10000; result: 0.4289609729927785
500 Forest's; Func runs: 10000; result: 0.1977883253085394
=============================
5 Megacity's; Func runs: 10000; result: 0.46153846153846156
25 Megacity's; Func runs: 10000; result: 0.2827692307692308
500 Megacity's; Func runs: 10000; result: 0.10590769230769326
=============================
All score: 3.43943 (38.22%)

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

Hilly

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

Forest

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

Megacity

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

Paraboloid

FBL на тестовой функции Paraboloid

GoldsteinPrice

FBL на тестовой функции GoldsteinPrice

Суммарный результат 38.22% не позволяет FBL войти в список 45 лучших методов оптимизации нашего стенда. Это честный и однозначный ответ на исходный вопрос: в текущем виде алгоритм не является готовым инструментом для сложных задач подбора параметров торговой системы. В рейтинговой таблице алгоритм FBL представлен ознакомительно.

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

FBL flock_by_leader 0,63162 0,45555 0,26404 1,35121 0,61123 0,42896 0,19778 1,23797 0,46153 0,28277 0,10590 0,85020 3,439 38,22

RW random walk 0,48754 0,32159 0,25781 1,06694 0,37554 0,21944 0,15877 0,75375 0,27969 0,14917 0,09847 0,52734 2,348 26,09



Выводы

Гипотеза подтвердилась частично. Архитектура «суброи по ARF + лидер по fB + выбросы» действительно формирует устойчивую иерархию: алгоритм избегает мгновенного схлопывания всей популяции в одну точку и позволяет нескольким суброям одновременно прорабатывать разные регионы ландшафта. Практический артефакт — реализация C_AO_FBL (псевдокод, класс для MQL5 с точками входа Init/Moving/Revision и параметрами popSize, k, phi, wSep) — готова к подключению к унифицированному тестовому стенду и повторяемым экспериментам.

Однако в текущей форме баланс разведки/эксплуатации смещён в пользу эксплуатации на малых размерностях: при 10 000 вычислений и параметрах popSize = 50, k = 10, phi = 0.5, wSep = 0.9 алгоритм даёт суммарный балл 38.22% — недостаточно для практической работы по подбору параметров торговых систем на нашем стенде. Конкретные узкие места и их локализация в коде/алгоритме:

  • Преждевременная потеря разнообразия суброев: глобальный компонент "phi" слишком быстро стягивает суброи к gBest, особенно на низкой размерности (механика Pass 1/Pass 2 и использование phi).
  • Отсутствие адаптации "phi": статическое значение "phi" не позволяет регулировать степень конвергенции в процессе поиска.
  • Высокая вычислительная стоимость O(n^2) из‑за пересчёта полной матрицы попарных расстояний каждую итерацию (узкое место при больших popSize).

Практические выводы и рекомендации:

  • Для прикладного использования FBL как рабочего инструмента нужно добавить минимум два улучшения: адаптивную стратегию phi (уменьшение притяжения по мере ухудшения диверсификации или по расписанию) и механизм поддержания/рестарта суброев (локальная реинициализация или архив перспективных точек).
  • Для ускорения реализации целесообразно заменить полную матрицу расстояний на приближённые k‑NN структуры (локальные эвристики, LSH или периодическое обновление матрицы).
  • FBL стоит рассматривать не как «готовый» инструмент, а как конструктивную базу: код можно сразу интегрировать в стенд для экспериментов, а узкие места локализованы и поддаются точечной доработке (адаптивный phi, динамический k, стратегии рестарта/мутации выбросов, уменьшение сложности поиска k‑NN).

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

tabl

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

chart

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


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

Плюсы:

  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 Testing AOs.mq5
Скрипт Единый испытательный стенд для всех популяционных алгоритмов оптимизации
9 Simple use of population optimization algorithms.mq5
Скрипт
Простой пример использования популяционных алгоритмов оптимизации без визуализации
10 Test_FBL.mq5
Скрипт Испытательный стенд для FBL

Прикрепленные файлы |
FBL.ZIP (440.71 KB)
Неопределенность как модель (Часть 3): Математическая статистика — как извлекать знания из данных Неопределенность как модель (Часть 3): Математическая статистика — как извлекать знания из данных
В данной части цикла разбираются механизмы Закона больших чисел (ЗБЧ) и Центральной предельной теоремы (ЦПТ) как теоретической основы для понимания рыночных закономерностей. Описывается инструментарий описательной статистики и методы нахождения точечных и интервальных оценок параметров распределений. Особое внимание уделено методологии проверки статистических гипотез, позволяющей объективно отделять истинные рыночные аномалии от случайного шума. Каждое теоретическое построение сопровождено практическим примером в приложении, что позволяет закрепить материал на конкретных данных.
Создание и тестирование совета из 15 моделей в MetaTrader 5 Создание и тестирование совета из 15 моделей в MetaTrader 5
Статья описывает переход от дебатов четырёх голосов к Council of 15: десять аналитиков, четыре независимых риск-менеджера и Председатель с жёстким регламентом голосования. Разобраны роли участников, трёхфазная архитектура и параллельное исполнение полного цикла за 10–15 секунд. Показаны журнал работы, правила риск-гейта и обратная совместимость, чтобы вы быстро подключили систему к советнику.
Внедрение в MQL5 практических модулей из других языков (Часть 03): Модуль schedule из Python — расширенные возможности OnTimer Внедрение в MQL5 практических модулей из других языков (Часть 03): Модуль schedule из Python — расширенные возможности OnTimer
Модуль schedule в Python предоставляет простой способ планирования повторяющихся задач. Хотя в MQL5 отсутствует встроенный аналог, в этой статье мы реализуем аналогичную библиотеку, чтобы упростить настройку событий по расписанию в MetaTrader 5.
Торговые инструменты MQL5 (Часть 12): Улучшение интерактивности панели корреляционной матрицы Торговые инструменты MQL5 (Часть 12): Улучшение интерактивности панели корреляционной матрицы
В этой статье мы улучшаем панель корреляционной матрицы в MQL5 с помощью интерактивных признаков, таких как перетаскивание панели, сворачивание / разворачивание, эффекты при наведении курсора мыши на кнопки и таймфреймы, а также обработка событий мыши для улучшения взаимодействия с пользователем. Мы добавили сортировку символов по средней силе корреляции в восходящем/нисходящем режимах, переключение между отображением корреляции и p-значения, а также включили переключение между светлой и темной темами с динамическим обновлением цвета.