preview
Алгоритм бактериальной эволюции — Bacterial Evolutionary Algorithm (BEA)

Алгоритм бактериальной эволюции — Bacterial Evolutionary Algorithm (BEA)

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

Содержание

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


Введение

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

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

Здесь, однако, нас поджидает терминологическая развилка, которую важно прояснить с самого начала, потому что под названием Bacterial Evolutionary Algorithm в литературе живут два совершенно разных алгоритма, и их регулярно путают. Первый восходит к работе Тисато Нумаоки 1996 года Bacterial Evolution Algorithm for Rapid Adaptation: это, строго говоря, не оптимизатор непрерывных функций, а агентная модель из области искусственной жизни. В ней колония адаптивных агентов-бактероидов приспосабливается к постоянно меняющейся среде, причём отбор осуществляет сама среда через гибель агентов, в общем случае безотносительно к их собственной функции приспособленности. Модель красива и посвящена быстрой ре-адаптации к подвижному оптимуму, но её перенос на статический стенд требует столь вольной интерпретации, что получается, по сути, другой алгоритм. Этой ветви — настоящей модели Нумаоки — будет посвящена отдельная статья, потому что её природа принципиально иная.

Второй алгоритм — тот, что в контексте оптимизации все называют BEA, — предложен Норберто Нава и Такеси Фурухаси в 1999 году. Из этой ветви позже выросли бактериальный меметический алгоритм и его дискретные варианты для задачи коммивояжёра. Именно этот оптимизационный вариант BEA Нава — Фурухаси мы и реализуем в нашем фреймворке C_AO. Проверим на тестовых функциях Hilly, Forest и Megacity, прогоним через композитный античит-тест и разберём, как алгоритм ведёт себя при росте размерности задачи. Историческую модель Нумаоки оставим на потом, обозначив её здесь лишь как развилку, к которой мы ещё вернёмся.


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

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

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

chromosome_segments

Рисунок 1. Хромосома бактерии — это вектор координат в пространстве поиска, мысленно разбитый на сегменты (непрерывные блоки координат)

Бактерия как особь популяции из popSize и посегментная организация хромосомы — отображено на рисунке выше.

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

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

bacterial_mutation

Рисунок 2. Бактериальная мутация как локальная оптимизация по сегментам.

Смотрим на рисунок 2. С бактерии снимаются Nc клонов (1); в выбранном сегменте координаты случайно перегенерируются во всех клонах, кроме одного, который сохраняется неизменным и удерживает текущий инкумбент; все клоны оцениваются (2); вариант сегмента лучшего клона распространяется на все клоны (3). Поскольку инкумбент всегда присутствует в наборе сравниваемых, итоговая бактерия никогда не оказывается хуже исходной — приспособленность в ходе мутации монотонно не убывает. Процедура повторяется для каждого сегмента.

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

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

gene_transfer

Рисунок 3. Перенос генов как горизонтальный обмен.

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

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

инициализировать популяцию случайно
оценить популяцию
пока не исчерпан бюджет:

    # --- бактериальная мутация (локальная оптимизация каждой особи) ---
    для каждой бактерии b:
        создать Nc клонов бактерии b
        для каждого сегмента хромосомы:
            во всех клонах, кроме одного, перегенерировать сегмент случайно
            оценить все клоны
            лучший вариант сегмента распространить на все клоны
        b <- лучший клон               # инкумбент в наборе => b не ухудшается

    # --- перенос генов (горизонтальный обмен блоками) ---
    отсортировать популяцию; разделить на сильную и слабую половины
    повторить Ninf раз:
        источник  <- случайная бактерия из сильной половины
        приёмник  <- случайная бактерия из слабой половины
        скопировать блок координат из источника в приёмник

    обновить глобально лучшее решение

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

Все алгоритмы серии живут в едином каркасе C_AO, и контракт его предельно прост. На каждой эпохе внешний цикл стенда вызывает метод Moving, который заполняет координаты всех агентов популяции — массив a[i].c; затем стенд сам оценивает каждого агента функцией приспособленности, записывая результат в a[i].f. Затем вызывается Revision, который читает эти оценки и обновляет внутреннее состояние алгоритма. Ключевое следствие этого контракта в том, что за одну эпоху функция приспособленности вызывается ровно popSize раз — по одному разу на агента, и ни больше, ни меньше.

Здесь и кроется трудность, обещанная в конце предыдущего раздела. Бактериальная мутация по своей природе требует вложенных оценок: внутри обработки одной бактерии нужно оценить её клоны на текущем сегменте, узнать победителя и лишь затем перейти к следующему сегменту. Это не укладывается в схему один пакет из popSize оценок на поколение и на одно поколение BEA приходится множество таких пакетов. Поэтому класс реализован как конечный автомат: каждая эпоха стенда соответствует одному срезу оценки, а переменные состояния помнят, на каком сегменте, клоне или акте переноса мы сейчас находимся. За одну эпоху оцениваются либо одноимённые клоны всех бактерий сразу, либо все бактерии после инфицирования — и популяционный слот a[i].c каждую эпоху занят осмысленным кандидатом.

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

//+----------------------------------------------------------------------------+
//|                           Structure                                        |
//+----------------------------------------------------------------------------+
struct S_BEA_Ind
 {
  double c           [];   // координаты (валидные, уже прогнаны через SeInDiSp)
  double             f;      // фитнес

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

На уровне класса хранятся три массива. Первый, bact[] размера popSize, — это закреплённые бактерии, текущая популяция. Второй, clone[] размера popSize * Nc, — рабочие клоны: для каждой бактерии в нём отведено Nc подряд идущих ячеек, и клон номер j бактерии b лежит по индексу b * Nc + j. Третий, ord[] размера popSize, — вспомогательный массив индексов для сортировки при переносе генов. Переменные phase, segIdx, cloneIdx и transIdx образуют состояние автомата: фаза (мутация или перенос), текущий сегмент, текущий клон-срез и текущая эпоха переноса.

//+------------------------------------------------------------------+
//|                          Class                                   |
//+------------------------------------------------------------------+
class C_AO_BEA : public C_AO
 {
public:
                    ~C_AO_BEA() {}
                     C_AO_BEA()
   {
    ao_name = "BEA";
    ao_desc = "Bacterial Evolutionary Algorithm";
    ao_link = "https://www.mql5.com/ru/users/joo";

    popSize   = 10;     // число бактерий
    Nc        = 2;      // свежих мутантов сегмента на бактерию (>=1)
    Nseg      = 10;     // число сегментов хромосомы
    nTrans    = 2;      // эпох переноса генов на поколение
    transFrac = 0.2;    // длина переносимого блока как доля coords (0..1]

    ArrayResize(params, 5);
    params [0].name = "popSize";
    params [0].val = popSize;
    params [1].name = "Nc";
    params [1].val = Nc;
    params [2].name = "Nseg";
    params [2].val = Nseg;
    params [3].name = "nTrans";
    params [3].val = nTrans;
    params [4].name = "transFrac";
    params [4].val = transFrac;
   }

  void               SetParams()
   {
    popSize   = (int)params [0].val;
    Nc        = (int)params [1].val;
    Nseg      = (int)params [2].val;
    nTrans    = (int)params [3].val;
    transFrac =      params [4].val;

    //--- предохранители
    if(popSize   < 1)
      popSize   = 1;
    if(Nc        < 1)
      Nc        = 1;     // число свежих мутантов на сегмент
    if(Nseg      < 1)
      Nseg      = 1;
    if(nTrans    < 0)
      nTrans    = 0;
    if(transFrac <= 0.0)
      transFrac = 0.01;
    if(transFrac >  1.0)
      transFrac = 1.0;
   }

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

  void               Moving();
  void               Revision();

  //--- видимые параметры
  int                Nc;          // клонов
  int                Nseg;        // сегментов
  int                nTrans;      // эпох переноса генов
  double             transFrac;   // доля coords для блока переноса

private:
  //--- фазы конечного автомата
  enum E_Phase { PH_MUT = 0, PH_TRANS = 1 };
  E_Phase            phase;
  int                segIdx;      // текущий сегмент в мутации
  int                cloneIdx;    // текущий клон (срез оценки) в мутации
  int                transIdx;    // текущая эпоха переноса

  //--- данные (массивы структур)
  S_BEA_Ind          bact  [];    // [popSize]       — закреплённые бактерии
  S_BEA_Ind          clone [];    // [popSize * Nc]  — рабочие клоны
  int                ord   [];    // [popSize]       — индексы для сортировки

  int                transLen;    // длина блока переноса (из transFrac)

  //--- вспомогательные
  void               SegBounds(int s, int &lo, int &hi);
  void               BuildClonesForSeg(int s);
  void               DoGeneTransfer();
 };

Метод Init после стандартной инициализации каркаса делает три вещи. Он ограничивает число сегментов сверху размерностью задачи (Nseg не может быть больше coords), вычисляет абсолютную длину переносимого блока из доли transFrac. Это важная деталь, поскольку параметры класса фиксированы на всю тройку тестов, а размерность от теста к тесту меняется, и привязка длины блока к доле координат делает её масштабируемой. Затем создаются массивы структур и стартовая популяция заполняется равномерно случайными координатами, после чего состояние автомата сбрасывается в начало фазы мутации.

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

//--- сегментов не больше, чем координат
  if(Nseg > coords)
    Nseg = coords;

//--- длина блока переноса
  transLen = (int)MathRound(transFrac * coords);
  if(transLen < 1)
    transLen = 1;
  if(transLen > coords)
    transLen = coords;

//--- буферы (массивы структур)
  ArrayResize(bact,  popSize);
  ArrayResize(clone, popSize * Nc);
  ArrayResize(ord,   popSize);

  for(int i = 0; i < popSize;      i++)
    bact  [i].Init(coords);
  for(int i = 0; i < popSize * Nc; i++)
    clone [i].Init(coords);

//--- стартовая случайная популяция бактерий
  for(int i = 0; i < popSize; i++)
   {
    for(int c = 0; c < coords; c++)
      bact [i].c [c] = u.SeInDiSp(u.RNDfromCI(rangeMin [c], rangeMax [c]),
                                  rangeMin [c], rangeMax [c], rangeStep [c]);
    bact [i].f = -DBL_MAX;
   }

//--- стартовое состояние автомата
  phase    = PH_MUT;
  segIdx   = 0;
  cloneIdx = 0;
  transIdx = 0;

  return true;
 }

Сегментация хромосомы. Границы сегмента вычисляются так, чтобы остаток координат равномерно распределялся между сегментами:

//+------------------------------------------------------------------+
//|   SegBounds — границы сегмента s в [lo, hi) с равномерным        |
//|   распределением остатка координат по сегментам                  |
//+------------------------------------------------------------------+
void C_AO_BEA::SegBounds(int s, int &lo, int &hi)
 {
  lo = (int)((long)s       * coords / Nseg);
  hi = (int)((long)(s + 1) * coords / Nseg);
  if(lo < 0)
    lo = 0;
  if(hi > coords)
    hi = coords;
 }

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

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

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

//+------------------------------------------------------------------+
//|   BuildClonesForSeg — снять с каждой бактерии Nc клонов и во     |
//|   ВСЕХ клонах случайно регенерировать координаты сегмента s.     |
//|   Инкумбент не хранится отдельным клоном: его фитнес помнится в  |
//|   bact.f и сравнивается при закреплении (см. Revision).          |
//+------------------------------------------------------------------+
void C_AO_BEA::BuildClonesForSeg(int s)
 {
  int lo, hi;
  SegBounds(s, lo, hi);

  for(int b = 0; b < popSize; b++)
   {
    for(int j = 0; j < Nc; j++)
     {
      int idx = b * Nc + j;

      //--- копия бактерии вне сегмента
      for(int c = 0; c < coords; c++)
        clone [idx].c [c] = bact [b].c [c];

      //--- регенерация сегмента во ВСЕХ клонах. Инкумбент хранится
      //    отдельно (bact.f) и сравнивается на этапе закрепления —
      //    переоценивать его клоном не нужно, экономим вызовы FF.
      for(int c = lo; c < hi; c++)
        clone [idx].c [c] = u.SeInDiSp(u.RNDfromCI(rangeMin [c], rangeMax [c]),
                                       rangeMin [c], rangeMax [c], rangeStep [c]);

      clone [idx].f = -DBL_MAX;
     }
   }
 }

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

//+------------------------------------------------------------------+
//|   DoGeneTransfer — сортировка популяции и инфицирование слабой   |
//|   половины блоками из сильной. Модифицирует bact[] на месте.     |
//+------------------------------------------------------------------+
void C_AO_BEA::DoGeneTransfer()
 {
  if(popSize < 2)
    return;

//--- сортировка индексов по фитнесу бактерий (по убыванию)
  for(int i = 0; i < popSize; i++)
    ord [i] = i;

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

  int half = popSize / 2;
  if(half < 1)
    half = 1;

  int span = coords - transLen + 1;
  if(span < 1)
    span = 1;

//--- каждый приёмник «слабой» половины принимает блок от случайного
//    источника «сильной» половины
  for(int k = half; k < popSize; k++)
   {
    int dst   = ord [k];
    int src   = ord [u.RNDminusOne(half)];
    int start = u.RNDminusOne(span);

    for(int t = 0; t < transLen; t++)
     {
      int c = start + t;
      if(c < coords)
        bact [dst].c [c] = bact [src].c [c];
     }
   }
 }

Приёмка перенесённых генотипов не жадная (non-greedy), как и в литературе: инфицированная бактерия принимается со своей новой приспособленностью независимо от того, стала ли она лучше или хуже. Это намеренно — перенос призван перемешивать материал, а не только улучшать, и временное ухудшение отдельной бактерии здесь допустимо. Глобально лучшее решение при этом не страдает: в Revision оно сохраняется отдельно в cB / fB сразу после оценки каждого слота, поэтому даже неудачный перенос не способен потерять найденный оптимум — за это отвечает элитизм каркаса.

Сама фаза распределена по эпохам через автомат. В Moving на старте сегмента (когда cloneIdx равен нулю) строятся все клоны текущего сегмента, после чего каждую эпоху в популяционные слоты загружается один клон-срез — клон с номером cloneIdx у всех бактерий сразу:

//+------------------------------------------------------------------+
//|                            Moving                                |
//|   Заполняет слоты a[i].c кандидатами для оценки этой эпохи.      |
//+------------------------------------------------------------------+
void C_AO_BEA::Moving()
 {
//--- первый прогон: бактерии -> слоты для первичной оценки FF
  if(!revision)
   {
    for(int i = 0; i < popSize; i++)
      for(int c = 0; c < coords; c++)
        a [i].c [c] = u.SeInDiSp(bact [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]);
    return;
   }

  if(phase == PH_MUT)
   {
    //--- на старте сегмента строим клоны
    if(cloneIdx == 0)
      BuildClonesForSeg(segIdx);

    //--- грузим текущий клон каждой бактерии в слоты
    for(int b = 0; b < popSize; b++)
     {
      int idx = b * Nc + cloneIdx;
      for(int c = 0; c < coords; c++)
        a [b].c [c] = clone [idx].c [c];
     }
   }
  else // PH_TRANS
   {
    //--- инфицируем слабую половину и грузим бактерии в слоты
    DoGeneTransfer();

    for(int b = 0; b < popSize; b++)
      for(int c = 0; c < coords; c++)
        a [b].c [c] = bact [b].c [c];
   }
 }

Честный учёт обращений к функции приспособленности. Теперь видно, почему фазовая организация так важна для корректного сравнения. Стоимость одного поколения BEA в обращениях к функции приспособленности складывается из двух слагаемых: мутация требует Nseg * Nc * popSize оценок (каждый из Nseg сегментов перебирает Nc клонов у всех popSize бактерий), перенос — nTrans * popSize. Принципиально, что ни то, ни другое не зависит от размерности задачи: при coords, равном десяти или тысяче, поколение стоит одинаково. Благодаря этому BEA расходует общий бюджет в десять тысяч вычислений по тем же правилам, что и остальные алгоритмы серии, и сравнение на фиксированном бюджете остаётся честным — никакой алгоритм не получает скрытой форы за счёт того, что его «поколение» на самом деле прячет в себе разное число оценок.

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

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

//+------------------------------------------------------------------+
//|                           Revision                               |
//|   Считывает a[i].f, обновляет cB/fB и продвигает автомат.        |
//+------------------------------------------------------------------+
void C_AO_BEA::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)
   {
    for(int i = 0; i < popSize; i++)
      bact [i].f = a [i].f;

    revision = true;
    phase    = PH_MUT;
    segIdx   = 0;
    cloneIdx = 0;
    transIdx = 0;
    return;
   }

  if(phase == PH_MUT)
   {
    //--- сохраняем фитнес оценённого среза клонов
    for(int b = 0; b < popSize; b++)
      clone [b * Nc + cloneIdx].f = a [b].f;

    cloneIdx++;

    //--- сегмент полностью оценён -> закрепляем лучший клон
    if(cloneIdx >= Nc)
     {
      for(int b = 0; b < popSize; b++)
       {
        int    best = 0;
        double bf   = clone [b * Nc + 0].f;
        for(int j = 1; j < Nc; j++)
         {
          double cf = clone [b * Nc + j].f;
          if(cf > bf)
           {
            bf = cf;
            best = j;
           }
         }

        //--- закрепляем лучший мутант ТОЛЬКО если он превзошёл
        //    инкумбента; иначе бактерия остаётся прежней (монотонность
        //    мутации сохраняется без переоценки инкумбента клоном)
        if(bf > bact [b].f)
         {
          int bi = b * Nc + best;
          for(int c = 0; c < coords; c++)
            bact [b].c [c] = clone [bi].c [c];
          bact [b].f = bf;
         }
       }

      cloneIdx = 0;
      segIdx++;

      //--- все сегменты пройдены -> перенос генов (или новое поколение)
      if(segIdx >= Nseg)
       {
        segIdx = 0;
        if(nTrans > 0)
         {
          phase = PH_TRANS;
          transIdx = 0;
         }
        else
          phase = PH_MUT;
       }
     }
   }
  else // PH_TRANS
   {
    //--- принимаем инфицированные генотипы (не-greedy)
    for(int b = 0; b < popSize; b++)
      bact [b].f = a [b].f;

    transIdx++;
    if(transIdx >= nTrans)
     {
      phase    = PH_MUT;
      segIdx   = 0;
      cloneIdx = 0;
     }
   }
 }

Важно понимать, что это не косметическая оптимизация, а содержательное улучшение. При Nc = 2 каноническая схема даёт на сегмент одного неизменного клона и одного мутанта, то есть проверяет всего один новый вариант. Наша схема при той же стоимости в два вызова функции даёт двух мутантов. Освободившийся от переоценки инкумбента слот занимается лишним свежим кандидатом, и именно это в наших прогонах подняло итоговый результат базовой конфигурации примерно с пятидесяти до пятидесяти трёх процентов. При этом монотонность мутации полностью сохраняется: условие if (bf > bact.f) означает, что бактерия либо улучшается, либо остаётся на месте, но никогда не деградирует — ровно то свойство, ради которого в каноне держали неизменный клон.

Параметры. Видимых параметров у класса пять. Размер популяции popSize — число бактерий. Число клонов Nc — сколько свежих мутантов проверяется на каждом сегменте. Число сегментов Nseg определяет крупность поблочной мутации. Число эпох переноса nTrans задаёт интенсивность горизонтального обмена за поколение. Наконец, доля transFrac определяет длину переносимого блока относительно размерности задачи. Их взаимодействие с бюджетом — а именно размен между качеством одного поколения и числом поколений, укладывающихся в фиксированный бюджет, — мы разберём при анализе результатов тестирования.


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

На общем счёте наших тестовых функций алгоритм набрал 53.13% при конфигурации с популяцией в 10 особей.

BEA|Bacterial Evolutionary Algorithm|10.0|2.0|10.0|2.0|0.2|
=============================
5 Hilly's; Func runs: 10000; result: 0.9217077440618244
25 Hilly's; Func runs: 10000; result: 0.5961506596554828
500 Hilly's; Func runs: 10000; result: 0.2934090356286264
=============================
5 Forest's; Func runs: 10000; result: 0.9690648482875481
25 Forest's; Func runs: 10000; result: 0.44500075828106833
500 Forest's; Func runs: 10000; result: 0.08233566231751262
=============================
5 Megacity's; Func runs: 10000; result: 0.9053333333333334
25 Megacity's; Func runs: 10000; result: 0.4317333333333332
500 Megacity's; Func runs: 10000; result: 0.13676000000000088
=============================
All score: 4.78150 (53.13%)

Античит-тест указывает на честные показатели поисковой способности алгоритма.

BEA|Bacterial Evolutionary Algorithm|10.0|2.0|10.0|2.0|0.2|
=============================
Composite anti-cheat test: Hilly + Forest + Megacity + Peaks + Skin
Coordinates: 10; Epochs: 1000; Repeats: 10
=============================
Run 1/10: 0.9468505530003426
Run 2/10: 0.9621805930887799
Run 3/10: 0.8517460551795464
Run 4/10: 0.968005214717207
Run 5/10: 0.8697467385084419
Run 6/10: 0.9729983388567323
Run 7/10: 0.9703386568386824
Run 8/10: 0.9663920708043637
Run 9/10: 0.9545824368636746
Run 10/10: 0.8336853637354364
=============================
Average result: 0.9296526022 (92.97%)
=============================

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

Hilly

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

Forest

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

Megacity

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

Peaks

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

Ackley

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

По результатам тестирования алгоритм занимает 37-е место в общем рейтинге лучших популяционных методов оптимизации.

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 differential evolution 0,96398 0,62346 0,26089 1,84833 0,98482 0,77018 0,11459 1,86959 0,93067 0,36213 0,11000 1,40280 5,121 56,90
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 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
31 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
32 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
33 ABO african buffalo optimization 0,92295 0,62528 0,29885 1,84708 0,92992 0,57468 0,09372 1,59832 0,73333 0,51333 0,14324 1,38990 4,835 53,72
34 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
35 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
36 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
37 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
38 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
39 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
40 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
41 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
42 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
43 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
44 BSO brain storm optimization 0,92207 0,57625 0,29732 1,79564 0,80764 0,42508 0,09448 1,32720 0,77200 0,36533 0,13065 1,26798 4,391 48,79
45 CAm camel algorithm M 0,71534 0,56917 0,35985 1,64436 0,84094 0,47174 0,10850 1,42118 0,70400 0,41947 0,19563 1,31910 4,385 48,72
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


Выводы

Бактериальный эволюционный алгоритм в версии Нава — Фурухаси оказался алгоритмом с ясным и честным характером — без скрытых геометрических уловок и с предсказуемым поведением, причины которого прозрачны. На общем счёте по тройке тестовых функций он набрал 53.13% при конфигурации с малой популяцией, и это число стоит читать не как одну цифру, а как сумму двух очень разных историй по разным размерностям.

В малой размерности BEA силён. На десяти координатах он показывает результаты верхней трети таблицы — порядка 0.92 на Hilly, 0.97 на Forest, 0.91 на Megacity. Объяснение лежит в самой природе бактериальной мутации: когда сегмент состоит из одной-двух координат, посегментная регенерация превращается в почти покоординатный глобальный поиск, который на мультимодальных функциях работает отлично, выбрасывая координату по всему диапазону и оставляя лучшее. Перенос генов добавляет к этому быстрый обмен удачными блоками, и вместе они дают сильную сходимость.

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

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

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

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

tab

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

chart


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


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

Плюсы:

  1. Хорошие поисковые качества.
  2. Стабильность результатов (малый разброс).

Минусы:

  1. Большое количество внешних параметров.

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



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

# Имя Тип Описание
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_BEA.mq5
Скрипт Испытательный стенд для BEA

Прикрепленные файлы |
BEA.zip (437.17 KB)
Неопределённость как модель (Часть 7): Стохастические регрессоры Неопределённость как модель (Часть 7): Стохастические регрессоры
Материал — практическое руководство по множественной регрессии со стохастическими факторами в алготрейдинге с акцентом на эндогенность. Показаны EDA/CDA-приёмы, тест Дарбина—Ву—Хаусмана и двухэтапный МНК для получения состоятельных коэффициентов. Все расчёты реализованы в MQL5 и проверены на внутридневной волатильности EURUSD. Читатель получит рабочий конвейер для выявления эндогенности и корректной спецификации моделей.
MQL5 Bootstrap (I): Набор переиспользуемых функций для работы с позициями и ордерами MQL5 Bootstrap (I): Набор переиспользуемых функций для работы с позициями и ордерами
В статье представлен компактный вспомогательный слой MQL5 для типовых торговых операций. Он включает функции проверки наличия позиций, счётчики позиций, вспомогательные функции для группового закрытия позиций, а также функции получения наиболее недавно открытой или самой старой позиции по символу, magic-номеру или типу. Интеграция демонстрируется на простом советнике с пересечением SMA. В результате советники становятся чище, уменьшается количество различий в реализации между проектами и упрощается сопровождение кода.
Нейросети в трейдинге: Когнитивная инерция в анализе финансовых рынков (Окончание) Нейросети в трейдинге: Когнитивная инерция в анализе финансовых рынков (Окончание)
Лучше всего здесь работает сочетание вариантов 2 и 4: оно сохраняет техническую точность, не обещает доходность и сразу показывает практический смысл архитектуры. В статье завершается адаптация фреймворка CogDriver к анализу финансовых рынков. Представление рыночной сцены, временная память, прогнозный план и оценка ожидаемой ошибки объединяются в единый торговый контур, при этом прогнозирование отделено от принятия решений. Рассматриваются построение моделей, организация обучения и проверка архитектуры в тестере стратегий MetaTrader 5 с акцентом на снижение избыточной реактивности и дрожания торговых решений.
Создание пользовательских индикаторов в MQL5 (Часть 5): Доработка WaveTrend Crossover с помощью Canvas — градиентный туман, сигнальные пузырьки и управление рисками Создание пользовательских индикаторов в MQL5 (Часть 5): Доработка WaveTrend Crossover с помощью Canvas — градиентный туман, сигнальные пузырьки и управление рисками
В этой статье мы улучшим индикатор Smart WaveTrend Crossover в MQL5, добавив отрисовку с использованием Canvas для градиентных туманных наложений, сигнальные боксы для обнаружения пробоев, а также метки сигналов в виде пузырьков с надписями BUY/SELL или простых треугольников. Мы также реализуем функции управления рисками с динамическими уровнями Take Profit и Stop Loss, рассчитываемыми через множители свечей или проценты и отображаемыми с помощью линий и таблицы, а также добавим параметры фильтрации по тренду и расширения сигнальных боксов.