Роевой оптимизатор с иерархией суброев — Flock by Leader
Содержание
Введение
Если вы оптимизируете параметры торговой системы или другую высокоразмерную модель с помощью роевых методов, вы часто сталкиваетесь с двумя конфликтующими проблемами: популяция быстро «схлопывается» к одной точке и прекращает разведку, а попытка удержать разнообразие резко увеличивает вычислительные затраты и разброс результатов. При фиксированном бюджете вычислений хотелось бы, чтобы алгоритм одновременно: (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", другая часть уходит в случайные места. В высокоразмерном пространстве это принципиально расширяет охват поиска. После любого прыжка скорость агента обнуляется — накопленная инерция предыдущего движения неактуальна для новой случайной позиции. Рассмотрим ниже иллюстрацию.

Рисунок 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-визуализации тестового стенда переработана и переписана. Теперь можно видеть функции в движении и оценивать работу алгоритма ещё нагляднее.

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

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

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

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

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).
Итого: архитектура интересна и воспроизводима, даёт преимущества на больших размерностях, но требует доработок по управлению разнообразием и по снижению вычислительной нагрузки, прежде чем стать надёжным инструментом для прикладной оптимизации параметров торговых систем.

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

Рисунок 3. Гистограмма результатов тестирования алгоритмов (по шкале от 0 до 100, чем больше, тем лучше, где 100 — максимально возможный теоретический результат), в архиве скрипт для расчета рейтинговой таблицы
Плюсы и минусы алгоритма FBL
Плюсы:
- Хорошие показатели на высоких размерностях задач.
Минусы:
- Слабая сходимость.
- Вычислительная сложность.
К статье прикреплён архив с актуальными версиями кодов алгоритмов. Автор статьи не несёт ответственности за абсолютную точность в описании канонических алгоритмов, во многие из них были внесены изменения для улучшения поисковых возможностей. Выводы и суждения, представленные в статьях, основываются на результатах проведённых экспериментов.
Программы, используемые в статье
| # | Имя | Тип | Описание |
|---|---|---|---|
| 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 |
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Неопределенность как модель (Часть 3): Математическая статистика — как извлекать знания из данных
Создание и тестирование совета из 15 моделей в MetaTrader 5
Внедрение в MQL5 практических модулей из других языков (Часть 03): Модуль schedule из Python — расширенные возможности OnTimer
Торговые инструменты MQL5 (Часть 12): Улучшение интерактивности панели корреляционной матрицы
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования