Алгоритм направления потока — Flow Direction Algorithm (FDA)
Содержание
Введение
Зачем трейдеру разбираться в алгоритмах оптимизации? Затем, что оптимизация — это сердце разработки любой торговой системы. Чем эффективнее алгоритм распоряжается ограниченным бюджетом прогонов, тем больше пространства параметров он успевает осмысленно исследовать за то же время. Тогда выше шанс найти действительно устойчивую область, а не случайный выброс. Именно поэтому в данной серии статей мы последовательно разбираем, реализуем и честно замеряем популяционные алгоритмы оптимизации. Каждый из них после разбора становится готовым инструментом, совместимым с единым фреймворком, а его место в общей рейтинговой таблице позволяет осознанно выбирать метод под задачу. В данной статье разберём нового кандидата для рейтинга.
Flow Direction Algorithm (FDA) был предложен в 2021 году Hojat Karami с соавторами. Метафора алгоритма — гидрологическая. Представьте водосборный бассейн после дождя: вода не знает, где находится точка водосброса, но каждая капля стекает туда, где ниже. Поток ощупывает окрестность, находит соседнюю точку с меньшей высотой и смещается в её сторону со скоростью, пропорциональной уклону — чем круче склон, тем быстрее течение. Если вокруг ровно, поток вливается в другой, более удачливый. В терминах оптимизации: агенты-потоки порождают пробные точки-соседей. Лучший сосед задаёт направление и уклон, а при отсутствии уклона агент дрейфует к более успешному собрату или к глобальному лидеру.
Идея выглядит стройно и результаты в публикациях — впечатляюще: в таблицах сравнения FDA и его модификаций показывают точные нули на целых семействах тестовых функций, обходя десятки конкурентов. Именно это и стало поводом присмотреться к алгоритму внимательнее. Забегая вперёд: значительная часть впечатляющих результатов обусловлена тремя артефактами в формулах — двумя центровыми и одним диагональным, а не силой поискового механизма. После их удаления алгоритм показывает на нашем стенде скромные 39–40%. Зато диагностика вскрыла, где именно механизм слаб, и точечные модификации подняли результат до 54.7% — об этом во второй половине статьи.
Реализация алгоритма
Как устроен канонический FDA? Популяция состоит из alpha потоков. Инициализация стандартна — равномерно по области поиска:
Flow_X(i) = lb + rand * (ub - lb) (1)
Каждую итерацию вокруг каждого потока порождается beta соседей:
Neighbor_X(j) = Flow_X(i) + randn * Delta (2) Delta = (rand * Xrand - rand * Flow_X(i)) * ||Best_X - Flow_X(i)|| * W (3)
где Xrand — случайная точка области поиска, Best_X — глобальный лидер, а W — затухающий по итерациям весовой множитель:
W = (1 - iter/MaxIter)^(2*randn) * (rand * iter/MaxIter) * rand (5)
Формулу (4) первоисточника пропускаем сознательно: она описывает объём стока через осадки и время и породить точку в пространстве поиска размерно не может — в реализации Xrand берётся равномерной случайной точкой области.
Обратите внимание на структуру (3): размер окрестности пропорционален расстоянию потока до лидера. Далёкие потоки ощупывают пространство широко, близкие — всё мельче. Это осмысленная и, пожалуй, лучшая идея алгоритма.
Дальше — развилка. Если лучший из соседей оказался выгоднее самого потока, поток стекает вдоль оси поток—сосед:
Flow_newX(i) = Flow_X(i) + V * (Flow_X(i) - Neighbor_X(j)) / ||Flow_X(i) - Neighbor_X(j)|| (6) V = randn * S0 (7) S0 = (Flow_fitness(i) - Neighbor_fitness(j)) / |dx| (8)
S0 — тот самый уклон: разность высот, делённая на расстояние. Если же сосед не лучше, срабатывает ветка (9): выбирается случайный поток r, и если он выгоднее — текущий поток дрейфует к нему; иначе — к глобальному лидеру.
Стоимость одной итерации — alpha * (beta + 1) вычислений целевой функции: beta соседей на поток плюс один ход. Важная деталь, которую легко упустить при сравнении с другими алгоритмами: при beta = 3 алгоритм расходует за итерацию вчетверо больше бюджета, чем обычный популяционный метод. Авторский анализ чувствительности даёт beta = 1 в трёх инженерных задачах из четырёх — и наши замеры это независимо подтвердят.
Первый артефакт. Формула (9):
if Flow_fitness(r) < Flow_fitness(i): Flow_newX(i) = s_L * Best_X else: Flow_newX(i) = Flow_X(i) + s_L * (Best_X - Flow_X(i))
Присмотритесь к строке Flow_newX(i) = s_L * Best_X: новая позиция — это вектор абсолютной позиции лидера, умноженный на случайное число. Если лидер находится в точке (3, -5), а s_L = 0.1, поток телепортируется в (0.3, -0.5). Каждое срабатывание геометрически стягивает координаты к нулю, и при малых s_L они доходят до машинного нуля в типе double. Теперь взгляните на тестовые наборы: функции Sphere, Griewank, и им подобные имеют оптимум ровно в начале координат. Алгоритм получает точный ноль — идеальный результат — вообще без оптимизации, простым умножением.
Второй артефакт. В формуле (3) стоит выражение:
rand1 * Xrand - rand2 * Flow_X(i)
Два независимых случайных числа умножаются на две абсолютные позиции. Раскроем скобки иначе:
rand1 * (Xrand - Flow_X) + (rand1 - rand2) * Flow_X
Первое слагаемое — честная случайная разность, она не зависит от того, где размещена система координат. А второе — паразитное: вектор абсолютной позиции потока, умноженный на случайный множитель с нулевым средним. Среднее у него нулевое, но разброс пропорционален квадрату расстояния потока от нуля координат. Поток, оказавшийся рядом с началом координат, делает микроскопические шаги — то есть получает тонкую доводку бесплатно. Поток на периферии делает гигантские прыжки. Возникает воронка сходимости, закреплённая ровно в точке (0, 0, ..., 0) — независимо от того, где на самом деле находится оптимум функции.
На стандартных тестовых наборах с симметричным диапазоном и оптимумом в нуле воронка целится точно в ответ. Это отдельный класс артефакта, центровой, и его диагностика — сдвиг задачи на постоянный вектор. Диапазон поиска и аргумент функции смещаются вместе, ландшафт остаётся тем же, а нуль координат уезжает из области. Честный алгоритм обязан выдать тот же результат.
Третий артефакт — самый скрытный; чтобы его увидеть, нужна выкладка, которой нет ни в одной из статей. Раскроем связку (6)–(8) покоординатно. Уклон по координате d:
S0_d = dF / |dx_d|
Скорость: V_d = randn * S0_d. Смещение по (6): шаг вдоль нормированной оси, то есть множитель dx_d / L. Перемножаем:
step_d = randn * (dF / |dx_d|) * (dx_d / L) = randn * dF * sign(dx_d) / L
Модуль |dx_d| сократился. Если randn — одно число на весь вектор (а формула (7) записана именно как скаляр), то модуль шага строго одинаков по всем координатам, различаются только знаки. Агент перемещается точно по диагонали гиперкуба — со знаковым узором, взятым из разности поток—сосед. Читатели предыдущих статей серии узнают картину: это тот самый диагональный чит, дающий фиктивное преимущество на тестовых функциях, у которых оптимум лежит на диагонали x1 = x2 = ... = xn, — только здесь он не виден невооружённым глазом, потому что размазан по трём формулам.
Попутно заметим арифметическое следствие того же сокращения: шаг обратно пропорционален L — расстоянию до соседа. Чем ближе сосед, тем дальше прыжок; при L, стремящемся к нулю шаг уходит в бесконечность. Механизм, который по метафоре должен обеспечивать плавное течение по склону, на деле не способен на маленький аккуратный шаг в принципе. Запомним это — при анализе результатов эта деталь окажется главной.
Наша реализация в рамках фреймворка C_AO следует принципу: механика первоисточника сохраняется, артефакты удаляются, каждое решение документируется. Перечислим ключевые.
Артефакты. Ветка (9) реализована в разностной форме — оба перемещения строятся только на разностях позиций, умножения на абсолютную позицию нет. Формула (3) взята в виде rand * (Xrand - Flow_X) — одно случайное число на разность, что делает шаг независимым от размещения системы координат. Все случайные множители — randn в (2) и (7), rand в (3) и (5) — покоординатно независимы, что снимает диагональ.
Нормировки. Величина Delta в (3) имеет размерность длины в квадрате, а S0 в (8) втягивает абсолютный масштаб целевой функции прямо в длину шага: умножьте фитнес на тысячу — и шаги вырастут в тысячу раз. Поэтому вся геометрия хода считается в нормированных координатах (смещения делятся на размер диапазона), а разность фитнеса — на размах фитнеса стартовой популяции. Сингулярность 1/L обуздана срезкой: нормированный шаг не длиннее полного диапазона координаты.
Бюджет. Тестовый стенд выделяет ровно popSize вычислений на эпоху, а итерация FDA стоит popSize * (beta + 1). Поэтом итерация растянута на beta + 1 эпох конечным автоматом: сначала эпохи соседей, затем эпоха хода. Каждая эпоха заполняет все слоты — бюджет расходуется без потерь. При popSize = 50 и beta = 1 стенд даёт 200 эпох, то есть примерно 99 полных итераций алгоритма, а при popSize = 25 и beta = 1 стенд даёт 400 эпох, то есть примерно 199 полных итераций алгоритма.
Здесь приведём сердце алгоритма, код класса C_AO_FDA с развёрнутыми комментариями — фазу хода:
//+------------------------------------------------------------------+ //| MakeMove — новая позиция потока: формулы (6)(7)(8) либо (9) | //+------------------------------------------------------------------+ void C_AO_FDA::MakeMove(int i) { double rSize, dXn, x; //--- лучший сосед выгоднее потока -> сток по уклону, (6)(7)(8) if(flow [i].bnF > flow [i].f) { //--- нормированная длина оси поток-сосед (первый проход, без буфера) double Ln = 0.0; for(int c = 0; c < coords; c++) { rSize = rangeMax [c] - rangeMin [c]; if(rSize <= 0.0) continue; dXn = (flow [i].c [c] - flow [i].bnC [c]) / rSize; Ln += dXn * dXn; } Ln = MathSqrt(Ln); if(Ln > 1.0e-10) { //--- безразмерная разность фитнеса: нормировка на замороженный // размах стартовой популяции (см. шапку, п.2) double dFn = (flow [i].bnF - flow [i].f) / fSpread; if(dFn > 1.0) dFn = 1.0; // сосед может быть лучше всей популяции //--- второй проход: уклон S0, скорость V, смещение for(int c = 0; c < coords; c++) { rSize = rangeMax [c] - rangeMin [c]; dXn = (flow [i].c [c] - flow [i].bnC [c]) / rSize; double S0 = dFn / MathMax(MathAbs(dXn), 1.0e-10); // (8) double V = RandN() * S0; // (7), randn покоординатно double stepN = V * (dXn / Ln); // (6) //--- срезка сингулярности 1/L: шаг не длиннее полного // диапазона координаты (см. шапку, п.3) if(stepN > 1.0) stepN = 1.0; if(stepN < -1.0) stepN = -1.0; x = flow [i].c [c] + stepN * rSize; a [i].c [c] = u.SeInDiSp(x, rangeMin [c], rangeMax [c], rangeStep [c]); } return; } //--- сосед вырожден (совпал с потоком) -> проваливаемся в (9) } //--- (9): сток к случайному более выгодному потоку либо к глобальному // лучшему. Обе ветки в разностной форме; // коэффициент 2*rand — реконструкция. int r = u.RNDminusOne(popSize); if(flow [r].f > flow [i].f) { for(int c = 0; c < coords; c++) { x = flow [i].c [c] + RandN() * (flow [r].c [c] - flow [i].c [c]); a [i].c [c] = u.SeInDiSp(x, rangeMin [c], rangeMax [c], rangeStep [c]); } } else { for(int c = 0; c < coords; c++) { x = flow [i].c [c] + 2.0 * u.RNDprobab() * (cB [c] - flow [i].c [c]); a [i].c [c] = u.SeInDiSp(x, rangeMin [c], rangeMax [c], rangeStep [c]); } } } //+------------------------------------------------------------------+
Схема канонического FDA.

Рисунок 1. Механика канонического FDA
Слева: масштаб окрестности зондов задаётся расстоянием до лидера (3); при удачном зонде поток делает шаг случайного знака вдоль оси поток—зонд (6), при неудачном — дрейфует к более успешному потоку или лидеру (9).
Справа: длина шага стока обратно пропорциональна расстоянию до зонда — чем ближе зонд, тем дальше прыжок. Малые шаги возможны только при далёких зондах, поэтому механизм тонкой доводки в алгоритме отсутствует.
Результаты канонической версии: Основной стенд серии: Hilly, Forest, Megacity в размерностях 10, 50 и 1000 координат, бюджет 10000 вычислений, 10 повторов.
FDA|Flow Direction Algorithm|25.0|1.0|
=============================
5 Hilly's; Func runs: 10000; result: 0.6383459067160635
25 Hilly's; Func runs: 10000; result: 0.44799903887241055
500 Hilly's; Func runs: 10000; result: 0.2766763300122244
=============================
5 Forest's; Func runs: 10000; result: 0.6820774094416302
25 Forest's; Func runs: 10000; result: 0.3932956443058898
500 Forest's; Func runs: 10000; result: 0.08511233872912391
=============================
5 Megacity's; Func runs: 10000; result: 0.5933333333333333
25 Megacity's; Func runs: 10000; result: 0.3376
500 Megacity's; Func runs: 10000; result: 0.11888000000000105
=============================
All score: 3.57332 (39.70%)
39.70% — и это лучшая из проверенных конфигураций. Изменения по размеру популяции дали 37.9–39.7% на popSize от 15 до 50. Алгоритм не реагирует на трёхкратный размен: число итераций против разнообразия, что само по себе диагноз. Увеличение числа соседей до пяти обвалило результат до 27.02% — пять зондов на итерацию означают втрое меньше итераций, а информации от дополнительных зондов не прибавляется. Это независимо подтверждает авторский выбор: beta = 1. Направление от одного зонда не хуже направления от пяти.
Самое поучительное — серия прогонов по структурным осям. Мы включали и выключали центровой артефакт из (3), принятие лучшего соседа в популяцию, замену плавающей нормировки фитнеса на замороженную — пять прогонов легли в диапазон 38.4–40.1%, целиком внутри шума стенда. Вывод двоякий. Во-первых, центровой артефакт на нашем наборе инертен: тестовые функции стенда не центрованы, их оптимумы не сидят в нуле координат, и воронке не за что зацепиться — количественное подтверждение того, что набор изначально спроектирован устойчивым к этому классу читов. Во-вторых, раз никакая из осей ничего не меняет, потолок принадлежит самому механизму.
И механизм этот мы уже видели: шаг стока пропорционален 1/L и не способен быть маленьким. Тонкой доводки в FDA нет конструктивно — срезка шага срабатывает постоянно, и фактическую работу выполняет ветка (9), простое притяжение к лучшему. Тридцать девять процентов — цена такого устройства.
Диагноз сформулирован, теперь лечение. Четыре точечных изменения, каждое бьёт в свой пункт диагноза, и — в традициях серии — каждое проверено отдельным замером, включая те, что не сработали. Итак, модификации:
1. Ход. Сингулярное ядро (6)–(8) удалено; перемещение — всегда ветка (9). Звучит радикально, но замеры показали, что работу и так выполняла она, а ядро лишь генерировало срезанные прыжки. Идея стока к более успешному сохранена — убрана только арифметика, которая не могла работать.
2. Зонды. Вместо Delta с расписанием W (напомним: множитель iter/MaxIter глушит разведку в первых эпохах наглухо) — степенное распределение PowerDistribution вокруг потока с адаптивным радиусом max(distN, radMin), где distN — нормированное расстояние до лидера. Родная идея FDA: далёкий поток ищет широко, близкий — мелко сохранена, но radMin гарантирует, что радиус никогда не схлопывается в ноль. Побочный бонус: в канонике поток, совпавший с лидером, замирает навсегда (расстояние ноль — окрестность нулевая — все смещения нулевые). С radMin он превращается в локальный дожим вокруг лучшей найденной точки.
3. Зонды не выбрасываются. В канонике сосед — только направление: даже зонд, оказавшийся лучше всей популяции, отбрасывается (глобальный лидер его запоминает, но популяция — нет). При beta = 1 это половина бюджета в мусор. В FDAm зонд, превзошедший свой поток, занимает его место — оценка уже оплачена.
4. Частичное перемещение. В ветке (9) каждая координата двигается с вероятностью moveProb, остальные остаются на месте. Об этом изменении стоит рассказать честно, потому что оно сработало не так, как задумывалось. Мотив был: полномерный шаг дрейфа в 1000-мерном пространстве почти наверняка портит больше координат, чем улучшает, и жадная приёмка его отбрасывает — гейт должен был оживить высокие размерности. Изменения по moveProb дали чёткий пик на 0.5 и прибавку в три пункта — но пришла она в малые и средние размерности, а на больших размерностях не сдвинулись. Механизм выигрыша оказался иным: сохранение хороших координат при дрейфе, по сути аналог равномерного кроссовера. При тысяче координат и moveProb = 0.5 шаг всё ещё задевает пятьсот из них — для высоких размерностей этого слишком много, а moveProb = 0.05, который мог бы помочь там, обваливает малые размерности до 46.5%. Прогноз не сбылся, прибавка осталась.
Отдельно о параметре radMin. Лучшая конфигурация нашлась при radMin = 0.7 и distrPower = 50 — и это заслуживает расшифровки. Радиус в 70–100% диапазона при жёсткой степенной концентрации означает: подавляющее большинство зондов ложится вплотную к потоку, но редкие улетают через полпространства. Мы получили тяжелохвостое поисковое ядро. Любопытно, что при таком поле адаптивная связь радиуса с расстоянием до лидера почти отключается. Контрольный прогон с фиксированным радиусом (radMin = 1.0) дал 50.78% против 51.83% — вклад адаптации существует, но не превышает пункта.

Рисунок 2. Механика FDAm
Слева: тяжелохвостое облако зондов PowerDistribution — основная масса ложится вплотную к потоку (доводка), редкие улетают через полпространства (разведка); зонд, превзошедший поток, занимает его место. Справа вверху: в фазе дрейфа каждая координата перемещается с вероятностью moveProb = 0.5, остальные сохраняются — хорошие координаты не разрушаются полномерным шагом. Справа внизу: масштаб зондов каноники (множитель W) мёртв в начале прогона и затухает к концу. В FDAm радиус никогда не опускается ниже пола radMin.
Структура S_FDAm_Flow — внутреннее состояние одного потока. Хранит текущую позицию c[] и её фитнес f, а также позицию bnC[] и фитнес bnF лучшего зонда, накопленного за фазу разведки текущей итерации. Метод Init() выделяет массивы координат и сбрасывает оба фитнеса в −DBL_MAX, то есть в состояние ещё ничего не найдено. Отделена от стендового массива агентов a[] намеренно: слоты a[] каждый раз занимают свежие кандидаты на оценку, а потоки живут между эпохами здесь.
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ struct S_FDAm_Flow { double c []; // позиция потока (валидная, прогнана через SeInDiSp) double bnC []; // позиция лучшего соседа текущей итерации double f; // фитнес потока double bnF; // фитнес лучшего соседа текущей итерации void Init(int coords) { ArrayResize(c, coords); ArrayResize(bnC, coords); f = -DBL_MAX; bnF = -DBL_MAX; } };
Класс C_AO_FDAm — реализация модифицированного алгоритма направления потока в рамках фреймворка C_AO. Наследует стандартный интерфейс базового класса (Init / Moving / Revision), популяцию агентов и глобальное лучшее решение. Внутри держит массив потоков flow[], счётчик фаз phase конечного автомата (эпохи зондов → эпоха хода), размах фитнеса стартовой популяции fSpread (нужен только каноническому ядру при useSlope = 1) и кэш второго значения Бокса—Мюллера.
Конструктор C_AO_FDAm() — задаёт имя, описание и ссылку алгоритма, значения параметров по умолчанию и регистрирует все шесть параметров в массиве params[] по конвенции серии: params[0] — всегда размер популяции.
SetParams() — переносит значения из params[] во внутренние поля и приводит их к допустимым диапазонам: популяция не меньше двух, число соседей 1–10, радиус и вероятность перемещения зажаты в (0, 1], степень концентрации не ниже единицы, флаг useSlope нормируется к 0/1. Гарантирует работоспособность при любых значениях, заданных пользователем в тестовом скрипте.
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class C_AO_FDAm : public C_AO { public: ~C_AO_FDAm() {} C_AO_FDAm() { ao_name = "FDAm"; ao_desc = "Flow Direction Algorithm M"; ao_link = "https://www.mql5.com/ru/articles/23731"; popSize = 25; // размер популяции (== число потоков) nNeighbors = 1; // соседей на поток radMin = 0.7; // пол радиуса зонда, доля диапазона distrPower = 50.0; // концентрация зонда у потока (PowerDistribution) useSlope = 0; // 0 -> ход только веткой (9); 1 -> каноническое ядро (6)(7)(8) moveProb = 0.5; // вероятность перемещения координаты в ветке (9) ArrayResize(params, 6); params [0].name = "popSize"; params [0].val = popSize; params [1].name = "nNeighbors"; params [1].val = nNeighbors; params [2].name = "radMin"; params [2].val = radMin; params [3].name = "distrPower"; params [3].val = distrPower; params [4].name = "useSlope"; params [4].val = useSlope; params [5].name = "moveProb"; params [5].val = moveProb; } void SetParams() { popSize = (int)params [0].val; nNeighbors = (int)params [1].val; radMin = params [2].val; distrPower = params [3].val; useSlope = (int)params [4].val; moveProb = params [5].val; //--- предохранители if(popSize < 2) popSize = 2; if(nNeighbors < 1) nNeighbors = 1; if(nNeighbors > 10) nNeighbors = 10; if(radMin < 0.0001) radMin = 0.0001; if(radMin > 1.0) radMin = 1.0; if(distrPower < 1.0) distrPower = 1.0; if(useSlope != 1) useSlope = 0; if(moveProb < 0.01) moveProb = 0.01; if(moveProb > 1.0) moveProb = 1.0; } bool Init(const double &rangeMinP [], const double &rangeMaxP [], const double &rangeStepP [], const int epochsP = 0); void Moving(); void Revision(); //--- видимые параметры int nNeighbors; // число соседей на поток double radMin; // пол радиуса зонда (доля диапазона) double distrPower; // концентрация PowerDistribution int useSlope; // 1 -> каноническое ядро хода (для A/B) double moveProb; // вероятность перемещения координаты в (9) private: S_FDAm_Flow flow []; // [popSize] — потоки int phase; // 0..nNeighbors-1 сосед, nNeighbors — ход int epochs; // всего эпох (от стенда) int epochNow; // текущая эпоха double fSpread; // размах фитнеса стартовой популяции (для useSlope=1) bool nCached; // кэш второго значения Бокса-Мюллера double nSpare; //--- вспомогательные double RandN(); void MakeNeighbor(int i); void MakeMove(int i); };Init() — стандартная инициализация через StandardInit() базового класса (диапазоны, шаг, массив агентов), затем сброс собственного состояния: счётчики эпох и фаз, кэш генератора, выделение и инициализация массива потоков по размеру популяции. Принимает число эпох от стенда — оно сохраняется, хотя в текущей модификации расписаний по эпохам нет.
//+------------------------------------------------------------------+ //| Init | //+------------------------------------------------------------------+ bool C_AO_FDAm::Init(const double &rangeMinP [], const double &rangeMaxP [], const double &rangeStepP [], const int epochsP = 0) { if(!StandardInit(rangeMinP, rangeMaxP, rangeStepP)) return false; epochs = epochsP; if(epochs < 1) epochs = 1; epochNow = 0; phase = 0; fSpread = 1.0; nCached = false; nSpare = 0.0; ArrayResize(flow, popSize); for(int i = 0; i < popSize; i++) flow [i].Init(coords); return true; }
RandN() — генератор стандартного нормального числа N(0, 1) по преобразованию Бокса—Мюллера со срезом на ±4 сигмы. Преобразование даёт значения парами, поэтому второе кэшируется и возвращается следующим вызовом: при 1000 координатах это вдвое сокращает число трансцендентных вычислений.
//+------------------------------------------------------------------+ //| RandN — N(0,1) по Боксу-Мюллеру, срез на +-4 сигмы, кэш пары | //+------------------------------------------------------------------+ double C_AO_FDAm::RandN() { if(nCached) { nCached = false; return nSpare; } double u1 = u.RNDprobab(); double u2 = u.RNDprobab(); if(u1 < 1.0e-12) u1 = 1.0e-12; double rad = MathSqrt(-2.0 * MathLog(u1)); double ang = 2.0 * M_PI * u2; double z0 = rad * MathCos(ang); double z1 = rad * MathSin(ang); if(z1 > 4.0) z1 = 4.0; if(z1 < -4.0) z1 = -4.0; nSpare = z1; nCached = true; if(z0 > 4.0) z0 = 4.0; if(z0 < -4.0) z0 = -4.0; return z0; }
MakeNeighbor() — фаза разведки, модификация 2. Сначала вычисляет нормированное расстояние потока до глобального лидера (та самая родная связь FDA из формулы (3)), затем формирует радиус зонда как max(distN, radMin) с потолком 1.0: далёкий от лидера поток ищет широко, близкий — дожимает, а radMin не даёт лучшему потоку замереть с нулевой окрестностью. Сам зонд порождается покоординатно через PowerDistribution — степенную концентрацию у текущей позиции, дающую при широком радиусе тяжелохвостое ядро: масса проб вплотную к потоку, редкие — через полпространства. Результат кладётся в a[i] через SeInDiSp.
//+------------------------------------------------------------------+ //| MakeNeighbor — зонд вокруг потока (Mодификация 2): | //| радиус = max(distN, radMin) по каждой координате, степенная | //| концентрация у потока. distN — нормированное расстояние до | //| глобального лучшего (родная связь FDA из (3)). | //+------------------------------------------------------------------+ void C_AO_FDAm::MakeNeighbor(int i) { double rSize, d, x, r; //--- нормированное расстояние до глобального лучшего double distN = 0.0; for(int c = 0; c < coords; c++) { rSize = rangeMax [c] - rangeMin [c]; if(rSize <= 0.0) continue; d = (cB [c] - flow [i].c [c]) / rSize; distN += d * d; } distN = MathSqrt(distN); //--- адаптивный радиус: далёкий поток шарит широко, близкий дожимает; // пол radMin не даёт лучшему потоку замереть (у него distN = 0) double radN = distN; if(radN < radMin) radN = radMin; if(radN > 1.0) radN = 1.0; for(int c = 0; c < coords; c++) { rSize = rangeMax [c] - rangeMin [c]; r = radN * rSize; x = u.PowerDistribution(flow [i].c [c], flow [i].c [c] - r, flow [i].c [c] + r, distrPower); a [i].c [c] = u.SeInDiSp(x, rangeMin [c], rangeMax [c], rangeStep [c]); } }
MakeMove() — фаза перемещения. При useSlope = 1 (режим A/B-сравнения) отрабатывает каноническое ядро (6)–(8) с нормировками и срезкой сингулярности 1/L — в дефолтной конфигурации этот блок выключен. Основной путь — модификация 1: ветка (9) как единственный ход. Выбирается случайный поток; если он выгоднее — дрейф к нему со случайным нормальным множителем, иначе — дрейф к глобальному лидеру. Оба смещения построены только на разностях позиций. Каждая координата перемещается с вероятностью moveProb, остальные копируются без изменений — хорошие координаты не разрушаются полномерным шагом.
//+------------------------------------------------------------------+ //| MakeMove — ход потока. | //| useSlope=0 (M1): всегда ветка (9) — сток к случайному более | //| выгодному потоку либо к глобальному лучшему, в разностной | //| форме, покоординатно. | //| useSlope=1: каноническое ядро (6)(7)(8) для A/B-замера | //| (нормировки и срезки — как в честной реализации FDA). | //+------------------------------------------------------------------+ void C_AO_FDAm::MakeMove(int i) { double rSize, dXn, x; //--- каноническое ядро хода — только по запросу if(useSlope == 1 && flow [i].bnF > flow [i].f) { double Ln = 0.0; for(int c = 0; c < coords; c++) { rSize = rangeMax [c] - rangeMin [c]; if(rSize <= 0.0) continue; dXn = (flow [i].c [c] - flow [i].bnC [c]) / rSize; Ln += dXn * dXn; } Ln = MathSqrt(Ln); if(Ln > 1.0e-10) { double dFn = (flow [i].bnF - flow [i].f) / fSpread; if(dFn > 1.0) dFn = 1.0; for(int c = 0; c < coords; c++) { rSize = rangeMax [c] - rangeMin [c]; dXn = (flow [i].c [c] - flow [i].bnC [c]) / rSize; double S0 = dFn / MathMax(MathAbs(dXn), 1.0e-10); double V = RandN() * S0; double stepN = V * (dXn / Ln); if(stepN > 1.0) stepN = 1.0; if(stepN < -1.0) stepN = -1.0; x = flow [i].c [c] + stepN * rSize; a [i].c [c] = u.SeInDiSp(x, rangeMin [c], rangeMax [c], rangeStep [c]); } return; } } //--- (9) как единственный ход (Mодификация 1): сток к случайному более выгодному // потоку, иначе — к глобальному лучшему int r = u.RNDminusOne(popSize); //--- Частичное перемещение (Mодификация 4): каждая координата двигается с // вероятностью moveProb, остальные остаются на месте. Полномерный // шаг в высокой размерности почти всегда портит больше координат, // чем улучшает, и жадная приёмка его отбрасывает — потоки в // 1000-мерных тестах жили на одних зондах. При moveProb=1.0 // поведение прежнее бит-в-бит. if(flow [r].f > flow [i].f) { for(int c = 0; c < coords; c++) { if(moveProb < 1.0 && u.RNDprobab() > moveProb) { a [i].c [c] = flow [i].c [c]; continue; } x = flow [i].c [c] + RandN() * (flow [r].c [c] - flow [i].c [c]); a [i].c [c] = u.SeInDiSp(x, rangeMin [c], rangeMax [c], rangeStep [c]); } } else { for(int c = 0; c < coords; c++) { if(moveProb < 1.0 && u.RNDprobab() > moveProb) { a [i].c [c] = flow [i].c [c]; continue; } x = flow [i].c [c] + 2.0 * u.RNDprobab() * (cB [c] - flow [i].c [c]); a [i].c [c] = u.SeInDiSp(x, rangeMin [c], rangeMax [c], rangeStep [c]); } } }
Moving() — диспетчер фаз. На самом первом вызове рассеивает популяцию равномерно по области поиска. Дальше по значению phase направляет эпоху либо в разведку (все слоты — зонды, по одному на поток), либо в перемещение (все слоты — ходы). Так итерация алгоритма стоимостью popSize·(nNeighbors + 1) оценок укладывается в стендовую сетку (popSize оценок за эпоху) без единого простаивающего слота.
//+------------------------------------------------------------------+ //| Moving | //+------------------------------------------------------------------+ void C_AO_FDAm::Moving() { //--- первый прогон: стартовые позиции потоков if(!revision) { for(int i = 0; i < popSize; i++) for(int c = 0; c < coords; c++) a [i].c [c] = u.SeInDiSp(u.RNDfromCI(rangeMin [c], rangeMax [c]), rangeMin [c], rangeMax [c], rangeStep [c]); return; } epochNow++; if(epochNow > epochs) epochNow = epochs; //--- фаза разведки окрестности if(phase < nNeighbors) { for(int i = 0; i < popSize; i++) MakeNeighbor(i); return; } //--- фаза перемещения for(int i = 0; i < popSize; i++) MakeMove(i); }
Revision() — приёмка результатов эпохи. Всегда обновляет глобального лучшего по всем слотам. На первом проходе превращает стартовую популяцию в потоки и замораживает размах её фитнеса (масштаб для канонического ядра). В фазе разведки накапливает лучшего зонда каждого потока и — модификация 3 — сразу принимает зонд на место потока, если тот его превзошёл: оценка уже оплачена бюджетом, выбрасывать её незачем. В фазе перемещения жадно принимает ход (только при улучшении), сбрасывает накопитель зондов и возвращает автомат в начало следующей итерации.
//+------------------------------------------------------------------+ //| Revision | //+------------------------------------------------------------------+ void C_AO_FDAm::Revision() { //--- глобальный лучший — по всем слотам for(int i = 0; i < popSize; i++) { if(a [i].f > fB) { fB = a [i].f; ArrayCopy(cB, a [i].c, 0, 0, coords); } } //--- первый проход: стартовая популяция становится потоками if(!revision) { double fMax = -DBL_MAX; double fMin = DBL_MAX; for(int i = 0; i < popSize; i++) { ArrayCopy(flow [i].c, a [i].c, 0, 0, coords); flow [i].f = a [i].f; flow [i].bnF = -DBL_MAX; if(a [i].f > fMax) fMax = a [i].f; if(a [i].f < fMin) fMin = a [i].f; } fSpread = fMax - fMin; if(fSpread < 1.0e-10) fSpread = 1.0e-10; phase = 0; revision = true; return; } //--- фаза разведки: копим лучшего соседа и СРАЗУ принимаем зонд, // если он лучше потока (Модификация 3): оценка уже оплачена бюджетом if(phase < nNeighbors) { for(int i = 0; i < popSize; i++) { if(a [i].f > flow [i].bnF) { flow [i].bnF = a [i].f; ArrayCopy(flow [i].bnC, a [i].c, 0, 0, coords); } if(a [i].f > flow [i].f) { flow [i].f = a [i].f; ArrayCopy(flow [i].c, a [i].c, 0, 0, coords); } } phase++; return; } //--- фаза перемещения: жадная приёмка хода for(int i = 0; i < popSize; i++) { if(a [i].f > flow [i].f) { flow [i].f = a [i].f; ArrayCopy(flow [i].c, a [i].c, 0, 0, coords); } flow [i].bnF = -DBL_MAX; } phase = 0; }
Результаты тестов
Финальная конфигурация 25|1|0.7|50|0|0.5, подтверждающий прогон на 30 повторах:
FDAm|Flow Direction Algorithm M|25.0|1.0|0.7|50.0|0.0|0.5|
=============================
5 Hilly's; Func runs: 10000; result: 0.8757299908689657
25 Hilly's; Func runs: 10000; result: 0.5880607504349583
500 Hilly's; Func runs: 10000; result: 0.27135635722877766
=============================
5 Forest's; Func runs: 10000; result: 0.9999680382790938
25 Forest's; Func runs: 10000; result: 0.6439970282200052
500 Forest's; Func runs: 10000; result: 0.0963338696148858
=============================
5 Megacity's; Func runs: 10000; result: 0.8426666666666668
25 Megacity's; Func runs: 10000; result: 0.4845333333333334
500 Megacity's; Func runs: 10000; result: 0.12005333333333433
=============================
All score: 4.92270 (54.70%)
54.70% против 39.70% у каноники — пятнадцать пунктов. Forest в размерности 10 решён практически идеально (0.99997), Hilly 10 поднят с 0.64 до 0.88, дискретный Megacity 10 — с 0.59 до 0.84. Путь к этому числу: базовые модификации 1–3 дали 41.8%, свип radMin и distrPower — 51.8%, гейт moveProb — 54.6–55.4% на двух репликах, и 54.70% на тридцати повторах легли ровно в этот интервал.
FDAm|Flow Direction Algorithm M|25.0|1.0|0.7|50.0|0.0|0.5|
=============================
Composite anti-cheat test: Hilly + Forest + Megacity + Peaks + Skin
Coordinates: 10; Epochs: 400; Repeats: 10; Shift: 0.000
=============================
Run 1/10: 0.9999995987592145
Run 2/10: 0.9998898875818913
Run 3/10: 0.8533299440624049
Run 4/10: 0.999087658370487
Run 5/10: 0.9866666664205196
Run 6/10: 0.8533266531143952
Run 7/10: 0.8931159091393855
Run 8/10: 0.893637053730243
Run 9/10: 0.9866470657753862
Run 10/10: 0.9966385186116538
=============================
Average result: 0.9462338956 (94.62%)
=============================
Античитерский композитный тест (десять координат, пять разнородных функций со своими диапазонами, фитнес — среднее) дал 94.62% — выше, чем средний уровень десятикоординатных ячеек основного стенда. Геометрических читов нет: все операторы FDAm покоординатны и построены на разностях позиций, и композит это подтверждает количественно.
Визуализация работы алгоритма FDAm на тестовых функциях и двух дополнительных.

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

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

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

FDAm на тестовой функции Peaks

FDAm на тестовой функции Ackley
По итогам тестирования алгоритм FDA занимает 30-е место в нашем рейтинге оптимизационных методов.
| cc | AO | Description | Hilly | Hilly Final | Forest | Forest Final | Megacity (discrete) | Megacity Final | Final Result | % of MAX | ||||||
| 10 p (5 F) | 50 p (25 F) | 1000 p (500 F) | 10 p (5 F) | 50 p (25 F) | 1000 p (500 F) | 10 p (5 F) | 50 p (25 F) | 1000 p (500 F) | ||||||||
| 1 | ANS | across neighbourhood search | 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 |
| 2 | 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 |
| 3 | 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 |
| 4 | (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 |
| 5 | 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 |
| 6 | 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 |
| 7 | 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 |
| 8 | 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 |
| 9 | 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 |
| 10 | 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 |
| 11 | COA | coyote_optimization_algorithm | 0,88909 | 0,70681 | 0,32718 | 1,92308 | 0,99467 | 0,85358 | 0,15152 | 1,99977 | 0,88533 | 0,71040 | 0,18981 | 1,78554 | 5,708 | 63,43 |
| 12 | 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 |
| 13 | 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 |
| 14 | 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 |
| 15 | 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 |
| 16 | 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 |
| 17 | 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 |
| 18 | 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 |
| 19 | 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 |
| 20 | 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 |
| 21 | 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 |
| 22 | 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 |
| 23 | 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 |
| 24 | 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 |
| 25 | DE | flow_direction_algorithm | 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 |
| 26 | 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 |
| 27 | (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 |
| 28 | 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 |
| 29 | 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 |
| 30 | FDAm | flow_direction_algorithm_M | 0,87573 | 0,58806 | 0,27135 | 1,73514 | 0,99997 | 0,64399 | 0,09633 | 1,74029 | 0,84267 | 0,48453 | 0,11888 | 1,44608 | 4,922 | 54,70 |
| 31 | 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 |
| 32 | 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 |
| 33 | 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 |
| 34 | 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 |
| 35 | 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 |
| 36 | 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 |
| 37 | 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 |
| 38 | BEA | bacterial_evolutionary_algorithm | 0,92170 | 0,59615 | 0,29340 | 1,81125 | 0,96906 | 0,44500 | 0,08233 | 1,49639 | 0,90533 | 0,43173 | 0,13676 | 1,47382 | 4,781 | 53,13 |
| 39 | BWOm | beluga_whale_optimization_M | 0,78488 | 0,56872 | 0,29557 | 1,64917 | 0,91370 | 0,61760 | 0,12988 | 1,66118 | 0,81333 | 0,49946 | 0,15004 | 1,46283 | 4,773 | 53,04 |
| 40 | 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 |
| 41 | ACA | andean_condor_algorithm | 0,78444 | 0,53260 | 0,33108 | 1,64812 | 0,79071 | 0,44960 | 0,10685 | 1,34716 | 0,92266 | 0,67733 | 0,17613 | 1,77612 | 4,771 | 53,02 |
| 42 | 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 |
| 43 | 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 |
| 44 | DOAm | dynastic_optimization_algorithm | 0,63202 | 0,58003 | 0,31943 | 1,53148 | 0,76947 | 0,62730 | 0,12425 | 1,52102 | 0,74133 | 0,59520 | 0,16550 | 1,50203 | 4,555 | 50,61 |
| 45 | 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 |
| 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 | |
Выводы
FDA — характерный представитель большой волны природных метаэвристик: красивая метафора, внушительные таблицы в публикации и механика, которая при внимательном разборе оказывается наполовину неработоспособной. Три артефакта — умножение на абсолютную позицию лидера, паразитный член в генерации соседей и диагональ, спрятанная в сокращающихся множителях трёх формул, — обеспечивали отличную сходимость на тестовых наборах с оптимумом в начале координат. На стенде с несмещёнными функциями всё это не даёт ничего, и честный FDA показывает 39.70%.
При этом сама работа с алгоритмом оказалась продуктивной: диагностика указала точное место поломки (шаг, не способный быть маленьким), и четыре прицельных изменения — при сохранении каркаса, бюджета и духа исходной метафоры — подняли результат до 54.70%. FDAm занимает 30-е место в рейтинговой таблице серии, а его композитный античит-тест чист.
Что из этого полезно читателю-трейдеру? Прежде всего — прикладной инструмент. FDAm с параметрами по умолчанию готов к использованию в качестве движка оптимизации параметров советников: он совместим с фреймворком C_AO, честен по построению и особенно силён в задачах малой и средней размерности — а подбор пяти-двадцати параметров торговой системы — это ровно тот диапазон, где FDAm показывает свои лучшие качества (0.84–1.00 на десятикоординатных задачах). Тяжелохвостое поисковое ядро даёт ему полезное для трейдинга свойство. Основную часть времени алгоритм дожимает найденную область, но регулярно проверяет дальние участки пространства параметров. Это снижает риск застрять в локальном оптимуме, который в задачах с зашумлённым фитнесом (а фитнес торговой системы всегда зашумлён) особенно коварен.

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

Рисунок 3. Гистограмма результатов тестирования алгоритмов (по шкале от 0 до 100: чем больше, тем лучше, где 100 — максимально возможный теоретический результат). В архиве скрипт для расчёта рейтинговой таблицы
Плюсы и минусы алгоритма FDAm
Плюсы:
- Существенный прирост качества: 54.70% против 39.70% у каноники при том же бюджете и каркасе.
- Практически идеальная работа в малой размерности: Forest 10 координат решён (0.99997), сильные результаты на гладком Hilly (0.88) и дискретном Megacity (0.84) — рабочий диапазон типичных прикладных задач.
- Тяжелохвостое поисковое ядро: постоянный дожим найденной области с регулярной проверкой дальних участков — устойчивость к застреванию в локальных оптимумах.
- Бюджет расходуется полностью: удачные зонды принимаются в популяцию, замирание лидера устранено полом радиуса.
- Честность по построению: все операторы покоординатны и построены на разностях позиций, подтверждено композитным античит-тестом (94.62%).
Минусы:
- Высокая размерность не пробита: на уровне каноники (~0.27/0.10/0.12) — предел схемы (зонд + дрейф), не устранённый ни одной из четырёх модификаций.
- Больше параметров, чем у каноники (radMin, distrPower, moveProb), и чувствительность к moveProb заметная: 0.05 обваливает результат до 46.5%.
- Умеренная точность на средней размерности (50 координат: 0.48–0.64).
К статье прикреплён архив с актуальными версиями кодов алгоритмов. Автор статьи не несёт ответственности за абсолютную точность в описании канонических алгоритмов, во многие из них внесены изменения для улучшения поисковых возможностей. Выводы и суждения, представленные в статьях, основываются на результатах проведённых экспериментов.
Программы, используемые в статье
| # | Имя | Тип | Описание |
|---|---|---|---|
| 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_FDAm.mq5 | Скрипт | Испытательный стенд для FDAm |
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
VQC-фильтр с живым обучением прямо в MQL5 поверх графового fair-value скальпера
Автоматизация торговых стратегий в MQL5 (Часть 42): Сессионная система пробоя начального диапазона (ORB)
Автоматизация торговых стратегий в MQL5 (Часть 43): Адаптивная стратегия на основе канала линейной регрессии
От начального до среднего уровня: Очереди, списки и деревья (IV)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования