preview
Алгоритм оптимизации быков — Bull Optimization Algorithm (BOA)

Алгоритм оптимизации быков — Bull Optimization Algorithm (BOA)

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

Содержание

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


Введение

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

Самый популярный кандидат — генетический алгоритм (GA), хорошо изученный и заслуженно проверенный десятилетиями практики. Он стал штатным оптимизатором в MetaTrader 5. Тем не менее с момента появления GA исследователи в области метаэвристик регулярно задают вопрос: можно ли модифицировать классическую схему так, чтобы получить алгоритм с другим характером поведения — иначе устроенным балансом исследования и уточнения, иной механикой передачи информации между поколениями? Подобные эксперименты порождают семейство алгоритмов «на основе генетических операторов», в котором классический GA — лишь одна из возможных реализаций.

В 2015 году турецкий исследователь Огуз Финдик опубликовал работу, в которой предложил один из таких экспериментов. Финдик сформулировал три наблюдения о поведении классического GA, с которыми он считал нужным поработать: о роли оператора селекции при сходимости популяции, о характере классической мутации, о пассивной роли лучшей найденной особи. На основании этих наблюдений он построил алгоритм с другой архитектурой: оператор селекции из цикла удалён, лучшая особь становится активным родителем для всех остальных через скрещивание, а вместо классической мутации работает мультипликативная — пропорциональная текущему значению гена. Алгоритм получил название Bull Optimization Algorithm (BOA).

Заявка серьёзная, но в нашем рейтинге заслуженно место занимает только тот алгоритм, который доказал свою эффективность на нашем стандартном стенде. К моменту прочтения этой статьи у читателя на руках будет: понимание того, как три архитектурных решения Финдика складываются в работающий алгоритм; разбор каждого решения с формулой и числовым примером; готовая реализация на MQL5 в формате фреймворка C_AO и тестовый скрипт; результаты прогона на стандартном стенде (Hilly, Forest, Megacity при разных размерностях поиска) и на античитер-стенде с пятью разнородными функциями; честная оценка того, в каких задачах BOA конкурентоспособен, а в каких его мультипликативная мутация играет против него.

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


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

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

Наблюдение первое:селекция теряет смысл при сходимости. Представим, что популяция из 50 особей уже немного сошлась — все они находятся в окрестности какого-то локального максимума, и фитнес-значения у них колеблются, скажем, в диапазоне от 0.847 до 0.853. Рулеточная селекция, самая популярная в ГА, выбирает родителей с вероятностями, пропорциональными их фитнесу. Но 0.847 и 0.853 для рулетки — это практически одно и то же. Селекция вырождается в случайный выбор, и алгоритм теряет «давление отбора» именно тогда, когда оно нужнее всего — на этапе тонкой настройки.

Наблюдение второе: мутация не различает «уточнить» и «бросить всё». Классическая ГА-мутация для гена с диапазоном [-100, 100] выглядит так: с вероятностью p_mut ген заменяется случайным значением из [-100, 100]. Если ген уже принял значение 0.0023 — близко к оптимуму, — то мутация почти наверняка выкинет его обратно куда-нибудь в [-87, +43]. Это не «исследовать окрестность», это «сбросить достижение». Поэтому в ГА мутацию приходится делать редкой (типичные значения 0.01–0.05), но редкая мутация плохо вытаскивает из локальных минимумов.

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

Эти три наблюдения автора вместе и легли в основу BOA. Каждое из них имеет разную степень дискуссионности — но как стартовая точка для эксперимента они образуют последовательную линию рассуждений. Что получилось в итоге — алгоритм, к разбору которого мы сейчас и переходим.

Идея 1: вместо селекции — обновление всех. Финдик убирает оператор селекции полностью. Особь не «соревнуется за выживание» — она просто живёт от итерации к итерации, и на каждой эпохе её координаты пересчитываются через скрещивание с лидером и мутацию. Лучший результат хранится глобально как cB (current Best) и обновляется каждый раз, когда какая-то особь дала фитнес выше предыдущего рекорда.

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

Идея 2: скрещивание всегда с лучшей особью. Двухточечный кроссовер в BOA устроен максимально просто. Для каждой особи i генерируются два целых индекса p1, p2 ∈ [0, D), где D — размерность задачи. Пусть lo = min(p1, p2), hi = max(p1, p2). Тогда для всех j ∈ [lo, hi]:

xijbestj

x_{ij} \leftarrow \text{best}_j​То есть участок генома [lo, hi] перезаписывается участком из лучшей особи. Это не классический обмен сегментами между двумя случайными родителями, а целенаправленное «перетягивание» части координат к лидеру.

Пусть D = 6, особь x = [3.2, -1.5, 0.8, 4.1, 2.0, -0.7], а лучшая best = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0]. Сгенерировались p1 = 4, p2 = 1, то есть lo = 1, hi = 4. После кроссовера:

x=[3.2,1.0,1.0,1.0,1.0,0.7]x = [3.2, \mathbf{1.0}, \mathbf{1.0}, \mathbf{1.0}, \mathbf{1.0}, -0.7]

Особь подтянулась к лидеру в четырёх координатах из шести.

Идея 3: мультипликативная мутация. Это самая неортодоксальная часть алгоритма. Формула мутации:

vij={xij+ωr1xij,r20.5xijωr1xij,r2>0.5r1,r2U[0,1]v_{ij} = \begin{cases} x_{ij} + \omega \cdot r_1 \cdot x_{ij}, & r_2 \le 0.5 \\ x_{ij} - \omega \cdot r_1 \cdot x_{ij}, & r_2 > 0.5 \end{cases} \quad r_1, r_2 \sim U[0,1]

Здесь ω — параметр инерции (масштаб мутации), r_1 и r_2 — независимые случайные числа. Ключевое отличие от классической мутации: возмущение пропорционально самому значению гена, а не диапазону его допустимых значений.

Пусть ω = 0.1, x_ij = 5.0, r_1 = 0.6, r_2 = 0.3. Условие r_2 ≤ 0.5 выполнено, поэтому:

vij=5.0+0.10.65.0=5.0+0.3=5.3v_{ij} = 5.0 + 0.1 \cdot 0.6 \cdot 5.0 = 5.0 + 0.3 = 5.3

А теперь пусть тот же ген принял значение x_ij = 0.01 — близко к нулю. С теми же ω, r_1, r_2:

vij=0.01+0.10.60.01=0.01+0.0006=0.0106v_{ij} = 0.01 + 0.1 \cdot 0.6 \cdot 0.01 = 0.01 + 0.0006 = 0.0106

Мутация изменила ген на 0.06% — на масштабе, согласованном с самим геном. Это очень полезное свойство для функций, у которых оптимум находится вблизи нуля: когда координата приближается к нулю, шаг мутации тоже сжимается, и алгоритм сходится с высокой точностью без необходимости вручную уменьшать "ω".

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

Идея 4 (вспомогательная): хаотическая мутация. Чтобы дать алгоритму шанс выскочить из локальных минимумов и из «нулевой ловушки» мультипликативной мутации, для каждого гена с малой вероятностью ChMR (типичное значение 0.001) выполняется случайный сброс:

xijxjmin+r(xjmaxxjmin),rU[0,1]x_{ij} \leftarrow x_j^{min} + r \cdot (x_j^{max} - x_j^{min}), \quad r \sim U[0,1]

Это эквивалент классической ГА-мутации, но включается крайне редко — в среднем один ген на тысячу пересчётов. Достаточно, чтобы изредка перебрасывать особь в случайную точку пространства; недостаточно, чтобы разрушать прогресс.

Идея 5 (вспомогательная): мутация лучшей особи. Чтобы усилить локальный поиск вокруг лидера, BOA дополнительно мутирует саму cB по той же формуле (11). Если результат окажется лучше — cB обновится. Это даёт постоянное «дрожание» лидера в окрестности его текущей позиции, что в сочетании с мультипликативной мутацией даёт точную сходимость на финальных эпохах.

Линейный спад инерции. В статье он явно не специфицирован, но параметры wMax и wMin в неявном виде подразумевают изменение ω во времени. Принимаем линейный спад в духе классического PSO Shi-Eberhart:

ω(t)=ωmax(ωmaxωmin)tTmax\omega(t) = \omega_{max} - (\omega_{max} - \omega_{min}) \cdot \frac{t}{T_{max}}

В начале оптимизации ω = wMax = 0.1 — мутация ведёт себя как глобальный поиск. К концу ω = wMin = 0.0001 — мутация становится тонким локальным уточнением. Эта схема даёт классический баланс exploration/exploitation, который BOA реализует не через изменение структуры алгоритма, а через изменение единственного параметра.

BOA_flowchart

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

Иллюстрация состоит из двух смысловых блоков:

  • Верхний — pipeline одной эпохи: популяция → кроссовер с лучшей особью cB (показан перенос участка генома цветными ячейками) → мультипликативная мутация с тремя ступенями (основная / хаотическая / знаковая) → оценка и обновление лучшего, без оператора селекции. Зарезервированный последний слот под мутированную копию cB вынесен отдельно. Красная пунктирная стрелка возвращает поток к началу следующей эпохи.
  • Нижний — механика мультипликативности: два числовых примера на одной формуле Δ = ω·r₁·x. Слева x = 5.0 даёт Δ = 0.30 (заметный шаг), справа x = 0.01 даёт Δ = 0.0006 (микроскопический шаг — точка почти на месте). Между панелями — главный вывод: самонастраивающийся масштаб шага, дающий точную сходимость к нулю, но превращающий сам нуль в ловушку при смещённом оптимуме.

Теперь напишем псевдокод алгоритма.

ИНИЦИАЛИЗАЦИЯ:
    Установить параметры: popSize, CR, MR, MSR, ChMR, ω_max, ω_min
    epochs   ← число эпох оптимизации
    epochNow ← 0
    ω        ← ω_max

    Для каждой особи i = 1 … popSize:
        Для каждой координаты c = 1 … D:
            x[i][c] ← random(rangeMin[c], rangeMax[c])    // Eq. (10)

ОСНОВНОЙ ЦИКЛ (повторять до завершения):

    // ──────────── ОЦЕНКА ────────────
    Вычислить фитнес f[i] для всех особей популяции

    // ──────────── ОБНОВЛЕНИЕ ЛУЧШИХ ────────────
    Для каждой особи i:
        Если f[i] > fB[i] (личный рекорд):
            fB[i] ← f[i];  cB_личн[i] ← x[i]
        Если f[i] > fB (глобальный рекорд):
            fB ← f[i];     cB ← x[i]

    // ──────────── СПАД ИНЕРЦИИ ────────────
    epochNow ← epochNow + 1
    ω ← ω_max − (ω_max − ω_min) · (epochNow / epochs)

    // ──────────── ОБНОВЛЕНИЕ ПОПУЛЯЦИИ ────────────
    // Последний слот резервируется под мутацию лидера → обработка с 1 до popSize-1
    Для каждой особи i = 1 … popSize − 1:

        // ─── ШАГ 1. Кроссовер с лучшей особью cB ───
        Если random(0,1) ≤ CR:
            p1 ← randInt(0, D);   p2 ← randInt(0, D)
            lo ← min(p1, p2);     hi ← max(p1, p2)
            Для c = lo … hi:
                x[i][c] ← cB[c]                            // перезапись участка генами лидера

        // ─── ШАГ 2. Мультипликативная мутация (Eq. 11) ───
        Для каждой координаты c = 1 … D:
            v ← x[i][c]

            Если random(0,1) ≤ MR:
                r1 ← random(0,1);   r2 ← random(0,1)
                Если r2 ≤ 0.5:
                    v ← v + ω · r1 · v                     // Eq. (11), знак "+"
                Иначе:
                    v ← v − ω · r1 · v                     // Eq. (11), знак "−"

            // ─── ШАГ 3. Хаотическая мутация (выход из локальных минимумов) ───
            Если random(0,1) ≤ ChMR:
                v ← random(rangeMin[c], rangeMax[c])

            // ─── ШАГ 4. Знаковая мутация (редкая инверсия) ───
            Если random(0,1) ≤ MSR:
                v ← −v

            x[i][c] ← clamp(v, rangeMin[c], rangeMax[c])

    // ─── ШАГ 5. Локальный поиск вокруг лидера ───
    // Последний слот популяции = мутированная копия cB
    Для каждой координаты c = 1 … D:
        v ← cB[c]

        Если random(0,1) ≤ MR:
            r1 ← random(0,1);   r2 ← random(0,1)
            Если r2 ≤ 0.5:  v ← v + ω · r1 · v
            Иначе:          v ← v − ω · r1 · v

        Если random(0,1) ≤ ChMR:
            v ← random(rangeMin[c], rangeMax[c])

        Если random(0,1) ≤ MSR:
            v ← −v

        x[popSize][c] ← clamp(v, rangeMin[c], rangeMax[c])

ВЕРНУТЬ: cB, fB

Приступим к написанию реализации алгоритма.

Класс C_AO_BOA_Bull. Класс наследуется от базового C_AO — стандартного интерфейса фреймворка популяционных алгоритмов. Это даёт ему «бесплатно» всю инфраструктуру: массив агентов a[] с координатами c[]/cP[]/cB[]/cW[] и фитнесами f/fP/fB/fW, глобальные cB/fB для лучшей особи, массив параметров params[], утилиты "u" (генераторы случайных чисел, дискретизация, проверки границ), а также стандартный жизненный цикл Init() → Moving() → Revision(). Реализующему алгоритму остаётся только определить три виртуальных метода, что и сделано.

Параметры алгоритма (декларированы как public-поля для прозрачности):

  • CR — crossover rate, вероятность применения двухточечного кроссовера к особи на каждой эпохе. При CR = 1.0 (значение по умолчанию из статьи) кроссовер применяется всегда.
  • MR — mutation rate, вероятность применения основной мультипликативной мутации к каждой координате. Дефолт 1.0 означает, что каждый ген на каждой эпохе проходит через формулу (11).
  • MSR — mutation sign rate, вероятность редкой инверсии знака координаты. Финдик упоминает этот параметр, но не определяет формулой; принятая интерпретация — x ← -x с малой вероятностью (≈0.001).
  • ChMR — chaotic mutation rate, вероятность случайного сброса координаты в случайную точку допустимого диапазона. Единственный механизм, способный вытащить агента из локальных минимумов, в которые его загоняет мультипликативная мутация.
  • wMax, wMin — границы инерционного веса "ω", который линейно убывает от первого ко второму по ходу оптимизации. Управляет балансом exploration/exploitation: в начале мутации крупные, к концу — микроскопические.

Приватные поля (служебные, для управления инерцией):

  • epochs — общее число эпох, переданное при инициализации; нужно для расчёта линейного спада "ω".
  • epochNow — счётчик текущей эпохи, инкрементируется в Revision.
  • wCur — текущее значение инерции, пересчитываемое на каждой эпохе.
  • Clamp — приватная утилита, обрезающая значение по границам [lo, hi]. Используется после мутации, потому что мультипликативная мутация может выкинуть координату за пределы допустимого диапазона.

Конструктор C_AO_BOA_Bull() заполняет три служебных поля базового класса (ao_name, ao_desc, ao_link), которые используются для идентификации алгоритма в логах и интерфейсе. Затем выставляет параметры по умолчанию, и регистрирует их в массиве params[]. Регистрация двойная — поле name для отображения и поле val для значения — это нужно, чтобы внешний скрипт мог как читать параметры по имени, так и подменять их через стандартный интерфейс.

Метод SetParams(). Обратное действие к конструктору: переносит значения из массива params[] обратно в именованные поля класса. Вызывается после того, как внешний скрипт обновил params[i].val своими input-значениями. Без этого шага класс работал бы со значениями по умолчанию, игнорируя пользовательскую настройку.

//+------------------------------------------------------------------+
class C_AO_BOA_Bull : public C_AO
 {
public:
                    ~C_AO_BOA_Bull() {}
                     C_AO_BOA_Bull()
   {
    ao_name = "BOA";
    ao_desc = "Bull Optimization Algorithm";
    ao_link = "https://www.mql5.com/ru/articles/22390";

    popSize = 50;
    CR      = 1.0;
    MR      = 1.0;
    MSR     = 0.001;
    ChMR    = 0.001;
    wMax    = 0.1;
    wMin    = 0.0001;

    ArrayResize(params, 7);
    params [0].name = "popSize"; params [0].val = popSize;
    params [1].name = "CR";      params [1].val = CR;
    params [2].name = "MR";      params [2].val = MR;
    params [3].name = "MSR";     params [3].val = MSR;
    params [4].name = "ChMR";    params [4].val = ChMR;
    params [5].name = "wMax";    params [5].val = wMax;
    params [6].name = "wMin";    params [6].val = wMin;
   }

  void               SetParams()
   {
    popSize = (int)params [0].val;
    CR      = params      [1].val;
    MR      = params      [2].val;
    MSR     = params      [3].val;
    ChMR    = params      [4].val;
    wMax    = params      [5].val;
    wMin    = params      [6].val;
   }

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

  void               Moving();
  void               Revision();

  //------------------------------------------------------------------
  double             CR;    // crossover rate                       [0..1]
  double             MR;    // mutation rate                        [0..1]
  double             MSR;   // mutation sign rate (редкая инверсия) [≈0.001]
  double             ChMR;  // chaotic mutation rate                [≈0.001]
  double             wMax;  // ω_max — масштаб мутации в начале     [≈0.1]
  double             wMin;  // ω_min — масштаб мутации в конце      [≈0.0001]

private: //----------------------------------------------------------
  int                epochs;   // всего эпох (для линейного спада ω)
  int                epochNow; // номер текущей эпохи
  double             wCur;     // текущее значение инерции

  double             Clamp(double v, double lo, double hi)
   {
    return v < lo ? lo : v > hi ? hi : v;
   }
 };
//+------------------------------------------------------------------+
Метод Init. Подготовка к запуску оптимизации.

Первая строка — StandardInit() — это вызов унаследованной от C_AO процедуры, которая инициализирует размер популяции, массив агентов, копирует диапазоны поиска во внутренние поля, обнуляет лучшую и худшую найденные приспособленности. Если стандартная инициализация провалилась (например, размерность задачи 0 или несогласованные размеры массивов), Init() возвращает false и дальше ничего не происходит.

Затем сохраняется число эпох (с защитой от деления на ноль через epochsP > 0 ? epochsP : 1), обнуляется счётчик текущей эпохи, и wCur устанавливается в стартовое значение wMax — отсюда инерция начнёт спадать.

Главная содержательная часть — двойной цикл, реализующий формулу (10) из статьи: каждая координата каждого агента инициализируется случайным значением из допустимого диапазона. Запись идёт в cP[] (previous coordinates), потому что Moving() ожидает увидеть «предыдущую позицию» и из неё вычислить «текущую» с учётом дискретизации шагом сетки.

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

  epochs   = epochsP > 0 ? epochsP : 1;
  epochNow = 0;
  wCur     = wMax;

//--- ур. (10): начальная популяция в [rangeMin, rangeMax]
  for(int i = 0; i < popSize; i++)
    for(int c = 0; c < coords; c++)
      a [i].cP [c] = u.RNDfromCI(rangeMin [c], rangeMax [c]);

  return true;
 }
//+------------------------------------------------------------------+
Метод Moving(). Выполняется первым на каждой эпохе оптимизатора, до вычисления фитнеса. Его задача — подготовить координаты для оценки.

В BOA метод тривиален: он просто копирует "cP" в "c", но через утилиту u.SeInDiSp(), которая делает две вещи. Во-первых, обрезает значение по границам [rangeMin, rangeMax] (защита от выхода за пределы поиска). Во-вторых, дискретизирует значение по шагу сетки rangeStep — если rangeStep[c] = 0.5, координата округлится до ближайшего кратного 0.5. На большинстве задач rangeStep = 0 и дискретизация выключена, но для дискретных задач (Megacity и подобных) этот шаг критичен.

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

//+------------------------------------------------------------------+
void C_AO_BOA_Bull::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]);
 }
//+------------------------------------------------------------------+

Метод Revision() — сердце алгоритма. Вызывается после того, как фитнес-функция оценила всех агентов. Состоит из четырёх блоков, последовательно обновляющих популяцию.

Блок 1: обновление личных и глобального рекордов. Стандартный цикл: для каждого агента, если его текущий фитнес f лучше его же личного рекорда fB, обновляем личный рекорд и копируем координаты в a[i].cB. Параллельно сравниваем с глобальным fB всего класса; если кто-то из агентов превзошёл его, обновляем глобальные fB и cB. Именно глобальный cB — это «бык», лидер, который дальше станет партнёром по скрещиванию для всей популяции.

Блок 2: линейный спад инерции. Счётчик эпох инкрементируется, и wCur пересчитывается по формуле линейной интерполяции: чем дальше зашли по числу эпох, тем меньше "ω". Защитная проверка if(wCur < wMin) срабатывает в случае, когда epochNow превысил заявленные epochs (бывает при переменной длине прогона) — ω фиксируется на минимуме, а не уходит в отрицательные значения, что разрушило бы мутацию.

Блок 3: основной цикл — обновление популяции без селекции. Здесь работает ключевая идея BOA. Цикл идёт от 0 до popSize - 2 (то есть обрабатывает все агенты, кроме последнего — этот слот зарезервирован для блока 4).

Внутри цикла каждый агент проходит две фазы:

  • фаза 3.1 — двухточечный кроссовер с cB. С вероятностью CR запускается процедура: генерируются два целочисленных индекса в диапазоне [0, coords) через (int)u.RNDfromCI(0.0, coords). Защитные проверки if(p1 >= coords) p1 = coords - 1 нужны на случай, если генератор случайных чисел из-за округления вернул значение точно равное coords. Затем определяется отрезок [lo, hi] от меньшего к большему индексу, и в этом отрезке координаты агента перезаписываются координатами лидера. Это не симметричный обмен сегментами как в классической ГА — это однонаправленное «притягивание» части генома к лучшему.
  • фаза 3.2 — мутация координат. По каждой координате последовательно проверяются три независимых события.

Первое — основная мультипликативная мутация по формуле (11). С вероятностью MR (обычно 1.0) генерируются r1, r2 ∈ U[0,1]. Если r2 ≤ 0.5, координата увеличивается на ω·r1·v, иначе уменьшается на ту же величину. Шаг пропорционален самому значению координаты — это и есть «мультипликативность»: возле нуля шаг крошечный, вдали от нуля заметный.

Второе — хаотическая мутация с вероятностью ChMR (обычно 0.001). Координата сбрасывается в случайную точку допустимого диапазона. Это единственный механизм глобального исследования после того, как основная мутация загнала координаты в малые значения.

Третье — знаковая мутация с вероятностью MSR (обычно 0.001). Координата меняет знак: v ← -v. Полезна на симметричных функциях, где оптимум может находиться в зеркальной точке относительно нуля.

После всех трёх мутаций результат обрезается по границам через Clamp() и записывается обратно в cP[] агента — на следующей эпохе Moving() подхватит это значение и подаст в фитнес-функцию.

Блок 4: локальный поиск вокруг лидера. Последний слот популяции (popSize - 1) на каждой эпохе заполняется мутированной копией cB. Логика мутации та же, что в блоке 3.2 — три ступени с теми же вероятностями, но стартовая точка не координаты текущего агента, а сам лидер.

Этот трюк решает важную задачу: в BOA лидер сам по себе не двигается, его обновление возможно только когда какой-то другой агент случайно набрёл на лучшее место. Мутированный слот лидера даёт лидеру шанс улучшить себя собственными силами — если мутация дала более высокий фитнес, на следующей эпохе блок 1 обновит cB этим значением. Это можно рассматривать как встроенный «локальный поиск» внутри популяционного алгоритма, без накладных расходов на отдельный механизм. Цена решения — один агент из 50 не используется как самостоятельная поисковая единица, а служит «зеркалом» для лидера.

//+------------------------------------------------------------------+
void C_AO_BOA_Bull::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. Линейный спад инерции: ω_max → ω_min --------------------
  epochNow++;
  wCur = wMax - (wMax - wMin) * ((double)epochNow / (double)epochs);
  if(wCur < wMin)
    wCur = wMin;

//--- 3. Обновление популяции (никакой селекции — особь всегда
//       замещается результатом crossover+mutation) ---------------
//
// ВНИМАНИЕ: один слот (последний) резервируется под мутированную
// версию gBest — для усиления локального поиска вокруг лидера.
//
  int N = popSize - 1;

  for(int i = 0; i < N; i++)
   {
    //--- 3.1 Двухточечный кроссовер с лучшей особью ---------------
    if(u.RNDfromCI(0.0, 1.0) <= CR)
     {
      int p1 = (int)u.RNDfromCI(0.0, (double)coords);
      int p2 = (int)u.RNDfromCI(0.0, (double)coords);
      if(p1 >= coords)
        p1 = coords - 1;
      if(p2 >= coords)
        p2 = coords - 1;
      int lo = p1 < p2 ? p1 : p2;
      int hi = p1 < p2 ? p2 : p1;
      for(int c = lo; c <= hi; c++)
        a [i].cP [c] = cB [c];
     }

    //--- 3.2 Мутация по ур. (11) + хаотическая + знаковая ---------
    for(int c = 0; c < coords; c++)
     {
      double v = a [i].cP [c];

      // Основная мутация: v = v ± ω·r1·v
      if(u.RNDfromCI(0.0, 1.0) <= MR)
       {
        double r1 = u.RNDfromCI(0.0, 1.0);
        double r2 = u.RNDfromCI(0.0, 1.0);
        if(r2 <= 0.5)
          v = v + wCur * r1 * v;
        else
          v = v - wCur * r1 * v;
       }

      // Хаотическая мутация: случайный сброс в [min, max]
      if(u.RNDfromCI(0.0, 1.0) <= ChMR)
        v = u.RNDfromCI(rangeMin [c], rangeMax [c]);

      // Знаковая мутация: редкая инверсия знака гена
      if(u.RNDfromCI(0.0, 1.0) <= MSR)
        v = -v;

      a [i].cP [c] = Clamp(v, rangeMin [c], rangeMax [c]);
     }
   }

//--- 4. Локальный поиск вокруг лидера ---------------------------
// Последний слот заполняется мутированной копией cB.
// Если она окажется лучше — обновится fB на следующей итерации.
  for(int c = 0; c < coords; c++)
   {
    double v = cB [c];

    if(u.RNDfromCI(0.0, 1.0) <= MR)
     {
      double r1 = u.RNDfromCI(0.0, 1.0);
      double r2 = u.RNDfromCI(0.0, 1.0);
      if(r2 <= 0.5)
        v = v + wCur * r1 * v;
      else
        v = v - wCur * r1 * v;
     }

    if(u.RNDfromCI(0.0, 1.0) <= ChMR)
      v = u.RNDfromCI(rangeMin [c], rangeMax [c]);

    if(u.RNDfromCI(0.0, 1.0) <= MSR)
      v = -v;

    a [popSize - 1].cP [c] = Clamp(v, rangeMin [c], rangeMax [c]);
   }
 }
//+------------------------------------------------------------------+


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

Итоговый счёт 46.39% на полном стенде — это на доли процента ниже порога входа в топ-45. Алгоритм не попадает в рейтинговую таблицу. Решающей оказалась размерность: на 5-мерных задачах BOA даёт уровень верхней трети рейтинга, на 25-мерных — середины, на 500-мерных проваливается. Причина структурная: мультипликативная мутация v = x ± ω·r·x при размерности 500 даёт настолько мелкий шаг по каждой координате, что хаотическая мутация с вероятностью 0.001 оказывается единственным реальным механизмом исследования — а 0.5 события на эпоху для пространства из 500 координат просто недостаточно.

BOA|Bull Optimization Algorithm|50.0|1.0|0.1|0.01|0.001|0.1|0.0001|
=============================
5 Hilly's; Func runs: 10000; result: 0.7128139073590452
25 Hilly's; Func runs: 10000; result: 0.5190082455068694
500 Hilly's; Func runs: 10000; result: 0.28947759543651924
=============================
5 Forest's; Func runs: 10000; result: 0.9696150570558139
25 Forest's; Func runs: 10000; result: 0.38037078784762096
500 Forest's; Func runs: 10000; result: 0.06579884061143552
=============================
5 Megacity's; Func runs: 10000; result: 0.7253333333333333
25 Megacity's; Func runs: 10000; result: 0.376
500 Megacity's; Func runs: 10000; result: 0.1366933333333343
=============================
All score: 4.17511 (46.39%)

Античитер-тест на пяти разных функциях с разнесёнными оптимумами подтвердил, что результаты, полученные BOA на стандартном стенде в малой размерности, не являются артефактом — алгоритм действительно находит компромиссные решения для разнородных ландшафтов. Forest 10D = 0.97 не «жульничает» через диагональное стягивание, как это было у HHO; кроссовер с лидером тянет популяцию к настоящему оптимуму, а не к удачному совпадению характера тестовой функции с архитектурой алгоритма. 

BOA|Bull Optimization Algorithm|50.0|1.0|0.1|0.01|0.001|0.1|0.0001|
=============================
Composite test: Hilly + Forest + Megacity + Peaks + Skin
Coordinates: 10; Epochs: 200; Repeats: 10
=============================
Run 1/10: 0.8534132121249
Run 2/10: 0.7319045719778601
Run 3/10: 0.9835678308131334
Run 4/10: 0.9799860818205021
Run 5/10: 0.9799860818205021
Run 6/10: 0.7128042825713253
Run 7/10: 0.9456553720712788
Run 8/10: 0.9456553720712788
Run 9/10: 0.8871885236634757
Run 10/10: 0.6966858914401158
=============================
Average result: 0.8716847220 (87.17%)
=============================

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

Hilly

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

Forest

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

Megacity

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

Ackley

BOA на функции Ackley

Peaks

BOA на функции Peaks

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

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


Выводы

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

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

Первый — реализация BOA в формате C_AO, готовая к подключению наряду с любым другим алгоритмом фреймворка. Для задач оптимизации с 5–25 параметрами (а именно столько обычно имеет средняя торговая стратегия — периоды индикаторов, уровни SL/TP, фильтры по волатильности, веса в композитных сигналах) BOA даёт результат, сопоставимый с алгоритмами рейтинга. Это не лучший инструмент в ящике, но это рабочий инструмент с осмысленной механикой, и в портфеле оптимизаторов ему есть место — особенно когда нужна быстрая сходимость и точное уточнение около найденного экстремума.

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

Третий — понимание самой идеи мультипликативного шага. Это инженерная находка: шаг возмущения, автоматически согласованный с масштабом самой переменной. Идея не привязана к BOA и может быть перенесена в другие алгоритмы — в гибридах с PSO, DE, в кастомных решениях для конкретных торговых задач. Семь параметров настройки BOA (CR, MR, MSR, ChMR, wMax, wMin, popSize) дают широкое поле для экспериментов.

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

tab

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

chart

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


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

Плюсы:

  1. Решает задачи до средней размерности.

Минусы:

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

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



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

#ИмяТипОписание
1#C_AO.mqh
Включаемый файл
Родительский класс популяционных алгоритмов оптимизации
2#C_AO_enum.mqh
Включаемый файл
Перечисление популяционных алгоритмов оптимизации
3TestFunctions.mqh
Включаемый файл
Библиотека тестовых функций
4
TestStandFunctions.mqh
Включаемый файл
Библиотека функций тестового стенда
5TestStand3D.mqhВключаемый файл3D-панель визуализации для тестового стенда 
6Utilities.mqh
Включаемый файл
Библиотека вспомогательных функций
7CalculationTestResults.mqh
Включаемый файл
Скрипт для расчета результатов в сравнительную таблицу
8Test_AO_All.mq5
СкриптЕдиный испытательный стенд для всех популяционных алгоритмов оптимизации
9Test_AO_AntiCheatСкриптТест на читерство алгоритмов оптимизации
10Simple use of population optimization algorithms.mq5
Скрипт
Простой пример использования популяционных алгоритмов оптимизации без визуализации
11Test_AO_BOA.mq5
СкриптИспытательный стенд для BOA
Прикрепленные файлы |
BOA_Bull.zip (391.09 KB)
Основы байесовского вывода в дискретном и непрерывном случаях: от теории к практической реализации моделей Основы байесовского вывода в дискретном и непрерывном случаях: от теории к практической реализации моделей
В статье рассматриваются основы байесовской статистики в дискретном и непрерывном случаях. Мы пройдём путь от классической теоремы Байеса и простых примеров с подбрасыванием монеты до сопряжённых распределений и динамического байесовского обновления, позволяющего проводить анализ котировок в режиме реального времени. На примере бета-биномиальной модели реализован простой индикатор разладки (change point detection), помогающий определять смену рыночного режима.
От сигнала к сделке через цепочку агентов: LangChain-архитектура поверх MQL5 От сигнала к сделке через цепочку агентов: LangChain-архитектура поверх MQL5
Описана архитектура, в которой MQL5-советник выполняет только сбор данных и исполнение, а логика вынесена в Python-сервер с тремя агентами LangChain: сигнальным, новостным и риск-менеджером. Агенты последовательно обрабатывают запрос по WebSocket, при отказе любого возвращается hold. Решения и фактический PnL сохраняются в SQLite, формируя память и статистику. Читатель получит схему взаимодействия, протокол команд и подход к обратной связи.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Бимодальный Market Profile с дельтой и памятью в MQL5 Бимодальный Market Profile с дельтой и памятью в MQL5
Классический Market Profile сорокалетней давности до сих пор тиражируется в десятках индикаторов, которые отличаются только цветом баров. В статье я разбираю три концептуальные слепые зоны оригинальной теории — монолитную Value Area при бимодальных распределениях, слепоту TPO к агрессору и отсутствие памяти между сессиями — и строю индикатор, который закрывает каждую из них: детекция бимодальности с dead zone, ордер-флоу через CopyTicksRange с absorption detection, композитная память рынка с Naked POC и HVN/LVN. Полный исходный код прилагается.