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

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

MetaTrader 5Трейдинг |
369 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, формируя память и статистику. Читатель получит схему взаимодействия, протокол команд и подход к обратной связи.
Синхронизация графиков для удобного технического анализа Синхронизация графиков для удобного технического анализа
Синхронизация графиков для упрощения технического анализа обеспечивает единообразное отображение графических объектов, таких как линии тренда, прямоугольники или индикаторы, на всех временных интервалах для одного и того же символа. Такие действия, как прокрутка, масштабирование или смена инструмента, отражаются на всех синхронизированных графиках, что позволяет легко просматривать и сравнивать один и тот же контекст ценового движения на разных временных интервалах.
Бимодальный Market Profile с дельтой и памятью в MQL5 Бимодальный Market Profile с дельтой и памятью в MQL5
Классический Market Profile сорокалетней давности до сих пор тиражируется в десятках индикаторов, которые отличаются только цветом баров. В статье я разбираю три концептуальные слепые зоны оригинальной теории — монолитную Value Area при бимодальных распределениях, слепоту TPO к агрессору и отсутствие памяти между сессиями — и строю индикатор, который закрывает каждую из них: детекция бимодальности с dead zone, ордер-флоу через CopyTicksRange с absorption detection, композитная память рынка с Naked POC и HVN/LVN. Полный исходный код прилагается.