Русский Español Deutsch 日本語 Português
preview
Covariance Matrix Adaptation Evolution Strategy (CMA-ES)

Covariance Matrix Adaptation Evolution Strategy (CMA-ES)

MetaTrader 5Trading |
1 491 0
Andrey Dik
Andrey Dik

Contents

  1. Introduction
  2. Implementation of the algorithm
  3. Test results


Introduction

There are many algorithms in the world of optimization, but we are looking for the most powerful ones to solve the optimization problems of our trading robots. CMA-ES (Covariance Matrix Adaptation Evolution Strategy) is one of those rare examples where mathematical rigor is combined with biological intuition, creating an algorithm that not only solves optimization problems, but also learns to understand their structure.

The history of CMA-ES began in the late 1990s in research laboratories in Germany, where Nikolaus Hansen and Andreas Ostermeier posed a fundamental question: Is it possible to create an optimization algorithm that does not simply search for a solution, but adapts to the geometry of the problem? Traditional evolutionary algorithms generated offspring in spherical regions around parent individuals, which worked well for simple functions but proved ineffective for complex, ill-conditioned problems. Let's take a look at this interesting algorithm.


Implementation of the algorithm

Imagine searching for treasure on an irregularly shaped island. The usual approach is to search in all directions equally, as if the island were round. CMA-ES gradually learns the shape of the island and gradually shifts its search toward directions where the likelihood of finding treasure is higher. Moreover, it remembers successful routes and uses this memory to plan future searches.

CMA-ES is based on a deceptively simple equation: x_k ~ N(m, σ²C). But behind this simplicity lies a deep mathematical structure. Each symbol here carries important information about the state of the search: m is the current best guess about the location of the optimum, σ is a measure of how far we are willing to risk moving away from the known, while C is a covariance matrix that encodes our understanding of the function geometry. The only change we can justify is replacing the normal distribution with a power distribution, which means that the implementation will follow a modified equation: x_k ~ PowerDist(m, σ²C). This modification changes the nature of the space exploration (wider "jumps"), but preserves the fundamental adaptive nature of the algorithm.

The covariance matrix C is the true heart of the algorithm. It begins its life as a humble identity matrix representing a spherical distribution. But with each iteration it evolves, stretching along directions of rapid improvement and shrinking where progress is slow. Gradually, the sphere turns into an ellipse, then into an elongated ellipsoid, ideally oriented along the contours of the function being optimized.

The main innovation of CMA-ES is the concept of evolutionary paths. This is a kind of genetic memory of the algorithm, which remembers not only where the successful points were, but also how the algorithm arrived at them. The evolutionary path accumulates information about successive successful steps, creating a directional vector that points to the most promising search areas. The second evolutionary path performs a more subtle function: it controls step size. If the successive steps of the algorithm are correlated, that is, each subsequent step continues the direction of the previous one, then the step size increases - the algorithm "feels" that it is moving in the right direction. If the steps are random and uncorrelated, the step size decreases - perhaps the algorithm is already close to the optimum and needs to be searched more carefully.

Behind the biological metaphor of evolution in CMA-ES lies a rigorous mathematical principle - maximum likelihood estimation. The algorithm constantly asks itself: What distribution parameters make the observed successful points most probable? This transforms evolutionary optimization from a heuristic into a statistically based method.

Each update of the C covariance matrix consists of two components: rank-one update based on the evolutionary path and rank-μ update using information from the entire selected population. Rank-one update provides stability and the ability to capture long-term trends, while rank-μ update allows rapid adaptation to new information from the current generation.

In the context of CMA-ES, a modified Heaviside function is used, which plays an important role in the stagnation detection mechanism of the algorithm. The function compares the length of the evolutionary path with the expected length; if the path is too short, it is a sign of "staggering". Deactivation conditions (hsig = 0): the algorithm "wanders" randomly and the steps cancel each other out, then the step size is probably too large. During stagnation, the influence of rank-one update decreases and we rely more on information from the entire population. Activation conditions (hsig = 1): the algorithm makes progress in a certain direction, successive steps are correlated, then the step size is adequate to the current situation. 

One of the deepest properties of CMA-ES is its invariance to transformations of the search space. The algorithm equally efficiently solves the function f(x) and the function f(Ax + b), where A is any non-singular matrix and b is any shift vector. This means that CMA-ES is independent of the choice of coordinate system. This invariance is not accidental; it is a direct consequence of the use of the maximum likelihood principle and the adaptation of the covariance matrix. The algorithm automatically detects a natural coordinate system for the problem, where the axes coincide with the principal directions of the function variation.

The beauty of theory must be combined with practical applicability. CMA-ES requires O(n²) of memory for storing the covariance matrix and O(n³) computations for eigendecomposition, which makes the algorithm applicable to problems with dimensions of up to several hundred variables. For large dimensions, specialized modifications have been developed: sep-CMA-ES is limited to diagonal covariance matrices, VkD-CMA uses variable dimensions, and LM-CMA applies the principles of limited memory. The implementation of these methods is currently beyond the scope of our article, but it may be possible to return to them in subsequent articles.

cmaes-algorithm

Figure 1. Illustration of the CMA-ES algorithm

The illustration shows the evolution through generations - three snapshots of the state of the algorithm (generations 1, 5 and 15), showing how the population gradually converges to the optimum,

Adaptation of the covariance matrix - the blue ellipses show how the shape of the distribution changes when the generation is: 1- round (identity matrix), 5 - elongated and rotated, 15 - finely tuned to local geometry.
Algorithm components: red dots are the entire population (λ descendants), green dots are the best solutions (μ parents), black dot m is the population mean, dashed circles are the isolines of the objective function. 

The key formula is below, with a note about the power law modification. The illustration clearly demonstrates how the algorithm adapts its search strategy to the landscape of the function being optimized, gradually narrowing the search space and approaching the optimum.

Let's prepare the pseudocode.

Initialization

  1. Set algorithm parameters:
    • Population size (lambda) = 50
    • Number of parents (mu) = 25
    • Learning rate for rank-1 update (c1) = 0.01
    • Learning rate for rank-μ update (cμ) = 0.8
    • Step size damping = 0.6
    • Initial step size (sigma) = 0.3
  2. Calculate recombination weights:
    • For each of the μ parents, calculate the weight as log(μ + 0.5) - log(i + 1)
    • Normalize the weights so that their sum equals 1
    • Calculate the effective selection mass of μ_eff
  3. Initialize strategy parameters:
    • Calculate learning rates for evolution path (cs, cc)
    • Compute the expected norm of a standard normal vector (chiN)
    • Determine the interval for eigenvalue decomposition
  4. Initialize data structures:
    • C covariance matrix = identity matrix
    • B matrix of eigenvectors = identity matrix
    • D vector of eigenvalues = unit vector
    • pc and ps evolution paths = zero vectors
    • Population mean m = random point in search space
  5. Create an initial population:
    • Generate lambda random points in the search space

Basic cycle of evolution

Repeat until stop criterion is reached:

Step 1: Generating offspring 

  1. For each of lambda descendants:
    • Generate the z random vector from the standard normal distribution
    • Apply the transformation: y = B × D × z
    • Create a descendant: x_k = m + σ × y
    • Apply search space constraints to x_k

Step 2: Evaluate and update 

  1. Assess the fitness of all descendants
  2. Sort the population:
    • Sort descendants in descending order of fitness
    • Update the best solution found if necessary
  3. Update population mean:
    • Store the previous mean m_old
    • Calculate the new mean as the weighted sum of the μ best offspring: m_new = Σ(w_i × x_i) for i from 1 to μ
  4. Update evolution paths:
    • Calculate the mean shift: y_w = (m_new - m_old) / σ
    • Calculate C^(-1/2) × y_w via eigenvalue decomposition
    • Update evolution path for step size: ps = (1 - cs) × ps + √(cs × (2 - cs) × μ_eff) × C^(-1/2) × y_w
    • Check the stagnation condition (Heaviside function)
    • Update the evolution path for the covariance matrix: pc = (1 - cc) × pc + stagnation_indicator × √(cc × (2 - cc) × μ_eff) × y_w
  5. Update the covariance matrix:
    • Prepare rank-1 update from the outer product of pc
    • Prepare the rank-μ update from the weighted sum of the best descendant's deviations
    • Update C: C = (1 - c1 - cμ) × C + c1 × (pc × pc^T) + cμ × Σ(w_i × y_i × y_i^T)
    • Ensure the matrix symmetry
  6. Update the step size:
    • Calculate the adaptation coefficient based on the ps path length
    • Update σ: σ = σ × exp((cs/damps) × (||ps||/chiN - 1))
    • Limit σ within reasonable limits for numerical stability
  7. Eigenvalue decomposition (if necessary):
    • If enough iterations have passed since the last decomposition:
      • Perform the Jacobi decomposition of C = B × D² × B^T
      • Sort eigenvalues and eigenvectors in descending order
      • Ensure positive definiteness of the matrix
  8. Check and correct the covariance matrix:
    • Periodically check the C positive definiteness
    • If necessary, add a small value to the diagonal
    • Ensure the matrix symmetry

Now let's write the C_AO_CMAES derived from the C_AO class, which will implement the CMA-ES optimization algorithm.

Public fields, method:

  • SetParams () - set CMA-ES parameter values from the params array.
  • Init () - initializes the algorithm. Accepts minimum, maximum values and step size for each variable, as well as the number of epochs.
  • Moving () - implements the main loop of the algorithm, responsible for generating new solutions.
  • Revision () - evaluation of new solutions and updating of the algorithm state.

Private fields:

  • CMA-ES parameters - the variables containing specific parameters of the CMA-ES algorithm: number of parents (mu), step size (sigma), learning rates (learningRateC1, learningRateCMu), damping factor (stepSizeDamping ) and others necessary for the algorithm to work.
  • CMA-ES data structures - arrays used to store recombination weights, covMatrix covariance matrix, B eigenvectors, D eigenvalues, evolution paths (pc, ps), as well as other auxiliary data. mu_eff, counteval, eigeneval, chiN, eigenInterval values are used to control and manage the algorithm progress.
  • Variables for performance optimization are designed to speed up calculations during the algorithm's operation, such as learning rates and damping.
  • Auxiliary arrays  used as temporary storage for vectors and matrices when performing various operations.
  • Auxiliary methods, which perform individual steps of the CMA-ES algorithm, such as initializing the distribution, updating the distribution, computing the eigendecomposition, sorting the population, computing the weights, updating the mean, checking for positive definiteness, and ensuring positive definiteness.

Overall, the C_AO_CMAES class encapsulates the logic of the CMA-ES algorithm, providing an interface for initialization, parameter tuning, and performing optimization. It contains the parameters, data arrays, and methods needed to implement the algorithm. The separation of public and private fields provides access control to the internal components of the class.

//————————————————————————————————————————————————————————————————————
class C_AO_CMAES : public C_AO
{
  public: //----------------------------------------------------------
  ~C_AO_CMAES () { }
  C_AO_CMAES ()
  {
    ao_name = "CMAES";
    ao_desc = "Covariance Matrix Adaptation Evolution Strategy";
    ao_link = "https://www.mql5.com/en/articles/18227";

    // Default parameters
    popSize         = 50;          // Default population size (lambda)
    mu              = 25;          // Number of parents (half of the population)
    learningRateC1  = 0.01;        // Learning rate for rank-1 update
    learningRateCMu = 0.8;         // Learning rate for rank-μ update
    stepSizeDamping = 0.6;         // Damping for step size

    // Create and initialize the parameters array
    ArrayResize (params, 5);
    params [0].name = "popSize";         params [0].val = popSize;
    params [1].name = "mu";              params [1].val = mu;
    params [2].name = "learningRateC1";  params [2].val = learningRateC1;
    params [3].name = "learningRateCMu"; params [3].val = learningRateCMu;
    params [4].name = "stepSizeDamping"; params [4].val = stepSizeDamping;
  }

  void SetParams ()
  {
    popSize         = (int)params [0].val;
    mu              = (int)params [1].val;
    learningRateC1  = params      [2].val;
    learningRateCMu = params      [3].val;
    stepSizeDamping = params      [4].val;
  }

  bool Init (const double &rangeMinP  [],  // minimum values
             const double &rangeMaxP  [],  // maximum values
             const double &rangeStepP [],  // step size
             const int     epochsP = 0);

  void Moving   ();
  void Revision ();

  //------------------------------------------------------------------
  private: //---------------------------------------------------------
  // CMA-ES specific parameters
  int mu;                  // Number of parents (selected points)
  double sigma;            // Step size
  double learningRateC1;   // Learning rate for rank-1 update
  double learningRateCMu;  // Learning rate for rank-μ update
  double stepSizeDamping;  // Damping factor for step size update

  // CMA-ES specific data structures
  double weights   [];      // Recombination weights
  double covMatrix [];      // Covariance matrix (stored as a one-dimensional array)
  double B  [];             // C eigenvectors
  double D  [];             // C eigenvalues (square roots)
  double pc [];             // Evolution path for C
  double ps [];             // Evolution path for step-size control
  double mu_eff;            // Effective selection mass
  int    counteval;         // Counter of function evaluations since the last decomposition
  int    eigeneval;         // Generation counter when decomposition was performed
  double chiN;              // Expected norm N(0,I)
  int    eigenInterval;     // Interval for eigenvalue decomposition

  // Variables for performance optimization
  double cs;                // Learning rate for the sigma path
  double cc;                // Learning rate for rank-1 path 
  double damps;             // Damping for sigma
  double hsig_threshold;    // Threshold for the Heaviside function

  // Auxiliary arrays
  double y_vec     [];         // Mutation vector
  double arindex   [];         // Array of indices for sorting
  double arfitness [];         // Fitness array for sorting
  double temp_vec  [];         // Temporary vector for matrix operations
  double invsqrtC_times_yw []; // Temporary storage for C^(-1/2) * y_w

  // Caching variables for Box-Muller
  double cached_normal;
  bool   has_cached;

  // Auxiliary methods
  void   InitDistribution          ();
  void   UpdateDistribution        ();
  void   ComputeEigendecomposition ();
  double GetChiN                   ();
  void   SortPopulation            ();
  void   ComputeWeights            ();
  void   UpdateMean                ();
  bool   CheckPositiveDefinite     ();
  void   EnforcePositiveDefinite   ();
};
//————————————————————————————————————————————————————————————————————

The Init method of the C_AO_CMAES class initializes the CMA-ES algorithm. It takes the minimum and maximum parameter values, step size and number of epochs as input. First, the StandardInit method is called to perform standard initialization. The has_cached flag is initialized to 'false' to indicate that there is no cached value for Box-Muller random number generation. Next, the sigma step size is initialized to the initial value of 0.3 (30% of the search range). The ComputeWeights method is called to compute the recombination weights, and the GetChiN method is called to compute the expected norm N(0, I).

cs, cc, damps and hsig_threshold parameters are calculated based on mu_eff (effective mass) and the number of coordinates (coords). These parameters are used to adapt the CMA-ES strategy during operation. The eigenInterval is calculated and set to determine how often the eigenvalue decomposition is performed. This parameter is configured to optimize performance.

Memory is allocated for CMA-ES-specific arrays such as the covMatrix covariance matrix, B eigenvectors, D eigenvalues, pc and ps evolution paths, y_vec, arindex, arfitness, temp_vec and invsqrtC_times_yw time vectors. Arrays are recreated only if the problem dimension needs to be changed.

The pc and ps evolution path arrays are initialized to zeros, the covMatrix covariance matrix and the B eigenvector matrix are initialized to the identity matrix, and the D eigenvalue array is initialized to ones. The InitDistribution method is called to initialize the initial distribution. Then the evaluation counters of the counteval and eigeneval functions are reset. The "revision" flag is set to 'true' to indicate that the fitness values should be recalculated. 'true' is returned on successful initialization.

In conclusion, the Init method performs all necessary initialization for the CMA-ES algorithm to work, including setting parameters, allocating memory for arrays, and initializing initial values. 

//————————————————————————————————————————————————————————————————————
bool C_AO_CMAES::Init (const double &rangeMinP [],  // minimum values
                       const double &rangeMaxP [],  // maximum values
                       const double &rangeStepP [], // step size
                       const int     epochsP = 0)   // number of epochs
{
  if (!StandardInit (rangeMinP, rangeMaxP, rangeStepP)) return false;

  //------------------------------------------------------------------
  // Initialize Box-Muller caching
  has_cached = false;

  // Initialize CMA-ES specific variables
  sigma          = 0.3;  // Initial step size (30% of search range)

  // Calculate the effective mass of the variance selection
  ComputeWeights ();

  // Expected norm N(0,I)
  chiN = GetChiN ();

  // Calculate and save strategy parameters
  cs = (mu_eff + 2.0) / (coords + mu_eff + 5.0);
  cc = (4.0 + mu_eff / coords) / (coords + 4.0 + 2.0 * mu_eff / coords);
  damps = 1.0 + 2.0 * MathMax (0.0, MathSqrt ((mu_eff - 1.0) / (coords + 1.0)) - 1.0) + cs;
  hsig_threshold = 1.4 + 2.0 / (coords + 1.0);

  // Set the eigenvalue decomposition interval - tuning for performance
  eigenInterval = (int)(coords / (10.0 * MathSqrt (learningRateC1 + learningRateCMu)));
  eigenInterval = MathMax (1, eigenInterval);

  // Allocate arrays only once
  ArrayResize (covMatrix, coords * coords);
  ArrayResize (B, coords * coords);
  ArrayResize (D, coords);
  ArrayResize (pc, coords);
  ArrayResize (ps, coords);
  ArrayResize (y_vec, coords);
  ArrayResize (arindex, popSize);
  ArrayResize (arfitness, popSize);
  ArrayResize (temp_vec, coords);
  ArrayResize (invsqrtC_times_yw, coords);

  // Initialize evolution paths with zeros
  ArrayInitialize (pc, 0);
  ArrayInitialize (ps, 0);

  // Fast initialization of the identity covariance matrix and decomposition
  ArrayInitialize (covMatrix, 0.0);
  ArrayInitialize (B, 0.0);

  for (int i = 0; i < coords; i++)
  {
    covMatrix [i * coords + i] = 1.0;
    B [i * coords + i] = 1.0;
    D [i] = 1.0;
  }

  // Initialize initial distribution
  InitDistribution ();

  // Reset calculation counters
  counteval = 0;
  eigeneval = 0;

  // Forced fitness recalculation
  revision = true;

  return true;
}
//————————————————————————————————————————————————————————————————————

The Moving method in the C_AO_CMAES class is responsible for generating new descendants (solutions) in the population. Transforming y = B * D * z: inside the loop over the population there is a nested loop that iterates over all the coordinates of each individual. At each i coordinate the y_vec [i] value is calculated. This is done by matrix multiplication. In fact, it is a multiplication of a vector of z random numbers by the matrix "B * D" matrix, where B are the eigenvectors of the covariance matrix, D are the eigenvalues of the covariance matrix, and z are random numbers generated using the u.PowerDistribution function. 

Descendant generation: the (a [k].c [i]) value is calculated for each i coordinate of each k descendant. This is done by adding the scaled vector y_vec[i] to the current mean cB[i] (the center of the current distribution). Scaling is done by multiplying y_vec[i] by 'sigma' step size. Thus, a [k].c [i] = cB [i] + sigma * y_vec [i].

The u.SeInDiSp function ensures that the coordinate values of the offspring remain within the valid ranges defined by rangeMin[i], rangeMax[i] and rangeStep[i]. It can clip values, bounce them off boundaries, and quantize them to the nearest valid step value. After all descendants are generated, the "revision" flag is set to 'true', and the fitness function (quality) values for the generated descendants should be recalculated.

As a result, the method generates a new population of solutions based on the current distribution (cB average and the covariance matrix, represented by B and D) and the sigma step size. It uses random numbers to create small variations around the current mean to explore the search space. The SeInDiSp function  ensures that new solutions remain within the admissible bounds, and the "revision" flag ensures that their fitness will then be evaluated.

//————————————————————————————————————————————————————————————————————
void C_AO_CMAES::Moving ()
{
  // Generate new lambda descendants
  for (int k = 0; k < popSize; k++)
  {

    // Apply the transformation y = B*D*z
    for (int i = 0; i < coords; i++)
    {
      y_vec [i] = 0.0;
      for (int j = 0; j < coords; j++)
      {
        y_vec [i] += B [i * coords + j] * D [j] * u.PowerDistribution (0.0, -8, 8, 20);
      }

      // Generate a descendant: x_k = m + σ * y
      a [k].c [i] = cB [i] + sigma * y_vec [i];
      a [k].c [i] = u.SeInDiSp (a [k].c [i], rangeMin [i], rangeMax [i], rangeStep [i]);
    }
  }

  // Fitness recalculation mark
  revision = true;
}
//————————————————————————————————————————————————————————————————————

The Revision method of the C_AO_CMAES class is responsible for updating the algorithm parameters after evaluating the fitness function for a new population. First, the method checks the value of the "revision" flag. Then, the SortPopulation() method is called, which sorts the current population so that the best individuals are placed at the top. The UpdateDistribution() method updates the parameters of the CMA-ES distribution (the cB mean of the strategy vector, the covariance matrix, the sigma step size, the evolutionary paths) based on information about the best individuals in the population. Distribution update is a key step of CMA-ES, as it allows the algorithm to adapt its search strategy based on the obtained results. 

As a result, the Revision  method represents the main adaptation cycle of CMA-ES. It sorts the population, updates the distribution parameters based on the best individuals, and increments the computation counter. 

//————————————————————————————————————————————————————————————————————
void C_AO_CMAES::Revision ()
{
  if (!revision) return;

  revision = false;

  // Sort the population by fitness
  SortPopulation ();

  // Update distribution parameters based on selected individuals
  UpdateDistribution ();

  // Update the calculation counter
  counteval++;
}
//————————————————————————————————————————————————————————————————————

The InitDistribution method of the C_AO_CMAES class is intended to initialize the initial distribution of the solution search. The method determines an initial mean value (cB) for each coordinate in the search space. For each i coordinate, the value of cB[i] is set randomly in the range between rangeMin[i] and rangeMax[i] using the u.RNDfromCI() function, with the initial mean value placed in the center of the valid range for each variable. The outer loop iterates over the entire population (popSize individuals). Inside this loop is an inner loop that iterates over all the coordinates of each individual. Coordinates are generated for each descendant. First, they are randomly selected from the given range RNDfromCI and then rounded to the sampling step using SeInDiSp.

The u.RNDfromCI function generates a uniformly distributed random number in the given interval. The u.SeInDiSp function ensures that the generated coordinates for each individual lie within the acceptable limits defined by rangeMin [i], rangeMax [i] and rangeStep [i].

The method initializes the initial population using random solutions uniformly distributed in the search space. The center of distribution (cB) is also chosen randomly. This provides a starting point for subsequent iterations of CMA-ES, in which the algorithm will adapt the search strategy to find the optimal solution.

//+------------------------------------------------------------------+
//| Initialize search distribution                                   |
//+------------------------------------------------------------------+
void C_AO_CMAES::InitDistribution ()
{
  // Set the initial mean to the center of the search space
  for (int i = 0; i < coords; i++)
  {
    cB [i] = u.RNDfromCI (rangeMin [i], rangeMax [i]);
  }

  for (int k = 0; k < popSize; k++)
  {
    for (int i = 0; i < coords; i++)
    {
      // Generate a uniformly distributed point
      a [k].c [i] = u.RNDfromCI (rangeMin [i], rangeMax [i]);
      a [k].c [i] = u.SeInDiSp (a [k].c [i], rangeMin [i], rangeMax [i], rangeStep [i]);
    }
  }
}
//————————————————————————————————————————————————————————————————————

The GetChiN method computes the expected norm of a random vector drawn from N (0,I) for a given number of coordinates (coords). The result is approximated based on the dimension of the search space.

//+------------------------------------------------------------------+
//| Calculate the expected norm N(0,I)                               |
//+------------------------------------------------------------------+
double C_AO_CMAES::GetChiN ()
{
  double n = (double)coords;
  return MathSqrt (n) * (1.0 - 1.0 / (4.0 * n) + 1.0 / (21.0 * n * n));
}
//————————————————————————————————————————————————————————————————————

The SortPopulation is designed to sort a population of solutions by their objective function values (fitness) in order to determine the best solution. First, the method copies the fitness values of each individual from the population into the arfitness auxiliary array. The arindex array is also created, which initially contains the indices of the individuals. This is necessary so that after sorting by fitness it is possible to restore the correspondence between the sorted fitness and the original individual in the population.

The insertion sort algorithm is used. It iterates through the arfitness array, and for each element, inserts it into the correct place in the sorted part of the array, shifting larger elements to the right. It is important to note that the sorting is performed in descending order ( arfitness [j] < tempFitness  in the 'while' loop), the goal is to maximize the target function (fitness). Along with arfitness, arindex is also sorted synchronously to track the position of the original indices of individuals.

After sorting, the method checks whether the fitness of the best individual is better than the current best solution, and if so, fB is updated and the best solution is replaced by the coordinates of the best individual from the population. Thus, the method not only sorts the population by fitness, but also keeps track of the best solution found at the moment.

//+------------------------------------------------------------------+
//| Sort the population by fitness                                   |
//+------------------------------------------------------------------+
void C_AO_CMAES::SortPopulation ()
{
  // Copy fitness values and indices
  for (int i = 0; i < popSize; i++)
  {
    arindex [i] = i;
    arfitness [i] = a [i].f;
  }

  for (int i = 1; i < popSize; i++)
  {
    double tempFitness = arfitness [i];
    double tempIndex = arindex [i];
    int j = i - 1;

    // Sort in descending order (for maximization)
    while (j >= 0 && arfitness [j] < tempFitness)
    {
      arfitness [j + 1] = arfitness [j];
      arindex [j + 1] = arindex [j];
      j--;
    }

    arfitness [j + 1] = tempFitness;
    arindex [j + 1] = tempIndex;
  }

  // Update the best solution if necessary
  if (arfitness [0] > fB)
  {
    fB = arfitness [0];
    int bestIdx = (int)arindex [0];
    ArrayCopy (cB, a [bestIdx].c, 0, 0, coords);
  }
}
//————————————————————————————————————————————————————————————————————

The  UpdateMean method updates the population mean (cB) by weighted recombination of the best individuals. For each coordinate, a new average is calculated as the sum of the weighted coordinate values from the best individuals, where the weights are specified in the weights array.

//+------------------------------------------------------------------+
//| Update the mean using weighted recombination                     |
//+------------------------------------------------------------------+
void C_AO_CMAES::UpdateMean ()
{
  // Weighted recombination: m^(g+1) = Σ w_i * x_{i:λ}^(g+1)
  for (int j = 0; j < coords; j++)
  {
    double meanSum = 0.0;
    for (int i = 0; i < mu; i++)
    {
      int idx = (int)arindex [i];
      meanSum += weights [i] * a [idx].c [j];
    }
    cB [j] = meanSum;
  }
}
//————————————————————————————————————————————————————————————————————

The ComputeWeights method calculates weights for weighted recombination, allocates the weights array by size mu, calculates log (mu + 0.5) for use in the weights formula. For each i from 0 to mu-1, it assigns a weight as the difference between log(mu + 0.5) and log(i+1), summing these weights, the method divides all the weights by the sum so that the sum is equal to 1, and calculates the sum of the squares of the weights, calculates mu_eff - the efficiency of using the weights, which is important for tuning the speed and variance in the algorithm.

This method provides optimal weights for recombination, taking into account the contribution of the best solutions.

//+------------------------------------------------------------------+
//| Calculate weighted recombination weights                         |
//+------------------------------------------------------------------+
void C_AO_CMAES::ComputeWeights ()
{
  // Allocate the array of weights
  ArrayResize (weights, mu);

  // Pre-calculate log(mu + 0.5)
  double log_mu_plus_half = MathLog (mu + 0.5);

  // Calculate positive weights
  double sum = 0.0;
  for (int i = 0; i < mu; i++)
  {
    weights [i] = log_mu_plus_half - MathLog (i + 1);
    sum += weights [i];
  }

  // Normalize weights
  double sum_weights = 0.0;
  double sum_squares = 0.0;
  for (int i = 0; i < mu; i++)
  {
    weights [i] /= sum;
    sum_weights += weights [i];
    sum_squares += weights [i] * weights [i];
  }

  // Calculate the effective mass of the variance selection
  mu_eff = sum_weights * sum_weights / sum_squares;
}
//————————————————————————————————————————————————————————————————————

The UpdateDistribution method updates the distribution parameters, namely the covariance matrix (covMatrix) and the step size (sigma) in the CMA-ES algorithm. If enough generations have passed (counteval - eigeneval > eigenInterval), then the eigenvalue decomposition is calculated and the current mean (cB) is stored in the oldMean temporary array. The method calls UpdateMean to calculate a new mean, calculates the difference between the new and old mean divided by sigma, and also performs the multiplication of y_w by C^(-1/2), broken into several steps using the B and D decomposition, and stores the result in invsqrtC_times_yw.

The method then updates the evolution path for ps step size using invsqrtC_times_yw, determines whether progress is considered "stuck" based on the ps norm and the expected length, then updates the evolution path for the pc covariance matrix using y_w and the hsig progress indicator. The method first calculates the c1a rank-1 update matrix, then the cmu rank-μ update matrix based on the difference between individuals and the old mean divided by sigma using 'weights', updates the covMatrix covariance matrix using the rank-1 and rank-μ updates, and adjusts c1 when progress stalls. The EnforcePositiveDefinite is periodically called to ensure that the covariance matrix is positive definite. The sigma step size is updated based on the ps norm and is constrained between min_sigma and max_sigma for numerical stability.

Generally, UpdateDistribution adjusts the distribution parameters (covariance matrix and step size) based on the search history (evolutionary path) and population data to adapt to the landscape of the objective function.

//+------------------------------------------------------------------+ 
//| Update distribution parameters                                   |
//+------------------------------------------------------------------+
void C_AO_CMAES::UpdateDistribution ()
{
  // Check the necessity of eigenvalue decomposition
  if (counteval - eigeneval > eigenInterval)
  {
    ComputeEigendecomposition ();
    eigeneval = counteval;
  }

  // Preserve the old average
  double oldMean [];
  ArrayResize (oldMean, coords);
  ArrayCopy (oldMean, cB, 0, 0, coords);

  // Update average
  UpdateMean ();

  // Calculate the mean shift
  double y_w [];
  ArrayResize (y_w, coords);
  for (int j = 0; j < coords; j++)
  {
    y_w [j] = (cB [j] - oldMean [j]) / sigma;
  }

  // Calculate C^(-1/2) * y_w
  // Step 1: B^T * y_w
  ArrayInitialize (temp_vec, 0.0);
  for (int i = 0; i < coords; i++)
  {
    for (int j = 0; j < coords; j++)
    {
      temp_vec [i] += B [j * coords + i] * y_w [j];
    }
  }

  // Step 2: D^(-1) * (B^T * y_w)
  for (int i = 0; i < coords; i++)
  {
    temp_vec [i] /= D [i];
  }

  // Step 3: B * D^(-1) * B^T * y_w
  ArrayInitialize (invsqrtC_times_yw, 0.0);
  for (int i = 0; i < coords; i++)
  {
    for (int j = 0; j < coords; j++)
    {
      invsqrtC_times_yw [i] += B [i * coords + j] * temp_vec [j];
    }
  }

  // Update the evolution path for sigma
  double norm_ps_squared = 0.0;
  for (int i = 0; i < coords; i++)
  {
    ps [i] = (1.0 - cs) * ps [i] + MathSqrt (cs * (2.0 - cs) * mu_eff) * invsqrtC_times_yw [i];
    norm_ps_squared += ps [i] * ps [i];
  }

  // Heaviside function
  double norm_ps = MathSqrt (norm_ps_squared);
  double expected_length = MathSqrt (1.0 - MathPow (1.0 - cs, 2.0 * counteval)) * chiN;
  bool hsig = norm_ps / expected_length < hsig_threshold;

  // Update the evolution path for C
  double delta_hsig = hsig ? 1.0 : 0.0;
  for (int i = 0; i < coords; i++)
  {
    pc [i] = (1.0 - cc) * pc [i] + delta_hsig * MathSqrt (cc * (2.0 - cc) * mu_eff) * y_w [i];
  }

  // Prepare rank-1 update
  double c1a [];
  ArrayResize (c1a, coords * coords);
  for (int i = 0; i < coords; i++)
  {
    for (int j = 0; j <= i; j++)
    {
      c1a [i * coords + j] = c1a [j * coords + i] = pc [i] * pc [j];
    }
  }

  // Prepare rank-μ update
  double cmu [];
  ArrayResize (cmu, coords * coords);
  ArrayInitialize (cmu, 0.0);

  for (int k = 0; k < mu; k++)
  {
    int idx = (int)arindex [k];

    // Calculate y_i = (x_i - m_old) / sigma
    for (int i = 0; i < coords; i++)
    {
      temp_vec [i] = (a [idx].c [i] - oldMean [i]) / sigma;
    }

    // Add the weighted outer product
    for (int i = 0; i < coords; i++)
    {
      for (int j = 0; j <= i; j++)
      {
        double update = weights [k] * temp_vec [i] * temp_vec [j];
        cmu [i * coords + j] += update;
        if (i != j) cmu [j * coords + i] += update;
      }
    }
  }

  // Update C the covariance matrix
  double c1 = learningRateC1;
  double cmu_rate = learningRateCMu;

  // Adjust c1 if hsig is 'false' (progress stalled)
  if (!hsig)
  {
    c1 *= (1.0 - (1.0 - delta_hsig) * cc * (2.0 - cc));
  }

  double one_minus_c1_cmu = 1.0 - c1 - cmu_rate;

  // Update C with rank-1 and rank-μ updates
  for (int i = 0; i < coords; i++)
  {
    for (int j = 0; j <= i; j++)
    {
      covMatrix [i * coords + j] = one_minus_c1_cmu * covMatrix [i * coords + j] +
                                   c1 * c1a [i * coords + j] +
                                   cmu_rate * cmu [i * coords + j];

      // Maintain symmetry
      if (i != j)
      {
        covMatrix [j * coords + i] = covMatrix [i * coords + j];
      }
    }
  }

  // Ensure positive definiteness
  if (counteval % (10 * eigenInterval) == 0)
  {
    EnforcePositiveDefinite ();
  }

  //  Update the sigma step size
  double exponent = (cs / damps) * (norm_ps / chiN - 1.0);
  sigma *= MathExp (exponent);

  // Limit sigma for numerical stability
  double min_sigma = 1e-16;
  double max_eigenvalue = D [0]; // D sorted in descending order
  double max_sigma = 1e4 * MathMax (1.0, MathSqrt (max_eigenvalue));

  if (sigma < min_sigma) sigma = min_sigma;
  else
    if (sigma > max_sigma) sigma = max_sigma;
}
//————————————————————————————————————————————————————————————————————

The ComputeEigendecomposition " performs the decomposition of the covMatrix symmetric covariance into eigenvalues and eigenvectors using the improved Jacobi method, the method creates a copy of the covMatrix so as not to modify the original data. Initially, B is set as the identity matrix representing the initial basis. It performs maximum iterations or until non-zero elements on the off-diagonal are less than "tolerance". 

Finding the maximum off-diagonal element: Find the element with the largest absolute value off the diagonal to select for rotation. Next, if the maximum is less than "tolerance", the cycle is interrupted. The method calculates phi to perform the Jacobian rotation, modifies the elements of C_copy by applying the rotation, and updates the columns of B matrix (eigenvectors) according to the rotation. After the iterations, the method finds the square root of the diagonal elements (ensuring minimum positivity of 1e-14) and orders the eigenvalues and corresponding eigenvectors in descending order for further use. Thus, the method allows for accurate calculation of the eigenvalues and eigenvectors of the covariance matrix.

//+------------------------------------------------------------------+
//| Calculate the eigenvalue decomposition using the Jacobi method   |
//+------------------------------------------------------------------+
void C_AO_CMAES::ComputeEigendecomposition ()
{
  // Create a copy of the covariance matrix for decomposition
  double C_copy [];
  ArrayResize (C_copy, coords * coords);
  ArrayCopy (C_copy, covMatrix);

  // Initialize B as the identity matrix
  for (int i = 0; i < coords; i++)
  {
    for (int j = 0; j < coords; j++)
    {
      B [i * coords + j] = (i == j) ? 1.0 : 0.0;
    }
  }

  // Improved Jacobi eigenvalue decomposition
  int max_iterations = 10; //50 * coords;
  double tolerance   = 0.01; //1e-14 * coords * coords;

  for (int iter = 0; iter < max_iterations; iter++)
  {
    // Find the largest off-diagonal element
    double max_val = 0.0;
    int p = 0, q = 1;

    for (int i = 0; i < coords - 1; i++)
    {
      for (int j = i + 1; j < coords; j++)
      {
        double val = MathAbs (C_copy [i * coords + j]);
        if (val > max_val)
        {
          max_val = val;
          p = i;
          q = j;
        }
      }
    }

    // Check convergence
    if (max_val < tolerance) break;

    // Calculate the rotation angle
    double app = C_copy [p * coords + p];
    double aqq = C_copy [q * coords + q];
    double apq = C_copy [p * coords + q];

    double phi = 0.5 * MathArctan (2.0 * apq / (aqq - app + 1e-14));
    double c = MathCos (phi);
    double s = MathSin (phi);

    // Update the matrix elements
    double app_new = c * c * app - 2 * c * s * apq + s * s * aqq;
    double aqq_new = s * s * app + 2 * c * s * apq + c * c * aqq;

    C_copy [p * coords + p] = app_new;
    C_copy [q * coords + q] = aqq_new;
    C_copy [p * coords + q] = C_copy [q * coords + p] = 0.0;

    // Update other elements in p and q rows/columns
    for (int i = 0; i < coords; i++)
    {
      if (i != p && i != q)
      {
        double aip = C_copy [i * coords + p];
        double aiq = C_copy [i * coords + q];
        C_copy [i * coords + p] = C_copy [p * coords + i] = c * aip - s * aiq;
        C_copy [i * coords + q] = C_copy [q * coords + i] = s * aip + c * aiq;
      }
    }

    // Update eigenvectors
    for (int i = 0; i < coords; i++)
    {
      double bip = B [i * coords + p];
      double biq = B [i * coords + q];
      B [i * coords + p] = c * bip - s * biq;
      B [i * coords + q] = s * bip + c * biq;
    }
  }

  // Extract eigenvalues and ensure positivity
  double min_eigenvalue = 1e-14;
  for (int i = 0; i < coords; i++)
  {
    D [i] = MathSqrt (MathMax (min_eigenvalue, C_copy [i * coords + i]));
  }

  // Sort eigenvalues and vectors in descending order
  for (int i = 0; i < coords - 1; i++)
  {
    int max_idx = i;
    for (int j = i + 1; j < coords; j++)
    {
      if (D [j] > D [max_idx]) max_idx = j;
    }

    if (max_idx != i)
    {
      // Exchange eigenvalues
      double temp = D [i];
      D [i] = D [max_idx];
      D [max_idx] = temp;

      // Exchange eigenvectors
      for (int k = 0; k < coords; k++)
      {
        temp = B [k * coords + i];
        B [k * coords + i] = B [k * coords + max_idx];
        B [k * coords + max_idx] = temp;
      }
    }
  }
}
//————————————————————————————————————————————————————————————————————

The CheckPositiveDefinite method checks whether the covariance matrix is positive definite. It quickly checks the positivity of diagonal elements and compares the minimum eigenvalue (from the D array sorted in descending order) with a small positive number 1e-14. If both checks pass, the method returns 'true'.

//+------------------------------------------------------------------+
//| Check the positive definiteness of the covariance matrix         |
//+------------------------------------------------------------------+
bool C_AO_CMAES::CheckPositiveDefinite ()
{
  // Quick check: all diagonal elements should be positive
  for (int i = 0; i < coords; i++)
  {
    if (covMatrix [i * coords + i] <= 0) return false;
  }

  // Check if the minimum eigenvalue is positive
  double min_eigenvalue = D [coords - 1]; // D is sorted in descending order
  return min_eigenvalue > 1e-14;
}
//————————————————————————————————————————————————————————————————————

The EnforcePositiveDefinite method ensures positive definiteness of the covMatrix covariance matrix . It performs several steps: it finds the minimum element on the diagonal and adds the missing "correction" value to the diagonal elements so that the minimum is equal to 1e-10, it maximizes the symmetry of the matrix by averaging each off-diagonal element with its transpose.

If the matrix is still not positive definite, an eigenvalue decomposition is performed using ComputeEigendecomposition, replacing all eigenvalues less than √1e-10 with √1e-10 to ensure positivity. The method then recalculates covMatrix using the B eigenvectors and the corrected D eigenvalues to obtain a corrected positive definite matrix. This approach ensures that the covariance matrix becomes positive definite and suitable for further calculations in the CMA-ES algorithm.

//+------------------------------------------------------------------+
//| Ensuring positive definiteness of the covariance matrix          |
//+------------------------------------------------------------------+
void C_AO_CMAES::EnforcePositiveDefinite ()
{
  // Method 1: Adding a small value to the diagonal
  double min_diag = 1e308; // A very large number
  for (int i = 0; i < coords; i++)
  {
    if (covMatrix [i * coords + i] < min_diag)
    {
      min_diag = covMatrix [i * coords + i];
    }
  }

  if (min_diag < 1e-10)
  {
    double correction = 1e-10 - min_diag;
    for (int i = 0; i < coords; i++)
    {
      covMatrix [i * coords + i] += correction;
    }
  }

  // Method 2: Ensuring symmetry
  for (int i = 0; i < coords; i++)
  {
    for (int j = i + 1; j < coords; j++)
    {
      double avg = (covMatrix [i * coords + j] + covMatrix [j * coords + i]) * 0.5;
      covMatrix [i * coords + j] = covMatrix [j * coords + i] = avg;
    }
  }

  // If still not positive definite, perform the factorization and fix
  if (!CheckPositiveDefinite ())
  {
    ComputeEigendecomposition ();

    double min_eigenvalue = 1e-10;
    for (int i = 0; i < coords; i++)
    {
      if (D [i] < MathSqrt (min_eigenvalue))
      {
        D [i] = MathSqrt (min_eigenvalue);
      }
    }

    // Reconstruct C = B * D^2 * B^T
    ArrayInitialize (covMatrix, 0.0);
    for (int i = 0; i < coords; i++)
    {
      for (int j = 0; j <= i; j++)
      {
        double sum = 0.0;
        for (int k = 0; k < coords; k++)
        {
          sum += B [i * coords + k] * D [k] * D [k] * B [j * coords + k];
        }
        covMatrix [i * coords + j] = covMatrix [j * coords + i] = sum;
      }
    }
  }
}
//————————————————————————————————————————————————————————————————————


Test results

Now we can finally look at the results. The algorithm, as can be immediately noted, copes well and quickly with medium-sized problems, and with certain settings, with low-dimensional problems, but the most difficult, multidimensional problems require a disproportionate amount of time to execute, so they were effectively excluded from the results. Why does this happen? Traditionally, matrix calculations in MQL5 present a serious computational problem. When implementing matrix operations manually, the algorithm demonstrates unsatisfactory performance on high-dimensional problems, which significantly limits its practical applicability. However, with the advent of built-in classes for working with matrices, the situation changes dramatically. It is now critical to use the platform's native implementation of matrix operations for all computationally intensive tasks.

CMAES|Covariance Matrix Adaptation Evolution Strategy|50.0|25.0|0.01|0.8|1.0|
=============================
5 Hilly's; Func runs: 10000; result: 0.7625797883550075
25 Hilly's; Func runs: 10000; result: 0.7208874560706138
500 Hilly's; Func runs: 10000; result: 0.0
=============================
5 Forest's; Func runs: 10000; result: 0.8205636421348295
25 Forest's; Func runs: 10000; result: 0.7961602627346933
500 Forest's; Func runs: 10000; result: 0.0
=============================
5 Megacity's; Func runs: 10000; result: 0.7584615384615383
25 Megacity's; Func runs: 10000; result: 0.49076923076923074
500 Megacity's; Func runs: 10000; result: 0.0
=============================
All score: 4.34942 (48.33%)

In the visualization, we can see that the algorithm works both with "large jumps" and focuses on the extrema of the function, exploring them thoroughly.

Hilly

CMA-ES on the Hilly test function

Forest

CMA-ES on the Forest test function

Megacity

CMA-ES on the Megacity test function

Based on the results, the CMA-ES algorithm ranks 38th in the ranking of the strongest population-based optimization algorithms.

#
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 0.94948 0.84776 0.43857 2.23581 1.00000 0.92334 0.39988 2.32323 0.70923 0.63477 0.23091 1.57491 6.134 68.15
2 CLA code lock algorithm (joo) 0.95345 0.87107 0.37590 2.20042 0.98942 0.91709 0.31642 2.22294 0.79692 0.69385 0.19303 1.68380 6.107 67.86
3 AMOm animal migration ptimization M 0.90358 0.84317 0.46284 2.20959 0.99001 0.92436 0.46598 2.38034 0.56769 0.59132 0.23773 1.39675 5.987 66.52
4 (P+O)ES (P+O) evolution strategies 0.92256 0.88101 0.40021 2.20379 0.97750 0.87490 0.31945 2.17185 0.67385 0.62985 0.18634 1.49003 5.866 65.17
5 CTA comet tail algorithm (joo) 0.95346 0.86319 0.27770 2.09435 0.99794 0.85740 0.33949 2.19484 0.88769 0.56431 0.10512 1.55712 5.846 64.96
6 TETA time evolution travel algorithm (joo) 0.91362 0.82349 0.31990 2.05701 0.97096 0.89532 0.29324 2.15952 0.73462 0.68569 0.16021 1.58052 5.797 64.41
7 SDSm stochastic diffusion search M 0.93066 0.85445 0.39476 2.17988 0.99983 0.89244 0.19619 2.08846 0.72333 0.61100 0.10670 1.44103 5.709 63.44
8 BOAm billiards optimization algorithm M 0.95757 0.82599 0.25235 2.03590 1.00000 0.90036 0.30502 2.20538 0.73538 0.52523 0.09563 1.35625 5.598 62.19
9 AAm archery algorithm M 0.91744 0.70876 0.42160 2.04780 0.92527 0.75802 0.35328 2.03657 0.67385 0.55200 0.23738 1.46323 5.548 61.64
10 ESG evolution of social groups (joo) 0.99906 0.79654 0.35056 2.14616 1.00000 0.82863 0.13102 1.95965 0.82333 0.55300 0.04725 1.42358 5.529 61.44
11 SIA simulated isotropic annealing (joo) 0.95784 0.84264 0.41465 2.21513 0.98239 0.79586 0.20507 1.98332 0.68667 0.49300 0.09053 1.27020 5.469 60.76
12 BBO biogeography based optimization 0.94912 0.69456 0.35031 1.99399 0.93820 0.67365 0.25682 1.86867 0.74615 0.48277 0.17369 1.40261 5.265 58.50
13 ACS artificial cooperative search 0.75547 0.74744 0.30407 1.80698 1.00000 0.88861 0.22413 2.11274 0.69077 0.48185 0.13322 1.30583 5.226 58.06
14 DA dialectical algorithm 0.86183 0.70033 0.33724 1.89940 0.98163 0.72772 0.28718 1.99653 0.70308 0.45292 0.16367 1.31967 5.216 57.95
15 BHAm black hole algorithm M 0.75236 0.76675 0.34583 1.86493 0.93593 0.80152 0.27177 2.00923 0.65077 0.51646 0.15472 1.32195 5.196 57.73
16 ASO anarchy society optimization 0.84872 0.74646 0.31465 1.90983 0.96148 0.79150 0.23803 1.99101 0.57077 0.54062 0.16614 1.27752 5.178 57.54
17 RFO royal flush optimization (joo) 0.83361 0.73742 0.34629 1.91733 0.89424 0.73824 0.24098 1.87346 0.63154 0.50292 0.16421 1.29867 5.089 56.55
18 AOSm atomic orbital search M 0.80232 0.70449 0.31021 1.81702 0.85660 0.69451 0.21996 1.77107 0.74615 0.52862 0.14358 1.41835 5.006 55.63
19 TSEA turtle shell evolution algorithm (joo) 0.96798 0.64480 0.29672 1.90949 0.99449 0.61981 0.22708 1.84139 0.69077 0.42646 0.13598 1.25322 5.004 55.60
20 DE differential evolution 0.95044 0.61674 0.30308 1.87026 0.95317 0.78896 0.16652 1.90865 0.78667 0.36033 0.02953 1.17653 4.955 55.06
21 SRA successful restaurateur algorithm (joo) 0.96883 0.63455 0.29217 1.89555 0.94637 0.55506 0.19124 1.69267 0.74923 0.44031 0.12526 1.31480 4.903 54.48
22 CRO chemical reaction optimization 0.94629 0.66112 0.29853 1.90593 0.87906 0.58422 0.21146 1.67473 0.75846 0.42646 0.12686 1.31178 4.892 54.36
23 BIO blood inheritance optimization (joo) 0.81568 0.65336 0.30877 1.77781 0.89937 0.65319 0.21760 1.77016 0.67846 0.47631 0.13902 1.29378 4.842 53.80
24 BSA bird swarm algorithm 0.89306 0.64900 0.26250 1.80455 0.92420 0.71121 0.24939 1.88479 0.69385 0.32615 0.10012 1.12012 4.809 53.44
25 HS harmony search 0.86509 0.68782 0.32527 1.87818 0.99999 0.68002 0.09590 1.77592 0.62000 0.42267 0.05458 1.09725 4.751 52.79
26 SSG saplings sowing and growing 0.77839 0.64925 0.39543 1.82308 0.85973 0.62467 0.17429 1.65869 0.64667 0.44133 0.10598 1.19398 4.676 51.95
27 BCOm bacterial chemotaxis optimization M 0.75953 0.62268 0.31483 1.69704 0.89378 0.61339 0.22542 1.73259 0.65385 0.42092 0.14435 1.21912 4.649 51.65
28 ABO african buffalo optimization 0.83337 0.62247 0.29964 1.75548 0.92170 0.58618 0.19723 1.70511 0.61000 0.43154 0.13225 1.17378 4.634 51.49
29 (PO)ES (PO) evolution strategies 0.79025 0.62647 0.42935 1.84606 0.87616 0.60943 0.19591 1.68151 0.59000 0.37933 0.11322 1.08255 4.610 51.22
30 FBA Fractal-Based Algorithm 0.79000 0.65134 0.28965 1.73099 0.87158 0.56823 0.18877 1.62858 0.61077 0.46062 0.12398 1.19537 4.555 50.61
31 TSm tabu search M 0.87795 0.61431 0.29104 1.78330 0.92885 0.51844 0.19054 1.63783 0.61077 0.38215 0.12157 1.11449 4.536 50.40
32 BSO brain storm optimization 0.93736 0.57616 0.29688 1.81041 0.93131 0.55866 0.23537 1.72534 0.55231 0.29077 0.11914 0.96222 4.498 49.98
33 WOAm wale optimization algorithm M 0.84521 0.56298 0.26263 1.67081 0.93100 0.52278 0.16365 1.61743 0.66308 0.41138 0.11357 1.18803 4.476 49.74
34 AEFA artificial electric field algorithm 0.87700 0.61753 0.25235 1.74688 0.92729 0.72698 0.18064 1.83490 0.66615 0.11631 0.09508 0.87754 4.459 49.55
35 AEO artificial ecosystem-based optimization algorithm 0.91380 0.46713 0.26470 1.64563 0.90223 0.43705 0.21400 1.55327 0.66154 0.30800 0.28563 1.25517 4.454 49.49
36 CAm camel algorithm M 0.78684 0.56042 0.35133 1.69859 0.82772 0.56041 0.24336 1.63149 0.64846 0.33092 0.13418 1.11356 4.444 49.37
37 ACOm ant colony optimization M 0.88190 0.66127 0.30377 1.84693 0.85873 0.58680 0.15051 1.59604 0.59667 0.37333 0.02472 0.99472 4.438 49.31
38 CMAES covariance_matrix_adaptation_evolution_strategy 0.76258 0.72089 0.00000 1.48347 0.82056 0.79616 0.00000 1.61672 0.75846 0.49077 0.00000 1.24923 4.349 48.33
39 BFO-GA bacterial foraging optimization - ga 0.89150 0.55111 0.31529 1.75790 0.96982 0.39612 0.06305 1.42899 0.72667 0.27500 0.03525 1.03692 4.224 46.93
40 SOA simple optimization algorithm 0.91520 0.46976 0.27089 1.65585 0.89675 0.37401 0.16984 1.44060 0.69538 0.28031 0.10852 1.08422 4.181 46.45
41 ABHA artificial bee hive algorithm 0.84131 0.54227 0.26304 1.64663 0.87858 0.47779 0.17181 1.52818 0.50923 0.33877 0.10397 0.95197 4.127 45.85
42 ACMO atmospheric cloud model optimization 0.90321 0.48546 0.30403 1.69270 0.80268 0.37857 0.19178 1.37303 0.62308 0.24400 0.10795 0.97503 4.041 44.90
43 ADAMm adaptive moment estimation M 0.88635 0.44766 0.26613 1.60014 0.84497 0.38493 0.16889 1.39880 0.66154 0.27046 0.10594 1.03794 4.037 44.85
44 CGO chaos game optimization 0.57256 0.37158 0.32018 1.26432 0.61176 0.61931 0.62161 1.85267 0.37538 0.21923 0.19028 0.78490 3.902 43.35
45 CROm coral reefs optimization M 0.78512 0.46032 0.25958 1.50502 0.86688 0.35297 0.16267 1.38252 0.63231 0.26738 0.10734 1.00703 3.895 43.27
RW random walk 0.48754 0.32159 0.25781 1.06694 0.37554 0.21944 0.15877 0.75375 0.27969 0.14917 0.09847 0.52734 2.348 26.09


Summary

CMA-ES's ability to adapt the search strategy to the local geometry of the objective function makes it virtually indispensable for complex, ill-conditioned problems with unknown structure. The heavy tails of the power-law distribution allow the algorithm to make "long jumps," which is critical for escaping local optima. Its computational complexity of O(n²) memory and O(n³) time severely limits the algorithm's applicability. For high-dimensional problems (n > 100), the resource intensity becomes disproportionate to the benefits obtained. The running time on multidimensional functions grows so quickly that it makes the algorithm practically inapplicable for large n.

CMA-ES works on noisy functions, copes with discontinuous objective functions, is effective on multi-extreme landscapes, and the secret to this versatility lies in the algorithm's fundamental philosophy: instead of making assumptions about the function's structure, it carefully explores it, adapting its search strategy based on accumulated experience. CMA-ES changes the landscape of evolutionary computation by showing that biologically inspired algorithms can have rigorous mathematical foundations and that algorithm adaptation is not simply a parameter tweak, but a fundamental principle of learning the structure of a problem.

The algorithm is great for medium-sized problems, it is a specialized tool that looks like a worthy alternative in its niche. The algorithm combines mathematical elegance with practical efficiency, but requires careful application given its computational limitations. In such cases, fast MQL5 matrix operations have recently been introduced by the MQL5 language developers to calculate matrices; in this implementation of the algorithm, conventional sequential calculations were used.

tab

Figure 2. Color gradation of algorithms according to the corresponding tests

chart

Figure 3. Histogram of algorithm testing results (scale from 0 to 100, the higher the better, where 100 is the maximum possible theoretical result, in the archive there is a script for calculating the rating table)

CMAES pros and cons:

Pros:

  1. Good convergence on functions of medium dimension.

Cons:

  1. Scatter of results on low-dimensional functions.
  2. Critical resource intensity on large-scale functions.

The article is accompanied by an archive with the current versions of the algorithm codes. The author of the article is not responsible for the absolute accuracy in the description of canonical algorithms. Changes have been made to many of them to improve search capabilities. The conclusions and judgments presented in the articles are based on the results of the experiments.


Programs used in the article

# Name Type Description
1 #C_AO.mqh
Include
Parent class of population optimization algorithms
2 #C_AO_enum.mqh
Include
Enumeration of population optimization algorithms
3 TestFunctions.mqh
Include
Library of test functions
4
TestStandFunctions.mqh
Include
Test stand function library
5
Utilities.mqh
Include
Library of auxiliary functions
6
CalculationTestResults.mqh
Include
Script for calculating results in the comparison table
7
Testing AOs.mq5
Script The unified test stand for all population optimization algorithms
8
Simple use of population optimization algorithms.mq5
Script
A simple example of using population optimization algorithms without visualization
9
Test_AO_CMAES.mq5
Script CMAES test bench

Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/18227

Attached files |
CMAES.zip (230.9 KB)
Meta-Labeling the Classics (Part 1): Filtering and Sizing RSI Trades Meta-Labeling the Classics (Part 1): Filtering and Sizing RSI Trades
RSI accumulates losses in trending conditions by firing at every threshold crossing regardless of market regime. A Random Forest secondary classifier trained on 12 contextual features — RSI momentum slope, EMA50 trend velocity, ATR-normalised trend stretch, and nine others — filters raw signals and scales position size by classifier confidence on EURUSD H1. Results compare plain RSI, meta-filtered RSI, and bet-sized RSI across a 16-month out-of-sample period with per-trade metrics and drawdown diagnostics.
Building a Dynamic STF Liquidity Sweep Indicator in MQL5 Building a Dynamic STF Liquidity Sweep Indicator in MQL5
The article delivers a dynamic MetaTrader 5 indicator that detects liquidity sweeps via swing‑point logic, wick‑ratio thresholds, and engulfing confirmation. It recognizes single‑wick and dual‑candle patterns without a fixed window, updates buy‑/sell‑side targets as price evolves, and invalidates broken levels to maintain a reliable liquidity map.
Joint Recurrence Quantification Analysis (JRQA) in MQL5: Detecting Simultaneous Recurrence in Two Series Joint Recurrence Quantification Analysis (JRQA) in MQL5: Detecting Simultaneous Recurrence in Two Series
We extend the RQA library for MetaTrader 5 with JRQA, which detects when two series simultaneously revisit their own past states. The article covers the joint recurrence matrix, twelve JRQA metrics (including TREND and COMPLEXITY), dual-epsilon configuration, and a rolling-window engine with OpenCL acceleration and automatic CPU fallback. A practical indicator plots JRR, JDET, JLAM, JENTR, and JTREND for any symbol pair with timestamp alignment and normalization.
Integrating AI into 3 Smart Money Concepts (SMC): OB, BOS, and FVG Integrating AI into 3 Smart Money Concepts (SMC): OB, BOS, and FVG
This guide integrates a trained XGBoost model (ONNX) into an SMC EA to evaluate trade setups before execution. The Python pipeline labels historical XAUUSD events and produces a 12-feature representation aligned with the EA. The result is a reproducible method to train, export, and embed the model so the EA can filter OB, FVG, and BOS signals programmatically.