preview
Эволюция городских советов — City Councils Evolution (CCE)

Эволюция городских советов — City Councils Evolution (CCE)

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

Содержание

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


Введение

Социальная метафора этого алгоритма привлекла мое внимание для рассмотрения в задачах оптимизации для торговых роботов.  В чем суть: город устроен иерархически: сначала есть мельчайшие кварталы, у каждого свой совет. Лучший член квартального совета становится его главой и входит в совет района. Лучший в районном совете идёт выше, и так до верховного совета города. Члены совета стараются улучшить свои показатели, чтобы быть избранными и подняться на уровень выше. Автор — Einollah Pira опубликовал алгоритм City Councils Evolution в 2022 году.

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

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

Далее по порядку: реализую каноническую версию в C AO, протестирую на стенде, проверю гипотезу о скрытых преимуществах и затем попробую устранить выявленные проблемы. Все промежуточные результаты по ходу работы будут приведены.


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

Итак, как работает каноническая версия. Прежде чем переходить к коду, соберём картину целиком — так, как алгоритм себя представляет.

Состояние. Есть совет из N решений, отсортированный по убыванию приспособленности. Первый элемент — верховный совет, он же является лучшим решением. Остальные разложены по уровням d-арного дерева, и родитель любого узла всегда стоит в массиве раньше него, то есть всегда лучше. Одна элементарная операция — это обработка одной координаты g у одного члена совета m. Происходит следующее.

Сначала выбирается напарник. Для корня напарником служит копия его самого, у которой координата g заменена равномерно случайным значением из диапазона — это единственное место во всём алгоритме, где рождается новый материал. Для остальных членов напарником становится их родитель по дереву. Дальше берётся случайное число alpha. Распределение у него необычное, двугорбое: с вероятностью одна вторая это малое положительное значение из [0; 0.5), иначе — большое отрицательное из [-1; -0.5]. Промежуточных значений оператор не выдаёт.

Затем строятся четыре кандидата. Все они — точные копии текущего решения, у которых изменена ровно одна координата g, и различаются только формулой этой координаты: две комбинации отталкиваются от значения потомка, две — от значения родителя, с множителями alpha и 1 - alpha соответственно. Все четыре обрезаются по границам диапазона. Наконец, все четыре кандидата оцениваются целевой функцией, лучший из них сравнивается с текущим решением, и если он лучше — заменяет его. Приём жадный: ухудшение не принимается никогда.

Один шаг алгоритма — это описанная операция, проделанная последовательно для всех членов совета сверху вниз, от корня к листьям. Порядок важен: потомок читает родителя уже обновлённым в этом же шаге. После обхода совет пересортировывается — это и есть перестройка дерева советов. Один полный цикл — это N таких шагов, по одному на каждую координату задачи. Затем цикл повторяется с первой координаты.

Отсюда следуют два ключевых ограничения, которые определят всё дальнейшее. Во-первых, за один шаг решение меняется ровно по одной координате из всех — это классический покоординатный спуск со всеми его свойствами, хорошими и плохими. Во-вторых, на одну координату одного агента требуется 4 вызова ЦФ. При бюджете 10000, совете из 30 членов и 1000 координат алгоритм не успевает пройти даже 10% координат хотя бы один раз.

Что в итоге, CCE — это покоординатный спуск с четырьмя пробами на координату, жадным приёмом и аддитивным оператором. Всё остальное — обёртка.

Псевдокод алгоритма:
ВХОД: N (число членов совета), d (степень дерева), maxEva, dim, [lb, ub]
h = floor(log(N*(d-1)) / log(d) + 1)
maxIter = round(maxEva / (dim * N * 4))

// Инициализация
M[1..N] = random_uniform(lb, ub)
for i = 1..N: fit[i] = FF(M[i])
sort_ascending(M, fit)

// Главный цикл
for t = 2..maxIter:
  for g = 1..dim:                          // проход по координатам

    Mnew = M                               // обновления идут каскадом

    // 1. Корень: мутация одной координаты у лучшего
    Z    = M[1];  Z[g] = lb[g] + rand * (ub[g] - lb[g])
    Y    = Improve(M[1], Z, g)              // 4 вызова ЦФ
    if FF(Y) < fit[1]: fit[1] = FF(Y);  Mnew[1] = Y

    // 2. Остальные узлы, сверху вниз
    for level = 2..h:
      sIndex = (d^(level-1) - 1)/(d-1) + 1
      eIndex = min((d^level - 1)/(d-1), N)
      for i = sIndex..eIndex:
        j = floor(i/d);  if (i mod d) > 1: j = j + 1
        parent = Mnew[j]                    // уже обновлён на этом же g
        child  = Mnew[i]
        (Y, Yfit) = Improve(parent, child, g)   // 4 вызова ЦФ
        if Yfit < fit[i]: fit[i] = Yfit;  Mnew[i] = Y

    M = Mnew
    sort_ascending(M, fit)                  // перестройка дерева

  best = M[1]


function Improve(X1, X2, g):
    if X1 == X2:                            // поэлементно!
        X2 = copy(X1);  X2[g] = U[lb[g], ub[g]]

    rnd   = U[0,1)
    alpha = (rnd < 0.5) ? rnd : -rnd

    Y1 = X2;  Y1[g] = X2[g] + alpha       * X1[g]
    Y2 = X2;  Y2[g] = X2[g] + (1 - alpha) * X1[g]
    Y3 = X2;  Y3[g] = X1[g] + alpha       * X2[g]
    Y4 = X2;  Y4[g] = X1[g] + (1 - alpha) * X2[g]

    clamp(Y1..Y4, lb, ub)
    evaluate Y1..Y4
    return argmin_fitness(Y1, Y2, Y3, Y4)

Теперь разберем работу алгоритма по схеме:

CCE_scheme

Рисунок 1. Схема работы CCE: обход дерева советов и построение четвёрки кандидатов

Слева — дерево советов. Тринадцать узлов при d = 3, в каждом стоит ранг в отсортированном массиве. Оранжевым выделена пара родитель 3 → потомок 9, подписанная как X1 и X2 — она и разбирается на средней панели, оранжевая стрелка ведёт туда. Число в узле означает ранг в отсортированном массиве.

Справа — сама операция. Векторы показаны полосками из шести ячеек, столбец g подсвечен жёлтым насквозь через все шесть строк. Сверху X1 и X2, под ними распределение α, дальше Y1…Y4 с формулами справа. Зелёным помечена единственная ячейка, которая в кандидатах отличается — это главное, что должно броситься в глаза. Внизу красным расход целевой функции, то есть отбор лучшего из четырёх и жадный приём, и четыре вызова на одну координату одного члена совета.

Снизу — шаг и цикл, пять пунктов, последний вынесен красным: за один шаг решение меняется ровно по одной координате из dim. 

При переносе в C_AO пришлось решить некоторые вопросы.

Расход целевой функции. Стенд вызывает целевую функцию ровно popSize раз за эпоху, а CCE требует четыре последовательных вызова на одного агента на одну координату. Решение: popSize жёстко равен четырём — это размер четвёрки Y1..Y4, а не параметр алгоритма. Размер совета вынесен в отдельный параметр councilSize. Одна эпоха стенда = один член совета на одной координате.

Направление оптимизации. У автора минимизация, в C_AO максимизация. Все сравнения и направление сортировки инвертированы.

Параметр h (высота дерева) не нужен: у автора он лишь ограничивает диапазон обхода уровней и при рабочих значениях покрывает все узлы, то есть эквивалентен простому проходу по всем членам совета.

Полный код канонической версии:

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
struct S_CCE_Member
  {
   double            c [];   // координаты принятого решения
   double            f;      // приспособленность принятого решения

   void              Init(int coords)
     {
      ArrayResize(c, coords);
      f = -DBL_MAX;
     }
  };

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class C_AO_CCE : public C_AO
  {
public:
                    ~C_AO_CCE() {}
                     C_AO_CCE()
     {
      ao_name = "CCE";
      ao_desc = "City Councils Evolution";
      ao_link = "https://www.mql5.com/ru/users/joo";

      popSize     = 4;    // всегда 4: кандидаты Y1..Y4 одного члена совета
      councilSize = 2;    // число членов совета (N в статье)
      treeDegree  = 3;    // d - степень дерева советов

      ArrayResize(params, 3);
      params [0].name = "popSize";
      params [0].val  = popSize;
      params [1].name = "councilSize";
      params [1].val  = councilSize;
      params [2].name = "treeDegree";
      params [2].val  = treeDegree;
     }

   void              SetParams()
     {
      //--- popSize не является параметром CCE: оператор Improve всегда
      //    порождает ровно четыре кандидата. Значение фиксируется, чтобы
      //    тестовый стенд верно посчитал бюджет вызовов ЦФ.
      popSize     = 4;
      councilSize = (int)params [1].val;
      treeDegree  = (int)params [2].val;

      if(councilSize < 2)
         councilSize = 2;    // корень + минимум один потомок
      if(treeDegree < 2)
         treeDegree = 2;

      params [0].val = popSize;
      params [1].val = councilSize;
      params [2].val = treeDegree;
     }

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

   void              Moving();
   void              Revision();

   //--- видимые параметры
   int               councilSize;   // N - число членов совета
   int               treeDegree;    // d - степень дерева советов

   //--- диагностика: доля кандидатов, упёршихся в границу диапазона
   double            ClampRate() { return (probesTotal > 0 ? (double)probesClamped / probesTotal : 0.0); }

private:
   S_CCE_Member      M  [];         // [councilSize] совет
   S_CCE_Member      Mt [];         // буфер сортировки
   int               gene;          // индекс обрабатываемой координаты
   int               curMember;     // член совета, чья очередь в этой эпохе
   int               filled;        // сколько членов совета уже заполнено

   long              probesClamped; // счётчики диагностики
   long              probesTotal;

   double            Alpha();
   bool              SameVector(const double &v1 [], const double &v2 []);
  };

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

   ArrayResize(M,  councilSize);
   ArrayResize(Mt, councilSize);

   for(int i = 0; i < councilSize; i++)
     {
      M  [i].Init(coords);
      Mt [i].Init(coords);
     }

   gene          = 0;
   curMember     = 0;
   filled        = 0;
   probesClamped = 0;
   probesTotal   = 0;

   return true;
  }

//+------------------------------------------------------------------+
//|   Alpha — авторская формула:                                     |
//|   rnd = U[0;1), k = round(1 + rnd), alpha = (-1)^(k+1) * rnd     |
//|   => alpha in [0; 0.5)  либо  alpha in [-1; -0.5]                |
//+------------------------------------------------------------------+
double C_AO_CCE::Alpha()
  {
   double rnd = u.RNDprobab();
   return (rnd < 0.5 ? rnd : -rnd);
  }

//+------------------------------------------------------------------+
//|   SameVector — поэлементное сравнение                            |
//+------------------------------------------------------------------+
bool C_AO_CCE::SameVector(const double &v1 [], const double &v2 [])
  {
   for(int c = 0; c < coords; c++)
      if(v1 [c] != v2 [c])
         return false;
   return true;
  }

//+------------------------------------------------------------------+
//|                            Moving                                |
//|   Одна эпоха = один член совета на одной координате.             |
//|   Слоты a[0..3] — кандидаты Y1..Y4.                              |
//+------------------------------------------------------------------+
void C_AO_CCE::Moving()
  {
//--- фаза инициализации: заполнение совета случайными решениями
   if(filled < councilSize)
     {
      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;
     }

   int    g = gene;
   int    m = curMember;
   int    p;
   double x1, x2, alpha, raw;

   if(m == 0)
     {
      //--- корень: партнёр — копия самого себя со случайным геном g
      p  = 0;
      x1 = M [0].c [g];
      x2 = u.SeInDiSp(u.RNDfromCI(rangeMin [g], rangeMax [g]),
                      rangeMin [g], rangeMax [g], rangeStep [g]);
     }
   else
     {
      //--- родитель узла m в d-арном дереве. Гаусс-Зейдель: p < m всегда,
      //    значит M[p] уже обновлён на этой же координате g.
      p  = (m - 1) / treeDegree;
      x1 = M [p].c [g];
      x2 = M [m].c [g];

      //--- ветвь оригинала if X1==X2: скрытая инъекция разнообразия
      if(SameVector(M [p].c, M [m].c))
         x2 = u.SeInDiSp(u.RNDfromCI(rangeMin [g], rangeMax [g]),
                         rangeMin [g], rangeMax [g], rangeStep [g]);
     }

   alpha = Alpha();

   for(int k = 0; k < 4; k++)
     {
      ArrayCopy(a [k].c, M [m].c, 0, 0, coords);

      switch(k)
        {
         case 0:
            raw = x2 + alpha         * x1;
            break;   // Y1
         case 1:
            raw = x2 + (1.0 - alpha) * x1;
            break;   // Y2
         case 2:
            raw = x1 + alpha         * x2;
            break;   // Y3
         default:
            raw = x1 + (1.0 - alpha) * x2;
            break;   // Y4
        }

      //--- диагностика клампинга
      probesTotal++;
      if(raw < rangeMin [g] || raw > rangeMax [g])
         probesClamped++;

      a [k].c [g] = u.SeInDiSp(raw, rangeMin [g], rangeMax [g], rangeStep [g]);
     }
  }

//+------------------------------------------------------------------+
//|                           Revision                               |
//+------------------------------------------------------------------+
void C_AO_CCE::Revision()
  {
//--- фаза инициализации
   if(filled < councilSize)
     {
      //--- лучшие из четвёрки идут в свободные места совета
      int order [];
      ArrayResize(order, popSize);
      for(int i = 0; i < popSize; i++)
         order [i] = i;

      for(int i = 0; i < popSize - 1; i++)
         for(int j = i + 1; j < popSize; j++)
            if(a [order [j]].f > a [order [i]].f)
              {
               int t = order [i];
               order [i] = order [j];
               order [j] = t;
              }

      for(int i = 0; i < popSize && filled < councilSize; i++, filled++)
        {
         M [filled].f = a [order [i]].f;
         ArrayCopy(M [filled].c, a [order [i]].c, 0, 0, coords);
        }

      if(filled >= councilSize)
        {
         u.Sorting(M, Mt, councilSize);
         gene      = 0;
         curMember = 0;
        }

      if(M [0].f > fB)
        {
         fB = M [0].f;
         ArrayCopy(cB, M [0].c, 0, 0, coords);
        }

      revision = true;
      return;
     }

//--- лучший из четвёрки, жадный приём
   int    bestIdx = 0;
   double bestF   = a [0].f;

   for(int k = 1; k < 4; k++)
     {
      if(a [k].f > bestF)
        {
         bestF   = a [k].f;
         bestIdx = k;
        }
     }

   int m = curMember;

   if(bestF > M [m].f)
     {
      M [m].f = bestF;
      ArrayCopy(M [m].c, a [bestIdx].c, 0, 0, coords);
     }

   if(M [m].f > fB)
     {
      fB = M [m].f;
      ArrayCopy(cB, M [m].c, 0, 0, coords);
     }

//--- следующий член совета; после полного обхода — пересортировка и
//--- переход к следующей координате 
   curMember++;
   if(curMember >= councilSize)
     {
      curMember = 0;
      u.Sorting(M, Mt, councilSize);

      gene++;
      if(gene >= coords)
         gene = 0;
     }
  }

Протестируем каноническую версию. Первое, что выяснилось при подборе параметров: композит строго монотонен по размеру совета, и растёт он при уменьшении.

councilSize 30 25 10 5 3 2
composite 32.29% 33.84% 36.21% 37.21% 38.52%
41.15%

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

Степень дерева проверена отдельно: d = 2 против d = 3 при фиксированном совете 40 дают 36.36% и 36.21%, то есть разница 0.15 пункта — внутри шума. Декоративность дерева подтверждена не только чтением кода, но и экспериментом.

Порядок обхода тоже проверен. Первая реализация использовала Якоби (все члены читают состояние предыдущей эпохи), вторая — точный Гаусс-Зейдель плюс восстановленную ветвь X1==X2 . Разница: 41.37% против 41.15%, то есть ноль. Алгоритм нечувствителен и к этому.

Финальный результат канона:

CCE|City Councils Evolution|4.0|2.0|3.0|
=============================
5 Hilly's; Func runs: 10000; result: 0.6572185761914582
25 Hilly's; Func runs: 10000; result: 0.5634432555302793
500 Hilly's; Func runs: 10000; result: 0.35465293619703997
=============================
5 Forest's; Func runs: 10000; result: 0.36172559279184446
25 Forest's; Func runs: 10000; result: 0.1753046542071895
500 Forest's; Func runs: 10000; result: 0.07924288133843145
=============================
5 Megacity's; Func runs: 10000; result: 0.7453333333333332
25 Megacity's; Func runs: 10000; result: 0.5661333333333334
500 Megacity's; Func runs: 10000; result: 0.2007066666666673
=============================
All score: 3.70376 (41.15%)

Отдельно бросается в глаза Forest — 0.362 на малой размерности против 0.657 у Hilly и 0.745 у Megacity. Объяснение структурное: Forest единственная на стенде функция с диагональными гребнями, а покоординатный спуск принципиально не может идти вдоль гребня. Чтобы двигаться по дну диагонального оврага, надо менять две координаты согласованно, а оператор меняет ровно одну. Агент упирается в стенки оврага по обеим осям и застревает в точке, которая не оптимальна ни в каком смысле, кроме покоординатного.

Теперь к самому интересному. Посмотрим ещё раз на оператор:

Y = x2 + alpha * x1

Это сумма координат, а не интерполяция. Шаг никак не привязан к ширине диапазона поиска — он пропорционален абсолютной величине координат, то есть расстоянию до начала координат. Оператор не инвариантен к переносу системы координат.

Что это значит на практике. Когда родитель и потомок сблизились, x1 ≈ x2 = v:

кандидат множитель при v alpha in [0; 0.5) alpha in [-1; -0.5]

Y1, Y3

1 + alpha [1.0; 1.5) [0; 0.5]
Y2, Y4 2 - alpha (1.5; 2.0]
[2.5; 3.0]

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

Любой стенд тестирования — это не только набор функций, но и договорённость о том, где расположена область поиска. И почти всегда она расположена симметрично относительно нуля: диапазон вида от минус десяти до плюс десяти. А это значит, что начало координат лежит ровно в центре области.

Теперь представим алгоритм, который вообще ничего не ищет, а просто стягивает свои решения к нулю. На таком стенде он окажется в центре области поиска — а центр для большинства тестовых функций место статистически неплохое, куда лучше случайной точки. Алгоритм получит приличный балл, не сделав ни одного осмысленного шага. 

Ровно такое подозрение и возникло к CCE. Оператор Y = x2 + alpha * x1 с отрицательной альфой даёт множитель в диапазоне от нуля до половины, то есть сжимает координату к нулю. Вопрос в том, сколько из набранного балла заработано поиском, а сколько — этим сжатием.

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

Как устроен тест. Область поиска сдвигается на ShiftFactor ширин диапазона, а перед вызовом целевой функции сдвиг вычитается обратно.

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

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- алгоритм
   C_AO *AO = new C_AO_CCE();
   AO.params [0].val = PopSize_P;
   AO.params [1].val = TreeDegree_P;
   AO.SetParams();
   Print(AO.GetName(), "|", AO.GetDesc(), "|", AO.GetParams());

//--- создаём 5 функций
#define NFUNCS 5
   C_Function *funcs [NFUNCS];
   funcs [0] = new C_Hilly();
   funcs [1] = new C_Forest();
   funcs [2] = new C_Megacity();
   funcs [3] = new C_Peaks();
   funcs [4] = new C_Skin();

//--- 10 координат: пара 0-1 = Hilly, 2-3 = Forest, 4-5 = Megacity,
//                   6-7 = Peaks, 8-9 = Skin
   int params = NFUNCS * 2;

   double rangeMin  [];
   double rangeMax  [];
   double rangeStep [];
   double offset    [];     // сдвиг по каждой координате

   ArrayResize(rangeMin,  params);
   ArrayResize(rangeMax,  params);
   ArrayResize(rangeStep, params);
   ArrayResize(offset,    params);

   for(int i = 0; i < NFUNCS; i++)
     {
      double xMin = funcs [i].GetMinRangeX();
      double xMax = funcs [i].GetMaxRangeX();
      double yMin = funcs [i].GetMinRangeY();
      double yMax = funcs [i].GetMaxRangeY();

      //--- сдвиг пропорционален ширине диапазона своей координаты,
      //    поэтому геометрия ландшафта не искажается
      offset [i * 2]     = ShiftFactor_P * (xMax - xMin);
      offset [i * 2 + 1] = ShiftFactor_P * (yMax - yMin);

      rangeMin  [i * 2]     = xMin + offset [i * 2];
      rangeMax  [i * 2]     = xMax + offset [i * 2];
      rangeStep [i * 2]     = 0.0;

      rangeMin  [i * 2 + 1] = yMin + offset [i * 2 + 1];
      rangeMax  [i * 2 + 1] = yMax + offset [i * 2 + 1];
      rangeStep [i * 2 + 1] = 0.0;
     }

//--- запуск тестов
   int    epochCount = NumbTestFuncRuns_P / (int)AO.params [0].val;
   double aveResult  = 0.0;

   Print("=============================");
   Print("Composite anti-cheat test: Hilly + Forest + Megacity + Peaks + Skin");
   Print("Coordinates: ", params, "; Epochs: ", epochCount,
         "; Repeats: ", NumberRepetTest_P);
   Print("Origin shift factor: ", DoubleToString(ShiftFactor_P, 3),
         (ShiftFactor_P == 0.0 ? "  (стандартный композитный прогон)"
          : "  (ноль вынесен из области поиска)"));
   Print("=============================");

   for(int test = 0; test < NumberRepetTest_P; test++)
     {
      //--- StandardInit() сидит MathSrand от GetMicrosecondCount(), поэтому
      //--- сид меняется от прогона к прогону без задержки. Sleep оставлен
      //--- защитно для паритета условий с остальными прогонами серии.
      Sleep(1000);

      if(!AO.Init(rangeMin, rangeMax, rangeStep, epochCount))
         break;

      for(int epochCNT = 1; epochCNT <= epochCount && !IsStopped(); epochCNT++)
        {
         AO.Moving();

         //--- фитнес агента: среднее по 5 функциям.
         //    Сдвиг вычитается ПЕРЕД вызовом Core, поэтому ландшафт
         //    в точности тот же, что и при ShiftFactor_P = 0.
         for(int set = 0; set < ArraySize(AO.a); set++)
           {
            double sum = 0.0;
            for(int i = 0; i < NFUNCS; i++)
              {
               sum += funcs [i].Core(AO.a [set].c [i * 2]     - offset [i * 2],
                                     AO.a [set].c [i * 2 + 1] - offset [i * 2 + 1]);
              }
            AO.a [set].f = sum / NFUNCS;
           }

         AO.Revision();
        }

      Print("Run ", test + 1, "/", NumberRepetTest_P, ": ", AO.fB);
      aveResult += AO.fB;
     }

   aveResult /= (double)NumberRepetTest_P;

   Print("=============================");
   Print("Average result: ", DoubleToString(aveResult, 10),
         " (", DoubleToString(aveResult * 100.0, 2), "%)");
   Print("=============================");

//--- освобождение
   for(int i = 0; i < NFUNCS; i++)
      delete funcs [i];
   delete AO;
  }
//+------------------------------------------------------------------+

Две строки вычитания — и Core всегда получает аргументы из своего родного диапазона. Ландшафт побитово тот же, файл TestFunctions.mqh трогать не нужно. При ShiftFactor = 0 тест вырождается в обычный прогон, что удобно как контроль.

Проверка проводилась на композитном стенде из пяти функций — Hilly, Forest, Megacity, Peaks и Skin, по две координаты на функцию, десять координат всего. Композитный стенд взят не случайно: он заодно страхует от геометрических читов вроде диагонального выравнивания, поскольку пять функций с разной геометрией одновременно обмануть гораздо труднее.

Результаты. Контрольный прогон без сдвига, каноническая конфигурация с советом из двух членов:

CCE|City Councils Evolution|8.0|3.0|
=============================
Composite anti-cheat test: Hilly + Forest + Megacity + Peaks + Skin
Coordinates: 10; Epochs: 1250; Repeats: 10
Origin shift factor: 0.000  (стандартный композитный прогон)
=============================
Run 1/10: 0.6483110084799746
Run 2/10: 0.7951162265855023
Run 3/10: 0.5955865723649618
Run 4/10: 0.6205429819310735
Run 5/10: 0.8968435531503033
Run 6/10: 0.7412102326450956
Run 7/10: 0.5909374742006821
Run 8/10: 0.8204399277988635
Run 9/10: 0.5869085305194864
Run 10/10: 0.8755874206945741
=============================
Average result: 0.7171483928 (71.71%)
=============================

Прежде чем двигаться дальше, обратите внимание на сами повторы. Они разделяются на две группы без промежуточных значений: пять значений в районе 0.59–0.65 и четыре в районе 0.80–0.90. Это не разброс, а два разных исхода — покоординатный спуск либо попадает в нужный бассейн на ранних проходах, либо не попадает и до конца дистанции оттуда не выбирается, поскольку механизма перезапуска у алгоритма нет. Повторный прогон той же конфигурации, где верхний кластер оказался многочисленнее, дал 80.45 процента. То есть любую цифру CCE нужно читать с поправкой на несколько пунктов.

Теперь тот же тест, но начало координат вынесено далеко за пределы области поиска:

CCE|City Councils Evolution|8.0|3.0|
=============================
Composite anti-cheat test: Hilly + Forest + Megacity + Peaks + Skin
Coordinates: 10; Epochs: 1250; Repeats: 10
Origin shift factor: 15.000  (ноль вынесен из области поиска)
=============================
Run 1/10: 0.4603848022729261
Run 2/10: 0.49190177329804446
Run 3/10: 0.475808107746248
Run 4/10: 0.35919649700418627
Run 5/10: 0.6379800933132185
Run 6/10: 0.6107661070329395
Run 7/10: 0.496872946742357
Run 8/10: 0.3626605945005663
Run 9/10: 0.4922209229995909
Run 10/10: 0.5747480775044673
=============================
Average result: 0.4962539922 (49.63%)
=============================

Здесь уже не расслоение, а равномерно низкие значения: верхний кластер исчез целиком, ни один из десяти повторов не дотянул даже до 0.64.

ShiftFactor 0.0  : 0.7171483928 (71.71%)   и повтор  0.8044943312 (80.45%)
ShiftFactor 0.7  : 0.7354187936 (73.54%)
ShiftFactor 1.5  : 0.6659142374 (66.59%)
ShiftFactor 2.0  : 0.7079809216 (70.80%)
ShiftFactor 3.0  : 0.6894945411 (68.95%)
ShiftFactor 5.0  : 0.5936715069 (59.37%)
ShiftFactor 15.0 : 0.4962539922 (49.63%)

Плато держится примерно до трёх ширин диапазона: значения от 66.6 до 73.5 неразличимы между собой на фоне того самого расслоения, которое мы только что видели в логе. Дальше начинается спад — 59.4 на пяти ширинах и 49.6 на пятнадцати.

Между сдвигом 0.7 и сдвигом 15 разрыв в 24 пункта. Подчеркну ещё раз: ландшафт при этом не менялся вообще, менялась только нумерация осей.

Почему так происходит. Спад оказался односторонним, и это существенная деталь. Из двух ожидаемых механизмов отказа реально работает только один.

Когда область поиска далеко от нуля — отказ. Шаг alpha * x1 при координатах порядка пятнадцати ширин диапазона составляет около семи с половиной ширин. Кандидат остаётся внутри границ, только пока alpha меньше отношения (ub - x2) / x1, то есть примерно меньше единицы, делённой на величину сдвига. Поскольку альфа распределена равномерно на половине единичного отрезка, доля выживших проб падает обратно пропорционально сдвигу: при сдвиге на три ширины внутри границ остаётся около 56 процентов положительных альф, при пяти — 40 процентов, при пятнадцати — всего 13. Остальные кандидаты вылетают за границу и обрезаются по границе. При больших сдвигах вся четвёрка вырождается в выбор между нижней и верхней границей, и осмысленный поиск прекращается.

Оговорка, важная для правильной формулировки: обрезанный кандидат не сбивает поиск с курса. Жадный приём просто отбрасывает его, если он оказался хуже текущего решения. То есть отказ при удалении области от нуля — это не порча направления, а постепенная кража бюджета: из четырёх проб на координату полезными остаются одна-две.

Когда область поиска рядом с нулём — не отказ, а подарок. При нулевом сдвиге начало координат лежит в центре области, и сжатие тянет агентов не в плохое место, а в центр. То самое бесплатное центральное смещение, ради поиска которого тест и затевался. 

Что это значит для практики. Из этого следует простое правило, применимое к любому вашему набору параметров. Важна не дистанция до нуля сама по себе, а её отношение к ширине диапазона (далее — “эксцентриситет”). Берём середину диапазона, делим её модуль на ширину диапазона, и смотрим на результат: до трёх алгоритм в безопасной зоне, около пяти начинается спад, дальше обвал.

Посчитаем для типичных параметров советника. Период индикатора от 5 до 200: середина 102.5, ширина 195, эксцентриситет 0.53 — безопасно. Размер лота от 0.01 до 1.0 даёт 0.51, стоп-лосс от 100 до 500 пунктов — 0.75. Все на плато, всё в порядке.

А теперь опасные случаи. Период скользящей средней, который вы решили искать в узком коридоре от 200 до 250: середина 225, ширина всего 50, эксцентриситет 4.5 — уже зона спада. И совсем плохо с ценовыми уровнями: диапазон от 1.0800 до 1.0900 имеет ширину одну сотую при середине 1.085, то есть эксцентриситет около ста восьми. Здесь канонический CCE работать не будет вовсе.

Обратите внимание на закономерность: беда не в больших числах как таковых, а в узких диапазонах, удалённых от нуля. Один и тот же период скользящей средней, заданный как от 5 до 250, совершенно безопасен, а как от 200 до 250 — уже нет. Сузив диапазон поиска в надежде ускорить оптимизацию, вы можете незаметно для себя загнать алгоритм в режим отказа

Модификация CCEm. Диагноз поставлен, лечение напрашивается само. Две болезни, и обе лечит одна правка оператора.

Аддитивность → разностность. Заменяем x2 + alpha * x1 на x2 + alpha * (x1 - x2). Шаг теперь задаётся расстоянием между партнёрами, а не расстоянием до нуля, и оператор становится инвариантным к переносу.

Покоординатность → векторность. Ход применяется ко всем координатам сразу, а не к одной. Появляется способность двигаться вдоль диагонального гребня.

Здесь есть подводный камень. Наивная разностная замена вырождается:

Y2 = x2 + (1 - alpha)*(x1 - x2) = x1 + alpha*(x2 - x1) = Y3

Тождественно. Четыре кандидата схлопываются в три, четверть бюджета уходит на дубликат. Поэтому четвёрка построена как две пары интерполяция / экстраполяция:

dif = x1 - x2
Y1 = x2 + alpha       * dif      // к x1
Y2 = x2 + (1 + alpha) * dif      // за x1
Y3 = x1 - alpha       * dif      // к x2
Y4 = x1 - (1 + alpha) * dif      // за x2

Авторское распределение alpha сохранено целиком, и теперь оно осмысленно: положительная альфа даёт короткую интерполяцию у одного конца и перелёт за другой, отрицательная — зеркально.

CCE_operators

Рисунок 2. Почему разностная форма чинит зависимость от системы координат

На обеих панелях изображена одна и та же ось координат: слева ноль, справа область поиска между lb и ub , внутри неё пара решений x1 и x2 — родитель и потомок. Фиолетовая размерная линия показывает, какая величина задаёт длину шага. У канона наверху это |x|, расстояние от нуля до решения, и красная стрелка показывает разброс, куда при таком шаге может улететь четвёрка кандидатов: практически на всю ось, причём большая часть попадёт за границы и будет обрезана (clamped). У модификации внизу шаг задаётся величиной |x1 − x2|, и зелёная стрелка укладывается в окрестность самой пары. Ноль при этом окрашен серым — он больше ни на что не влияет, отсюда и инвариантность к переносу (translation invariant). Нижние строки панелей отмечают вторую разницу: канон меняет одну координату за ход, модификация — все.

Ещё три момента. Alpha своя на каждую координату — один скаляр на весь вектор дал бы связку скаляр-на-вектор, то есть движение строго вдоль одной прямой и одну степень свободы на ход независимо от размерности. Партнёром корня становится свежий случайный вектор, поскольку при разностной форме копия с одним случайным геном дала бы нулевую разность по всем остальным координатам. И ветвь родитель равен потомку из декоративной становится несущей: при нулевой разности все четыре кандидата вырождаются в точку. Дерево советов, размер совета, жадный приём, пересортировка и сама четвёрка проб оставлены каноническими.

Полный код модификации:

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
struct S_CCEm_Member
  {
   double            c [];
   double            f;

   void              Init(int coords)
     {
      ArrayResize(c, coords);
      f = -DBL_MAX;
     }
  };
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class C_AO_CCEm : public C_AO
  {
public:
                    ~C_AO_CCEm() {}
                     C_AO_CCEm()
     {
      ao_name = "CCEm";
      ao_desc = "City Councils Evolution M";
      ao_link = "https://www.mql5.com/ru/articles/24072";

      popSize     = 4;    // всегда 4: кандидаты Y1..Y4 одного члена совета
      councilSize = 20;   // число членов совета (N в статье)
      treeDegree  = 3;    // d - степень дерева советов
      moveProb    = 0.4;  // вероятность участия координаты в ходе

      ArrayResize(params, 4);
      params [0].name = "popSize";
      params [0].val  = popSize;
      params [1].name = "councilSize";
      params [1].val  = councilSize;
      params [2].name = "treeDegree";
      params [2].val  = treeDegree;
      params [3].name = "moveProb";
      params [3].val  = moveProb;
     }

   void              SetParams()
     {
      popSize     = (int)params [0].val;
      councilSize = (int)params [1].val;
      treeDegree  = (int)params [2].val;
      moveProb    =      params [3].val;

      if(councilSize < 2)
         councilSize = 2;
      if(treeDegree < 2)
         treeDegree = 2;

      if(moveProb < 0.0)
         moveProb = 0.0;
      if(moveProb > 1.0)
         moveProb = 1.0;

      params [0].val = popSize;
      params [1].val = councilSize;
      params [2].val = treeDegree;
      params [3].val = moveProb;
     }

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

   void              Moving();
   void              Revision();

   //--- видимые параметры
   int               councilSize;
   int               treeDegree;
   double            moveProb;      // вероятность участия координаты в ходе

   //--- диагностика: доля координат кандидатов, упёршихся в границу
   double            ClampRate() { return (probesTotal > 0 ? (double)probesClamped / probesTotal : 0.0); }

private:
   S_CCEm_Member     M  [];
   S_CCEm_Member     Mt [];
   double            partner [];
   bool              gate    [];   // маска двигаемых координат
   int               curMember;
   int               filled;

   long              probesClamped;
   long              probesTotal;

   double            Alpha();
   bool              SameVector(const double &v1 [], const double &v2 []);
   void              MakeGate();
   void              MakeRandomVector(double &v []);
  };
//+------------------------------------------------------------------+

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

   ArrayResize(M,  councilSize);
   ArrayResize(Mt, councilSize);
   ArrayResize(partner, coords);
   ArrayResize(gate,    coords);

   for(int i = 0; i < councilSize; i++)
     {
      M  [i].Init(coords);
      Mt [i].Init(coords);
     }

   curMember     = 0;
   filled        = 0;
   probesClamped = 0;
   probesTotal   = 0;

   return true;
  }
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
double C_AO_CCEm::Alpha()
  {
   double rnd = u.RNDprobab();
   return (rnd < 0.5 ? rnd : -rnd);
  }
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
bool C_AO_CCEm::SameVector(const double &v1 [], const double &v2 [])
  {
   for(int c = 0; c < coords; c++)
      if(v1 [c] != v2 [c])
         return false;
   return true;
  }
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
void C_AO_CCEm::MakeRandomVector(double &v [])
  {
   for(int c = 0; c < coords; c++)
      v [c] = u.SeInDiSp(u.RNDfromCI(rangeMin [c], rangeMax [c]),
                         rangeMin [c], rangeMax [c], rangeStep [c]);
  }
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//|   MakeGate — маска двигаемых координат, общая для Y1..Y4         |
//+------------------------------------------------------------------+
void C_AO_CCEm::MakeGate()
  {
   int cnt = 0;

   for(int c = 0; c < coords; c++)
     {
      gate [c] = (u.RNDprobab() < moveProb);
      if(gate [c])
         cnt++;
     }

//--- ни одной координаты не выбрано - берём одну случайную,
//--- иначе четвёрка выродится в копию текущего решения
   if(cnt == 0)
      gate [u.RNDminusOne(coords)] = true;
  }
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//|                            Moving                                |
//|   Одна эпоха = один член совета.                                 |
//+------------------------------------------------------------------+
void C_AO_CCEm::Moving()
  {
//--- фаза инициализации: заполнение совета случайными решениями
   if(filled < councilSize)
     {
      for(int i = 0; i < popSize; i++)
         MakeRandomVector(a [i].c);
      return;
     }

   int m = curMember;
   int p;

//--- выбор партнёра X1
   if(m == 0)
     {
      //--- корень: партнёр — свежий случайный вектор
      MakeRandomVector(partner);
     }
   else
     {
      //--- родитель узла m в d-арном дереве. Гаусс-Зейдель: p < m всегда.
      p = (m - 1) / treeDegree;

      if(SameVector(M [p].c, M [m].c))
         MakeRandomVector(partner);          // нулевая разность недопустима
      else
         ArrayCopy(partner, M [p].c, 0, 0, coords);
     }

   MakeGate();

//--- четыре кандидата, alpha своя на каждую двигаемую координату
   double x1, x2, alpha, dif, raw;

   for(int c = 0; c < coords; c++)
     {
      if(!gate [c])
        {
         //--- координата не участвует в ходе
         for(int k = 0; k < 4; k++)
            a [k].c [c] = M [m].c [c];
         continue;
        }

      x1    = partner [c];      // X1 - родитель (или случайный партнёр)
      x2    = M [m].c [c];      // X2 - сам член совета
      alpha = Alpha();
      dif   = x1 - x2;

      for(int k = 0; k < 4; k++)
        {
         switch(k)
           {
            case 0:
               raw = x2 + alpha         * dif;
               break;  // к x1
            case 1:
               raw = x2 + (1.0 + alpha) * dif;
               break;  // за x1
            case 2:
               raw = x1 - alpha         * dif;
               break;  // к x2
            default:
               raw = x1 - (1.0 + alpha) * dif;
               break;  // за x2
           }

         probesTotal++;
         if(raw < rangeMin [c] || raw > rangeMax [c])
            probesClamped++;

         a [k].c [c] = u.SeInDiSp(raw, rangeMin [c], rangeMax [c], rangeStep [c]);
        }
     }
  }
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//|                           Revision                               |
//+------------------------------------------------------------------+
void C_AO_CCEm::Revision()
  {
//--- фаза инициализации
   if(filled < councilSize)
     {
      int order [];
      ArrayResize(order, popSize);
      for(int i = 0; i < popSize; i++)
         order [i] = i;

      for(int i = 0; i < popSize - 1; i++)
         for(int j = i + 1; j < popSize; j++)
            if(a [order [j]].f > a [order [i]].f)
              {
               int t = order [i];
               order [i] = order [j];
               order [j] = t;
              }

      for(int i = 0; i < popSize && filled < councilSize; i++, filled++)
        {
         M [filled].f = a [order [i]].f;
         ArrayCopy(M [filled].c, a [order [i]].c, 0, 0, coords);
        }

      if(filled >= councilSize)
        {
         u.Sorting(M, Mt, councilSize);
         curMember = 0;
        }

      if(M [0].f > fB)
        {
         fB = M [0].f;
         ArrayCopy(cB, M [0].c, 0, 0, coords);
        }

      revision = true;
      return;
     }

//--- лучший из четвёрки, жадный приём
   int    bestIdx = 0;
   double bestF   = a [0].f;

   for(int k = 1; k < 4; k++)
     {
      if(a [k].f > bestF)
        {
         bestF   = a [k].f;
         bestIdx = k;
        }
     }

   int m = curMember;

   if(bestF > M [m].f)
     {
      M [m].f = bestF;
      ArrayCopy(M [m].c, a [bestIdx].c, 0, 0, coords);
     }

   if(M [m].f > fB)
     {
      fB = M [m].f;
      ArrayCopy(cB, M [m].c, 0, 0, coords);
     }

//--- следующий член совета; после полного обхода — перестройка дерева
   curMember++;
   if(curMember >= councilSize)
     {
      curMember = 0;
      u.Sorting(M, Mt, councilSize);
     }
  }
//+------------------------------------------------------------------+



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

Для таблицы взят совет 50: композит тот же, а значение совпадает с дефолтным popSize серии.

CCEm|City Councils Evolution M|4.0|50.0|3.0|1000.0|
=============================
5 Hilly's; Func runs: 10000; result: 0.7801904525720962
25 Hilly's; Func runs: 10000; result: 0.5674856077456875
500 Hilly's; Func runs: 10000; result: 0.2557411330267817
=============================
5 Forest's; Func runs: 10000; result: 0.934390650619403
25 Forest's; Func runs: 10000; result: 0.6492739753440023
500 Forest's; Func runs: 10000; result: 0.1171451679865052
=============================
5 Megacity's; Func runs: 10000; result: 0.8173333333333334
25 Megacity's; Func runs: 10000; result: 0.4378666666666667
500 Megacity's; Func runs: 10000; result: 0.10876000000000094
=============================
All score: 4.66819 (51.87%)

Forest 5 вырос с 0.362 до 0.934. Диагноз про диагональный гребень подтвердился радикально. Forest 25 поднялся с 0.175 до 0.649.

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

Hilly

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

Forest

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

Megacity

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

Ackley

CCEm на тестовой функции Ackley

Shaffer

CCEm на тестовой функции Shaffer

По результатам тестирования алгоритм CCEm занимает 44-е место в рейтинговой таблице.

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 CCEm city_councils_evolution_M_ 0,78019 0,56748 0,25574 1,60341 0,93439 0,64927 0,11714 1,70080 0,81733 0,43786 0,10876 1,36395 4,668 51,87
45 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
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



Выводы

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

CCEm|City Councils Evolution M|4.0|20.0|3.0|0.5|
=============================
5 Hilly's; Func runs: 10000; result: 0.9234158594854092
25 Hilly's; Func runs: 10000; result: 0.6662193087032339
500 Hilly's; Func runs: 10000; result: 0.29003867410771245
=============================
5 Forest's; Func runs: 10000; result: 0.9792448015016874
25 Forest's; Func runs: 10000; result: 0.7161320797886964
500 Forest's; Func runs: 10000; result: 0.15173332606789042
=============================
5 Megacity's; Func runs: 10000; result: 0.9440000000000002
25 Megacity's; Func runs: 10000; result: 0.5786666666666667
500 Megacity's; Func runs: 10000; result: 0.15142666666666718
=============================
All score: 5.40088 (60.01%)

Оговорка о методике. Значение 20 для совета и 0.5 для вероятности получены перебором по сетке, а не выведены из каких-либо соображений, и перебор этот не был исчерпывающим — оси взаимодействуют между собой, так что найденная точка почти наверняка не абсолютный максимум, а лишь лучшая из проверенных. Все результаты получены при 10 повторах; разброс составляет несколько пунктов (по логам композитного теста).

Работа над CCE оказалась в равной мере разбором алгоритма и разбором того, как алгоритм подан.

Метафора не соответствует коду. Ни дерева советов, ни выборов, ни продвижения по иерархии в реализации нет. Каждое из пяти заявленных свойств проверено экспериментально и не подтвердилось.

Степень дерева d мы меняли с двух на три при неизменном всём остальном — результат сдвинулся на 0.15 пункта, то есть меньше, чем гуляют два одинаковых прогона между собой. Значит, форма дерева на поведение не влияет вовсе. Размер совета мы уменьшали от тридцати до двух, и композит при этом не падал, а рос — с 32.29% до 41.15%. Популяция, которая должна была бы помогать, на деле только отнимала бюджет у полезной работы: чем меньше решений алгоритм держит одновременно, тем больше проходов по координатам успевает сделать. Что остаётся после всех этих проверок — покоординатный спуск с четырьмя пробами на координату. Простой локальный поиск без какой-либо иерархии.

Оператор не инвариантен к переносу. Мы сдвигали область поиска, не трогая при этом сам ландшафт: функции те же, оптимумы на тех же местах относительно рельефа, сложность задачи не изменилась ни на йоту. Изменилось только то, где находится точка ноль относительно области. Канон при этом потерял 13 пунктов на основном стенде (с 41.15% до 27.98%) и 24 пункта на композитном.

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

Правка оператора поднимает результат с 41% до 60%. Мы заменили сложение координат на их разность, разрешили менять все координаты за один ход вместо одной и добавили третьим параметром вероятность участия координаты в ходе. Лучшая найденная конфигурация — совет из двадцати членов при вероятности одна вторая — даёт 60.01% композита против 41.15% у канона.

Разберём, что именно улучшилось. Forest в десятимерной ячейке поднялся с 0.362 до 0.934, почти втрое — это функция с диагональными гребнями, вдоль которых покоординатный спуск ходить не умеет в принципе, а векторный ход умеет. Зависимость от системы координат исчезла, не стала меньше, а исчезла полностью и притом по построению: разность двух решений при переносе осей не меняется, значит и шаг не меняется. Это свойство, доказуемое на бумаге, а прогон со сдвигом служил лишь проверкой того, что код написан верно.

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

tab

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

chart

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


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

Плюсы:

  • Уверенно работает на овражных функциях.
  • Существенно лучше оригинала в высокой размерности на нецентрированных задачах.
  • Совет в CCEm впервые начинает приносить пользу — оптимум внутренний, а не на границе.

    Минусы:

    • Существенных недостатков не выявлено.


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


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

      # Имя Тип Описание
      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_CCEm.mq5
      Скрипт Испытательный стенд для CCEm
      Прикрепленные файлы |
      CCEm.zip (452.5 KB)
      Машинное обучение и Data Science (Часть 47): Прогнозирование рынка с помощью модели DeepAR на Python Машинное обучение и Data Science (Часть 47): Прогнозирование рынка с помощью модели DeepAR на Python
      В этой статье мы попробуем спрогнозировать движения рынка с помощью достаточно хорошей модели прогнозирования временных рядов под названием DeepAR. Модель представляет собой сочетание глубоких нейронных сетей и авторегрессионных свойств, характерных для таких моделей, как ARIMA и VAR (векторная авторегрессионная модель).
      Нейросети в трейдинге: Адаптация прогноза при смене рыночного режима (Объект верхнего уровня) Нейросети в трейдинге: Адаптация прогноза при смене рыночного режима (Объект верхнего уровня)
      Представлен объект верхнего уровня CNeuronOMPBOCL для адаптации рыночного представления внутри энкодера. Он объединяет байесовскую остаточную поправку, групповой затвор, раздельные истории Source и Target, оценку Mismatch и KL-ограничение. Разобраны инициализация, режимы работы, числовые проверки, прямой и обратный проходы в MQL5/OpenCL. Реализация разделяет прогноз и последующее обновление параметров и даёт готовый слой для дальнейшей интеграции в модель.
      База данных — это просто (Часть 1): Легковесный ORM-фреймворк для MQL5 на базе SQLite База данных — это просто (Часть 1): Легковесный ORM-фреймворк для MQL5 на базе SQLite
      В данной статье представлен структурированный подход к управлению данными SQLite в MQL5 с помощью ORM-слоя для MetaTrader 5. В нем представлены базовые классы для моделирования сущностей и доступа к базе данных, fluent API для CRUD, хуки рефлексии для OnGet/OnSet, а также макросы для быстрого определения моделей. В практических примерах кода показано создание таблиц, привязка полей, вставка, обновление, выборка и удаление записей. Разработчики получают готовые к повторному использованию типобезопасные компоненты, которые сводят к минимуму однотипный SQL-код.
      Алгоритм оптимизации на основе кровеносной системы — Circulatory System Based Optimization (CSBO) Алгоритм оптимизации на основе кровеносной системы — Circulatory System Based Optimization (CSBO)
      CSBO — алгоритм, вдохновлённый кровеносной системой, рассматривается как разностно-ориентированный метод: венозное движение, систематический круг с кроссовером и лёгочная мутация. Приведена реализация в виде класса C_AO_CSBO для MQL5 с переводом в максимизацию, нормировкой шагов и двухфазными эпохами. Представлены итоги, исходники и практические настройки.