Алгоритм оптимизации быков — Bull Optimization Algorithm (BOA)
Содержание
Введение
Когда трейдер берётся за оптимизацию параметров торговой стратегии, он почти всегда упирается в одну и ту же стену: пространство поиска огромно, целевая функция шумна и многоэкстремальна, а времени и вычислительных ресурсов всегда меньше, чем хотелось бы. Стандартный перебор в тестере 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]:
То есть участок генома [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. После кроссовера:
Особь подтянулась к лидеру в четырёх координатах из шести.
Идея 3: мультипликативная мутация. Это самая неортодоксальная часть алгоритма. Формула мутации:
Здесь ω — параметр инерции (масштаб мутации), r_1 и r_2 — независимые случайные числа. Ключевое отличие от классической мутации: возмущение пропорционально самому значению гена, а не диапазону его допустимых значений.
Пусть ω = 0.1, x_ij = 5.0, r_1 = 0.6, r_2 = 0.3. Условие r_2 ≤ 0.5 выполнено, поэтому:
А теперь пусть тот же ген принял значение x_ij = 0.01 — близко к нулю. С теми же ω, r_1, r_2:
Мутация изменила ген на 0.06% — на масштабе, согласованном с самим геном. Это очень полезное свойство для функций, у которых оптимум находится вблизи нуля: когда координата приближается к нулю, шаг мутации тоже сжимается, и алгоритм сходится с высокой точностью без необходимости вручную уменьшать "ω".
Но у этой медали есть обратная сторона. Если оптимум находится, скажем, в точке x = 7.0, а ген случайно прошёл через ноль (например, после хаотической мутации), то возле нуля шаг мутации становится микроскопическим, и выбраться оттуда обычной мутацией почти невозможно. Это структурная ловушка алгоритма, и спасает от неё только хаотическая мутация — следующая идея.
Идея 4 (вспомогательная): хаотическая мутация. Чтобы дать алгоритму шанс выскочить из локальных минимумов и из «нулевой ловушки» мультипликативной мутации, для каждого гена с малой вероятностью ChMR (типичное значение 0.001) выполняется случайный сброс:
Это эквивалент классической ГА-мутации, но включается крайне редко — в среднем один ген на тысячу пересчётов. Достаточно, чтобы изредка перебрасывать особь в случайную точку пространства; недостаточно, чтобы разрушать прогресс.
Идея 5 (вспомогательная): мутация лучшей особи. Чтобы усилить локальный поиск вокруг лидера, BOA дополнительно мутирует саму cB по той же формуле (11). Если результат окажется лучше — cB обновится. Это даёт постоянное «дрожание» лидера в окрестности его текущей позиции, что в сочетании с мультипликативной мутацией даёт точную сходимость на финальных эпохах.
Линейный спад инерции. В статье он явно не специфицирован, но параметры wMax и wMin в неявном виде подразумевают изменение ω во времени. Принимаем линейный спад в духе классического PSO Shi-Eberhart:
В начале оптимизации ω = wMax = 0.1 — мутация ведёт себя как глобальный поиск. К концу ω = wMin = 0.0001 — мутация становится тонким локальным уточнением. Эта схема даёт классический баланс exploration/exploitation, который BOA реализует не через изменение структуры алгоритма, а через изменение единственного параметра.

Рисунок 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%)
=============================
Визуализация работы алгоритма на тестовых функциях и на функциях, входящих в поставку программы.

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

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

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

BOA на функции Ackley

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) | |||||||
| ANS | across neighbourhood search | 1,00000 | 0,88228 | 0,40138 | 2,28366 | 1,00000 | 0,95281 | 0,28092 | 2,23373 | 0,94667 | 0,85733 | 0,22389 | 2,02789 | 6,545 | 72,72 |
| AMOm | animal migration optimization M | 0,91624 | 0,83603 | 0,46790 | 2,22017 | 0,98482 | 0,92010 | 0,36391 | 2,26883 | 0,91733 | 0,81707 | 0,25177 | 1,98617 | 6,475 | 71,94 |
| CLA | code lock algorithm (joo) | 0,95139 | 0,86199 | 0,37879 | 2,19217 | 0,99349 | 0,93500 | 0,26497 | 2,19346 | 0,93600 | 0,84267 | 0,24060 | 2,01927 | 6,405 | 71,17 |
| (P+O)ES | (P+O) evolution strategies | 0,86571 | 0,89539 | 0,39740 | 2,15850 | 0,97761 | 0,89820 | 0,26878 | 2,14459 | 0,92133 | 0,80240 | 0,23952 | 1,96325 | 6,266 | 69,62 |
| SDSm | stochastic diffusion search M | 0,95195 | 0,84944 | 0,36249 | 2,16388 | 0,98061 | 0,88457 | 0,22112 | 2,08630 | 0,92267 | 0,79013 | 0,21380 | 1,92660 | 6,177 | 68,63 |
| AAm | archery algorithm M | 0,84685 | 0,73320 | 0,42590 | 2,00595 | 0,96709 | 0,77837 | 0,27789 | 2,02335 | 0,86133 | 0,77707 | 0,28712 | 1,92552 | 5,955 | 66,17 |
| SIA | simulated isotropic annealing (joo) | 0,93543 | 0,86504 | 0,38483 | 2,18530 | 0,94069 | 0,80609 | 0,23835 | 1,98513 | 0,86400 | 0,66160 | 0,19536 | 1,72096 | 5,891 | 65,46 |
| TETA | time evolution travel algorithm (joo) | 0,91452 | 0,86369 | 0,25579 | 2,03400 | 0,99654 | 0,91291 | 0,14394 | 2,05339 | 0,85467 | 0,82213 | 0,10443 | 1,78123 | 5,869 | 65,21 |
| ESG | evolution of social groups (joo) | 0,98111 | 0,79857 | 0,31167 | 2,09135 | 0,98954 | 0,82270 | 0,15032 | 1,96256 | 0,92133 | 0,73440 | 0,15315 | 1,80888 | 5,863 | 65,14 |
| CTA | comet tail algorithm (joo) | 0,92435 | 0,86786 | 0,27838 | 2,07059 | 0,99039 | 0,84571 | 0,19448 | 2,03058 | 0,95467 | 0,69680 | 0,11008 | 1,76155 | 5,863 | 65,14 |
| ECBO | enhanced colliding bodies optimization | 0,94024 | 0,72363 | 0,32356 | 1,98743 | 0,99477 | 0,80291 | 0,13056 | 1,92824 | 0,87600 | 0,70160 | 0,17433 | 1,75193 | 5,668 | 62,98 |
| DA | dialectical algorithm | 0,93117 | 0,75400 | 0,26205 | 1,94722 | 0,98925 | 0,81375 | 0,08662 | 1,88962 | 0,92667 | 0,68107 | 0,11315 | 1,72089 | 5,558 | 61,76 |
| BBO | biogeography based optimization | 0,95876 | 0,70609 | 0,35752 | 2,02237 | 0,92981 | 0,70660 | 0,16970 | 1,80611 | 0,87467 | 0,63013 | 0,20813 | 1,71293 | 5,541 | 61,57 |
| BHAm | black hole algorithm M | 0,79558 | 0,76207 | 0,34682 | 1,90447 | 0,99836 | 0,75798 | 0,13826 | 1,89460 | 0,85067 | 0,64427 | 0,17020 | 1,66514 | 5,464 | 60,71 |
| HS | harmony search | 0,91420 | 0,69049 | 0,29924 | 1,90393 | 0,97627 | 0,73373 | 0,14193 | 1,85193 | 0,91733 | 0,62720 | 0,15364 | 1,69817 | 5,454 | 60,60 |
| RFO | royal flush optimization (joo) | 0,80989 | 0,74481 | 0,34546 | 1,90016 | 0,95251 | 0,77926 | 0,15185 | 1,88362 | 0,80400 | 0,66427 | 0,19071 | 1,65898 | 5,443 | 60,48 |
| BOAm | billiards optimization algorithm M | 0,76177 | 0,72421 | 0,25275 | 1,73873 | 0,90890 | 0,81960 | 0,28853 | 2,01703 | 0,83733 | 0,74613 | 0,09763 | 1,68109 | 5,437 | 60,41 |
| ASO | anarchy society optimization | 0,73070 | 0,73713 | 0,31195 | 1,77978 | 0,99732 | 0,87700 | 0,17619 | 2,05051 | 0,72000 | 0,68773 | 0,18988 | 1,59761 | 5,428 | 60,31 |
| EOm | extremal optimization_M | 0,76527 | 0,75205 | 0,31908 | 1,83640 | 0,99999 | 0,76426 | 0,12437 | 1,88862 | 0,84133 | 0,64133 | 0,15247 | 1,63513 | 5,360 | 59,56 |
| ACS | artificial cooperative search | 0,75545 | 0,77162 | 0,31653 | 1,84360 | 1,00000 | 0,80488 | 0,10705 | 1,91193 | 0,76933 | 0,60800 | 0,14157 | 1,51890 | 5,274 | 58,60 |
| SSG | saplings sowing and growing | 0,75436 | 0,63206 | 0,35935 | 1,74577 | 0,91907 | 0,69694 | 0,19755 | 1,81356 | 0,81867 | 0,60533 | 0,21347 | 1,63747 | 5,197 | 57,74 |
| AOSm | atomic orbital search M | 0,76184 | 0,68435 | 0,31344 | 1,75963 | 0,90015 | 0,80044 | 0,11501 | 1,81560 | 0,82800 | 0,63280 | 0,15696 | 1,61776 | 5,193 | 57,70 |
| TSEA | turtle shell evolution algorithm (joo) | 0,95809 | 0,64852 | 0,29571 | 1,90232 | 0,99522 | 0,58104 | 0,10542 | 1,68168 | 0,92133 | 0,52160 | 0,14567 | 1,58860 | 5,173 | 57,48 |
| DE | differential evolution | 0,96398 | 0,62346 | 0,26089 | 1,84833 | 0,98482 | 0,77018 | 0,11459 | 1,86959 | 0,93067 | 0,36213 | 0,11000 | 1,40280 | 5,121 | 56,90 |
| BIO | blood inheritance optimization (joo) | 0,72580 | 0,66522 | 0,31228 | 1,70330 | 0,99995 | 0,68125 | 0,11540 | 1,79660 | 0,85467 | 0,59333 | 0,15364 | 1,60164 | 5,102 | 56,69 |
| (PO)ES | (PO) evolution strategies | 0,73972 | 0,58190 | 0,38896 | 1,71058 | 0,91199 | 0,59975 | 0,21262 | 1,72436 | 0,82400 | 0,56240 | 0,23432 | 1,62072 | 5,056 | 56,18 |
| BO | bonobo optimizer | 0,75555 | 0,64366 | 0,32657 | 1,72578 | 0,94332 | 0,70442 | 0,13999 | 1,78773 | 0,73467 | 0,61440 | 0,16728 | 1,51635 | 5,030 | 55,89 |
| SRA | successful restaurateur algorithm (joo) | 0,89010 | 0,63359 | 0,29115 | 1,81484 | 0,96634 | 0,55285 | 0,08914 | 1,60833 | 0,89333 | 0,52800 | 0,13911 | 1,56044 | 4,984 | 55,38 |
| CRO | chemical reaction optimisation | 0,91281 | 0,65681 | 0,29866 | 1,86828 | 0,90513 | 0,56020 | 0,10939 | 1,57472 | 0,82800 | 0,50133 | 0,14149 | 1,47082 | 4,914 | 54,60 |
| BCOm | bacterial chemotaxis optimization M | 0,82589 | 0,61733 | 0,31584 | 1,75906 | 0,95296 | 0,63718 | 0,11984 | 1,70998 | 0,76533 | 0,51653 | 0,15800 | 1,43986 | 4,909 | 54,54 |
| DOA | dream optimization algorithm | 0,78522 | 0,78121 | 0,36036 | 1,92679 | 0,61584 | 0,42117 | 0,12254 | 1,15955 | 0,86667 | 0,72587 | 0,21127 | 1,80381 | 4,890 | 54,33 |
| ABO | african buffalo optimization | 0,92295 | 0,62528 | 0,29885 | 1,84708 | 0,92992 | 0,57468 | 0,09372 | 1,59832 | 0,73333 | 0,51333 | 0,14324 | 1,38990 | 4,835 | 53,72 |
| BSA | bird swarm algorithm | 0,94432 | 0,67941 | 0,26401 | 1,88774 | 0,91649 | 0,65619 | 0,12054 | 1,69322 | 0,80933 | 0,33547 | 0,10652 | 1,25132 | 4,832 | 53,69 |
| TSm | tabu search M | 0,87806 | 0,61040 | 0,28993 | 1,77839 | 0,98116 | 0,52165 | 0,08544 | 1,58825 | 0,82667 | 0,49547 | 0,13552 | 1,45766 | 4,824 | 53,60 |
| BSA | backtracking search algorithm | 0,87128 | 0,53190 | 0,28675 | 1,68993 | 0,92408 | 0,51602 | 0,09153 | 1,53163 | 0,96000 | 0,47253 | 0,13760 | 1,57013 | 4,792 | 53,24 |
| WOAm | whale optimization algorithm M | 0,93893 | 0,59477 | 0,26695 | 1,80065 | 0,98036 | 0,53873 | 0,07112 | 1,59021 | 0,78667 | 0,47600 | 0,11892 | 1,38159 | 4,772 | 53,02 |
| CSO | competitive swarm optimizer | 0,85151 | 0,60786 | 0,29896 | 1,75833 | 0,84085 | 0,58491 | 0,11974 | 1,54550 | 0,80000 | 0,48560 | 0,14184 | 1,42744 | 4,731 | 52,57 |
| FBA | fractal-based algorithm | 0,69419 | 0,64267 | 0,28955 | 1,62641 | 0,99812 | 0,54905 | 0,08705 | 1,63422 | 0,76133 | 0,51253 | 0,13689 | 1,41075 | 4,671 | 51,90 |
| ECOi | eco-inspired evolutionary algorithm | 0,78817 | 0,54402 | 0,29360 | 1,62579 | 0,88996 | 0,46592 | 0,09747 | 1,45335 | 0,78533 | 0,45173 | 0,14295 | 1,38001 | 4,459 | 49,54 |
| BSO | brain storm optimization | 0,92207 | 0,57625 | 0,29732 | 1,79564 | 0,80764 | 0,42508 | 0,09448 | 1,32720 | 0,77200 | 0,36533 | 0,13065 | 1,26798 | 4,391 | 48,79 |
| CAm | camel algorithm M | 0,71534 | 0,56917 | 0,35985 | 1,64436 | 0,84094 | 0,47174 | 0,10850 | 1,42118 | 0,70400 | 0,41947 | 0,19563 | 1,31910 | 4,385 | 48,72 |
| ACOm | ant colony optimization_M | 0,71885 | 0,48410 | 0,30990 | 1,51285 | 0,75792 | 0,48639 | 0,11871 | 1,36302 | 0,83600 | 0,48667 | 0,16148 | 1,48415 | 4,360 | 48,44 |
| BSO | brain storm optimization | 0,91331 | 0,55813 | 0,29705 | 1,76849 | 0,85329 | 0,44038 | 0,09447 | 1,38814 | 0,72267 | 0,32880 | 0,13325 | 1,18472 | 4,341 | 48,23 |
| AEFA | artificial electric field algorithm | 0,88798 | 0,65769 | 0,25276 | 1,79843 | 0,95550 | 0,63602 | 0,03946 | 1,63098 | 0,60800 | 0,16000 | 0,09845 | 0,86645 | 4,296 | 47,73 |
| SOA | simple optimization algorithm | 0,91664 | 0,47040 | 0,27095 | 1,65799 | 0,88264 | 0,31546 | 0,06044 | 1,25854 | 0,82933 | 0,31733 | 0,11579 | 1,26245 | 4,179 | 46,43 |
| BOA_Bull | bull_optimization_algorithm | 0,71281 | 0,51900 | 0,28947 | 1,52128 | 0,96961 | 0,38037 | 0,06579 | 1,41577 | 0,72533 | 0,37600 | 0,13669 | 1,23802 | 4,175 | 46,39 |
| RW | random walk | 0,49970 | 0,32333 | 0,25791 | 1,08094 | 0,30754 | 0,11470 | 0,04400 | 0,46624 | 0,36133 | 0,17013 | 0,10244 | 0,63390 | 2,181 | 24,23 |
Выводы
Огуз Финдик в 2015 году сформулировал три критики классического генетического алгоритма (вырождающаяся при сходимости селекция, разрушительная мутация на удалении от диапазона, пассивная роль элитной особи) и предложил три точечных ответа: убрать селекцию совсем, сделать лучшую особь обязательным партнёром по скрещиванию для всех остальных, заменить аддитивную мутацию мультипликативной.
В начале статьи мы поставили проблему трейдера, упирающегося в стену при оптимизации параметров стратегии: пространство огромно, целевая функция многоэкстремальна. После прохождения через этот материал у трейдера появляется три инструмента.
Первый — реализация BOA в формате C_AO, готовая к подключению наряду с любым другим алгоритмом фреймворка. Для задач оптимизации с 5–25 параметрами (а именно столько обычно имеет средняя торговая стратегия — периоды индикаторов, уровни SL/TP, фильтры по волатильности, веса в композитных сигналах) BOA даёт результат, сопоставимый с алгоритмами рейтинга. Это не лучший инструмент в ящике, но это рабочий инструмент с осмысленной механикой, и в портфеле оптимизаторов ему есть место — особенно когда нужна быстрая сходимость и точное уточнение около найденного экстремума.
Второй — понимание, какие задачи BOA не решает. Если оптимизация портфеля включает 100+ параметров (например, веса в большой системе), BOA — неподходящий выбор: его мультипликативная мутация структурно не масштабируется на такие размерности. Здесь стоит брать алгоритмы из верхней части рейтинга, специально показавшие устойчивость на больших размерностях.
Третий — понимание самой идеи мультипликативного шага. Это инженерная находка: шаг возмущения, автоматически согласованный с масштабом самой переменной. Идея не привязана к BOA и может быть перенесена в другие алгоритмы — в гибридах с PSO, DE, в кастомных решениях для конкретных торговых задач. Семь параметров настройки BOA (CR, MR, MSR, ChMR, wMax, wMin, popSize) дают широкое поле для экспериментов.
Алгоритм оставлен в открытом доступе, и я приглашаю желающих попробовать поиграть параметрами. В рейтинговой таблице на сегодня BOA занимает позицию «не вошёл, но был близок». Это честный результат. Но это не означает, что работа неудачна: три идеи, сформулированные в статье автора, заслуживают изучения сами по себе, а реализация показывает, как именно они работают вместе.

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

Рисунок 3. Гистограмма результатов тестирования алгоритмов (по шкале от 0 до 100, чем больше, тем лучше, где 100 — максимально возможный теоретический результат), в архиве скрипт для расчёта рейтинговой таблицы
Плюсы и минусы алгоритма
Плюсы:
- Решает задачи до средней размерности.
Минусы:
- Большое количество внешних параметров.
- Склонность к застреванию на некоторых типах задач.
К статье прикреплён архив с актуальными версиями кодов алгоритмов. Автор статьи не несёт ответственности за абсолютную точность в описании канонических алгоритмов, во многие из них были внесены изменения для улучшения поисковых возможностей. Выводы и суждения, представленные в статьях, основываются на результатах проведённых экспериментов.
Программы, используемые в статье
| # | Имя | Тип | Описание |
|---|---|---|---|
| 1 | #C_AO.mqh | Включаемый файл | Родительский класс популяционных алгоритмов оптимизации |
| 2 | #C_AO_enum.mqh | Включаемый файл | Перечисление популяционных алгоритмов оптимизации |
| 3 | TestFunctions.mqh | Включаемый файл | Библиотека тестовых функций |
| 4 | TestStandFunctions.mqh | Включаемый файл | Библиотека функций тестового стенда |
| 5 | TestStand3D.mqh | Включаемый файл | 3D-панель визуализации для тестового стенда |
| 6 | Utilities.mqh | Включаемый файл | Библиотека вспомогательных функций |
| 7 | CalculationTestResults.mqh | Включаемый файл | Скрипт для расчета результатов в сравнительную таблицу |
| 8 | Test_AO_All.mq5 | Скрипт | Единый испытательный стенд для всех популяционных алгоритмов оптимизации |
| 9 | Test_AO_AntiCheat | Скрипт | Тест на читерство алгоритмов оптимизации |
| 10 | Simple use of population optimization algorithms.mq5 | Скрипт | Простой пример использования популяционных алгоритмов оптимизации без визуализации |
| 11 | Test_AO_BOA.mq5 | Скрипт | Испытательный стенд для BOA |
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Основы байесовского вывода в дискретном и непрерывном случаях: от теории к практической реализации моделей
От сигнала к сделке через цепочку агентов: LangChain-архитектура поверх MQL5
Бимодальный Market Profile с дельтой и памятью в MQL5
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования