Русский
preview
Coyote Optimization Algorithm (COA)

Coyote Optimization Algorithm (COA)

MetaTrader 5 — Trading |
82 0
Andrey Dik
Andrey Dik

Contents

  1. Introduction
  2. Implementation of the Algorithm
  3. Test Results
  4. Conclusions


Introduction

Anyone who has ever run a trading system optimization knows that feeling. The parameter space is vast: indicator periods, stop-loss and take-profit levels, time and volatility filters — even a simple Expert Advisor can easily have a dozen dimensions. An exhaustive search is physically impossible. Premature convergence is the main enemy of anyone seeking stable parameters in a multimodal landscape, and the profit landscape of a trading system is precisely that: rugged, noisy, and full of narrow peaks and broad, gentle plateaus — which are actually what interest us most.

We have an optimization problem, and we know that relying on a single swarm converging to a single point is a gamble. This raises the question: Are there algorithms that can explore multiple regions of space in parallel without sacrificing convergence speed? In our series of articles on population-based optimization algorithms, we have already examined dozens of metaheuristics — from classic swarm-based algorithms to the most exotic ones — and run each through the same test bench: the Hilly, Forest, and Megacity functions, three problem dimensionalities, a fixed budget of 10,000 calls to the objective function, and ten repetitions. This standardized protocol lets us compare all the algorithms under equal conditions rather than taking their developers at their word. The series' ranking table has shown time and again that a catchy name and an appealing metaphor from nature are no guarantee of success.

Today's guest in the series is the Coyote Optimization Algorithm (COA), proposed by Juliano Pierezan and Leandro dos Santos Coelho in 2018. At first glance, it is just another animal in the sprawling zoo of metaheuristics. But COA has a distinctive structural feature: its population is divided from the outset into isolated packs, each of which leads its own social life, evolves around its own leader, and only occasionally exchanges individuals with its neighbors. It is not a swarm that breathes as a single entity — it is an archipelago of small populations that naturally preserves diversity. There are also two mechanisms absent from a typical swarm: the pack's collective culture, expressed through the median (rather than the mean — and this matters), and the birth of pups — genetic crossover built directly into the swarm-based scheme.

By the end of this article, the reader will have an understanding of the social mechanics of COA, a ready-to-use class implementation within the standard C_AO architecture with an explanation of all adaptations of the original authors’ sequential scheme to our test bench, honest results across the full set of functions, and the algorithm’s position in the series’ ranking table. Along the way, the reader will also get an answer to a practical question: does the pack structure offer a real advantage in maintaining diversity, or is it merely an attractive wrapper around an ordinary swarm? First, we will break down the concept and the math behind the algorithm using real-life examples, then implement it in MQL5, discuss the pitfalls of porting it, and test the result on the test bench.


Implementation of the Algorithm

Metaphor: not a swarm, but a society

Most swarm algorithms simulate a crowd: all agents see the global leader and move toward it. COA models something more complex — a society with a social structure.

In the wild, coyotes live in packs of several individuals. Every pack has an alpha — the fittest individual. The pack also has collective experience — its culture: a set of behaviors that belongs to no single individual but is shared by all. Coyotes learn from one another; pups are born in packs and inherit their parents’ traits; and sometimes an individual leaves one pack to join another, bringing its accumulated experience with it.

To put this in a trader's terms, imagine not just one team of trading system developers, but several independent teams. Each team has its own leader with the best current performance and its own school of thought — a trading style typical of that team. There is a constant exchange of ideas within the team; from time to time, hybrid strategies emerge — pieced together from parts of two other strategies — and occasionally a trader moves from one team to another, bringing a fresh perspective with them. Several teams will almost certainly explore different trading ideas; one large team will almost certainly settle on just one. Now, let's talk about the mechanisms.

Coyote = solution, pack = subpopulation. Each coyote is a vector of problem parameters — that is, a candidate solution: in our terminology, a specific set of trading system parameters. A coyote's social state is its position in the search space; its fitness is the value of the objective function.

A population of popSize coyotes is divided into nPacks packs, each containing nCoy individuals (the authors use 20 packs of 5 coyotes each). The grouping is random and — crucially — remains almost unchanged: the packs are not re-sorted by fitness at every iteration, as in some algorithms with castes. The composition of the pack changes only on rare occasions when an exchange takes place, as discussed below. It is precisely this stable isolation that allows packs to disperse across different areas of the landscape.

Alpha and pack culture. At each iteration, two reference points are determined for each pack. The first is the alpha: the best coyote in the pack in terms of fitness. This is the equivalent of a local leader — not a global one! Each pack looks up to its own champion, not the world champion. If one pack finds a high peak, the others do not abandon their areas and continue digging where they are. The second is the pack's social tendency: the coordinate-wise median of the positions of all its coyotes. This is the formalization of culture — a generalized portrait of the pack that does not correspond to any single individual.

Why the median rather than the mean — that is a detail worth considering. Imagine a team of five traders, four of whom trade using a moving average with a period of about 20, while the fifth — an experimenter — uses a period of 200. The arithmetic mean comes out to 56 — a value that no one uses in trading and that reflects neither the consensus nor the experiment. The median gives 20 — an honest reflection of “how things are done on this team.” The median is resistant to outliers: a single coyote that has strayed to the edge of the space does not drag the entire pack’s culture along with it. In an algorithm where outliers are a normal result of exploration, this is a deliberate engineering decision.

Social move: learning from the leader and the environment. Primary movement operator. Every coyote in the pack (except for one — its place will be taken by a pup, as explained below) builds a new social state according to the following formula:

new = x + r1 * (alpha - x_rc1) + r2 * (tend - x_rc2)

where:

  • x — the coyote's current position,
  • alpha — the alpha's position,
  • tend — the pack's cultural tendency,
  • x_rc1 and x_rc2 are two randomly selected pack members from the same pack (distinct from each other and from the coyote itself),
  • r1, r2 are random numbers from [0, 1].

Let's break down what is actually happening here. The coyote takes two steps at once. The first is what I learn from the leader: the vector (alpha - x_rc1) shows how far and in which direction the alpha differs from a randomly selected ordinary pack member. Second — what my environment teaches me: the vector (tend - x_rc2) shows how much a random pack member deviates from the cultural norm and pulls toward that norm.

A real-life example: You are a trader on a team. You look at the leader and a randomly selected colleague and think, “The leader's stop-loss is half as wide as Petya's — I will try tightening mine, too.” Then you look at the team's usual practice and another colleague: “On our team, we apply a filter to Asian-session trading, but Vasya does not — I suppose I am closer to the team's practice.” Importantly, in both cases the reference point is not the learner itself, but randomly selected pack members. This introduces a stochastic component into the step that depends on the current spread of the pack — as long as the pack is spread out, the steps are large; when the pack contracts around a peak, the steps automatically become smaller. A simple, built-in mechanism for transitioning from exploration to refinement, without any coefficients that decay with the iteration number.

Acceptance of a new state is greedy and strict: a coyote moves to a new point only if it is better than the previous one. No accepting worse solutions for the sake of diversity — other mechanisms handle diversity in COA.

Birth of a Pup: Genetics Within the Swarm

The most unusual part of the algorithm. Once per iteration, a pup is born in each pack — a new solution assembled from the genes of two randomly selected parents who are pack members, plus fresh random material.

Here's how it works. Two different parents are selected at random. Next, each coordinate's fate is decided: it is inherited from the first parent, inherited from the second, or generated randomly anew (mutation). The algorithm ensures that at least one coordinate is taken from the first parent and at least one from the second: the pup cannot be a pure clone or an entirely random solution. The mutation probability for each coordinate is Ps = 1/D, where D is the dimensionality of the problem, so on average one coordinate mutates per pup, regardless of dimensionality; otherwise, the coordinate is inherited from either parent with equal probability, (1 - Ps)/2 for each.

In trading system terms: let’s take Masha’s system and Igor’s system. For the new system, the channel period comes from Masha, the take-profit coefficient comes from Igor, the volatility filter comes from Masha, and we randomly select the trading session time. The result is a hybrid that no one had before — and sometimes such a hybrid really takes off where both parents were mediocre, because a successful combination of parameters does not necessarily lie on the line segment between two successful solutions. The social move is a continuous operator: it can move along directions; crossover is a combinatorial operator: it can jump to corners of the space that smooth motion cannot reach. In COA, they work together — a combination rarely found in swarm algorithms.

A pup's fate is determined by natural selection: it takes the place of the worst-performing coyote in the pack — if it proves to be better than that coyote. This is where age comes into play: all else being equal, the oldest of the worst-performing coyotes is replaced. The age of all coyotes increases by one each iteration; a newborn pup starts at zero. The pack slowly but continuously rejuvenates, and veterans whose progress has stalled cannot keep their places forever just because of their seniority.

Inter-pack exchange: rare, but important. If the packs were completely isolated, the algorithm would degenerate into a set of independent small optimizations — each with its own budget and its own dead ends. The authors introduce a limited communication channel: with a probability of p_leave = 0.005 * nCoy² per iteration, two randomly selected coyotes from two randomly selected packs swap places.

With a standard pack size of 5 coyotes, this works out to 0.125 — the exchange occurs approximately once every eight iterations. Rarely enough for the packs to retain their individual identities, and often enough for a discovery made by one pack to eventually migrate to its neighbors. This is the very same archipelago model from the theory of island genetic algorithms: isolation preserves diversity, while migration spreads success. Note the quadratic dependence on pack size — the authors calibrated the exchange rate so that large packs, which have more than enough internal diversity, would exchange members more frequently and avoid stagnation.

Putting the Whole Picture Together

A "year" in the life of a population looks like this. Within each pack: the alpha was identified, the cultural median was calculated, each coyote made a social move with greedy acceptance, and a pup was born and competed for a place in the pack. Between packs: a pair of individuals may have been exchanged. Everyone is a year older. The best solution across all packs is the algorithm's current output.

This scheme clearly illustrates the division of labor among the mechanisms: the social move handles local refinement and convergence, pup crossover enables combinatorial leaps and major discoveries, the pack structure maintains multiple search regions in parallel, exchange spreads success, and age drives membership turnover. None of the operators duplicates another — a welcome rarity for an algorithm devised in 2018, at a time when new metaheuristics were churned out simply by renaming old ones.

The test bench will show just how well this design works in practice. In the meantime, let's move on to the implementation in MQL5 and look at the areas where we had to carefully adapt the original sequential scheme to our C_AO architecture.

COA_scheme

Figure 1. The Coyote Optimization Algorithm: The Social Structure of Search

In the illustration, the population is divided into isolated packs, each of which occupies its own region of the multimodal landscape. Each pack follows its own alpha (the pack’s best coyote) rather than a single global leader, which naturally preserves diversity. A pack’s social tendency is the coordinate-wise median of its members; unlike the mean, it is resistant to outliers. Rare migration (probability p_leave = 0.005·nCoy² per iteration, approximately once every 8 iterations when nCoy = 5) swaps two coyotes between two random packs, spreading successful traits throughout the archipelago.

  1. Social movement: Each coyote simultaneously takes a step toward the alpha and toward the pack culture, using two random pack members (rc1 and rc2) as reference points; a new position is adopted only if it strictly improves the solution.

  2. Pup birth: Once per iteration, each pack produces a pup through binary crossover between two randomly selected parents — at least one gene is guaranteed to be inherited from each parent, and each gene mutates to a new random value with probability Ps = 1/D.

  3. Age and replacement: Each coyote ages by one unit per iteration; a pup replaces the worst coyote in the pack (the oldest in the event of a fitness tie) only if the pup is better, so the pack is continually rejuvenated without losing quality.

Pseudocode for the COA algorithm:

popSize, nCoy, nPacks = popSize/nCoy, coords    — sizes
pLeave  = 0.005 * nCoy^2                        — pack exchange probability (Eq. 4)
Ps      = 1/coords                              — pup coordinate mutation probability
probPup = (1 - Ps)/2                            — coordinate inheritance probability
x[i], f[i]                                      — coyote coordinates and fitness
snap_c[i], snap_f[i]                            — state at the start of the epoch
age[i], isPup[i]                                — coyote age and pup flag
pack[p]                                         — indices of the nCoy coyotes in pack p
cB, fB                                          — globally best coordinates and fitness

INIT
StandardInit(rangeMin, rangeMax, rangeStep)
pLeave  = 0.005 * nCoy^2;   probPup = (1 - 1/coords)/2
for i, c:  cP[i][c] = random_uniform(rangeMin[c], rangeMax[c])   # initial population
age[*] = 0;  isPup[*] = false
indices = shuffle([0 .. popSize-1])                              # Fisher–Yates
distribute the shuffled indices among[0..nPacks-1], with nCoy indices per pack

MOVING
if first pass:                       # only cP → c, so that the test bench can evaluate FF
    for i, c:  x[i][c] = bring_into_range(cP[i][c])
    return

for i:  snap_c[i] = x[i];  snap_f[i] = f[i]      # 1. epoch starting point

if nPacks > 1 and random() < pLeave:             # 2. rare exchange between packs (Eq. 4)
    exchange pack[p1][c1] <-> pack[p2][c2]  for random p1!=p2, c1, c2

isPup[*] = false

for p = 0 .. nPacks-1:                            # 3. operations inside the pack
    alpha = argmax_{k in pack[p]} snap_f[k]               # pack alpha (Eq. 5)
    for c:  tend[c] = median_{k in pack[p]} snap_c[k][c]  # social tendency = median (Eq. 6)
    pup_slot = worst by snap_f (if equal — older by age)   # pup slot
    isPup[pup_slot] = true

    for each i from pack[p]:
        if i == pup_slot:                         # PUP BIRTH (Eq. 7, Alg. 1)
            select two different parents par1, par2 from the pack
            pdr = shuffle([0 .. coords-1])
            mask[*] = 0;  mask[pdr[0]] = 1;  mask[pdr[1]] = 2    # minimum 1 from each
            for j = 2 .. coords-1:
                r = random()
                if   r < probPup:      mask[pdr[j]] = 1
                elif r > 1 - probPup:  mask[pdr[j]] = 2          # otherwise 0 = mutation
            for c:
                v = (mask[c]==1) ? snap_c[par1][c] :
                    (mask[c]==2) ? snap_c[par2][c] :
                                   random_uniform(rangeMin[c], rangeMax[c])
                x[i][c] = bring_into_range(v)
        else:                                     # SOCIAL MOVE (Eq. 12)
            select rc1, rc2 from the pack (both != i, rc1 != rc2)
            r1 = random();  r2 = random()                       # scalars per agent
            for c:
                v = snap_c[i][c]
                  + r1*(snap_c[alpha][c] - snap_c[rc1][c])      # attraction to the leader
                  + r2*(tend[c]          - snap_c[rc2][c])      # attraction to pack culture
                x[i][c] = bring_into_range(v)

After Moving the test bench evaluates f[i] for all agents — exactly popSize FF calls.

REVISION
for i:                                            # updating the best solutions
    update the personal best if f[i] is higher
    if f[i] > fB:  fB = f[i];  cB = x[i]

if first_pass:  revision = true;  return

for i:                                            # adaptation (Eq. 14): greedy acceptance
    accepted = (f[i] > snap_f[i])
    if not accepted:  x[i] = snap_c[i];  f[i] = snap_f[i]      # rollback to the starting point
    age[i]++
    if isPup[i] and accepted:  age[i] = 0                      # a pup's age is reset to zero

EXTERNAL LOOP (stand)
Init();  Moving();  evaluate f[*];  Revision()
for epoch = 1 .. epochCount:
    Moving();  evaluate f[*];  Revision()
response = cB (fitness fB)

Now for the implementation. The C_AO_COA_Coyote class inherits from the base C_AO class and implements the Coyote Optimization Algorithm within the standard architecture used in this series: all iteration logic is split between two methods — Moving (generating new candidates) and Revision (deciding whether to accept candidates and updating the best solutions) — which the test bench’s outer loop calls in turn at each epoch. The base class is not modified.

The user-configurable parameter 'nCoy' specifies the number of coyotes in a pack and, together with the inherited popSize, determines the auxiliary variable nPacks = popSize / nCoy — the number of packs. The parameters pLeave (the probability of individual exchange between packs, equal to 0.005·nCoy²) and probPup (the probability that a pup will inherit a coordinate, equal to (1−1/coords)/2) are calculated once in 'Init', since they depend only on 'nCoy' and the dimensionality.

The composition of the packs is stored in a flat array 'packs' of length 'nPacks·nCoy': the indices of the coyotes belonging to pack 'p' lie in the slice packs[p·nCoy .. p·nCoy+nCoy−1]. The  'ages' array stores the age of each coyote, and 'isPup' marks the slot occupied by a pup in the current epoch.

A snapshot of the population's state at the beginning of the epoch is stored in snap_c (a flat coordinate buffer, popSize·coords) and snap_f (fitness values, popSize) — all moves in the epoch are calculated relative to this snapshot, and rejected candidates are rolled back to it. The working buffers medBuf (for calculating the pack median), tend (social tendency), pdr (permutation of dimensions when a pup is born), and mask (the source of each coordinate of the pup) are reused and do not retain state between epochs.

The constructor sets the algorithm's identifying information (ao_name, ao_desc, and a link to the article) and default values: popSize = 20, nCoy = 5. It registers two configurable parameters in the params array and immediately calculates nPacks so that the object is consistent even before SetParams is explicitly called.

SetParams reads popSize and nCoy from the params array (where an external script places them before execution) and recalculates nPacks = popSize / nCoy. popSize is intentionally left unadjusted: it is an external parameter that the user is responsible for setting to a multiple of nCoy.

//+------------------------------------------------------------------+
//|                            Class                                 |
//+------------------------------------------------------------------+
class C_AO_COA_Coyote : public C_AO
 {
public:
                    ~C_AO_COA_Coyote() {}
                     C_AO_COA_Coyote()
   {
    ao_name = "COA(Coyote)";
    ao_desc = "Coyote Optimization Algorithm";
    ao_link = "https://www.mql5.com/en/articles/23053";

    popSize = 20;
    nCoy    = 5;

    ArrayResize(params, 2);
    params [0].name = "popSize";
    params [0].val = popSize;
    params [1].name = "nCoy";
    params [1].val = nCoy;

    nPacks = popSize / nCoy;
   }

  void               SetParams()
   {
    popSize = (int)params [0].val;
    nCoy    = (int)params [1].val;

    nPacks = popSize / nCoy;
   }

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

  void               Moving();
  void               Revision();

  //--- visible parameters
  int                nCoy;       // coyotes in a pack (original: 5)

private:
  int                nPacks;     // number of packs = popSize / nCoy
  double             pLeave;     // probability of exchange between packs: 0.005*nCoy^2
  double             probPup;    // (1 - Ps)/2, where Ps = 1/coords

  int                packs  [];  // [nPacks * nCoy] — agent indices grouped by pack
  int                ages   [];  // [popSize] — coyote age
  bool               isPup  [];  // [popSize] — pup slot in the current epoch

  //--- snapshot for greedy acceptance
  double             snap_c [];  // [popSize * coords]
  double             snap_f [];  // [popSize]

  //--- working buffers
  double             medBuf [];  // [nCoy]   — for the median
  double             tend   [];  // [coords] — pack's social tendency
  int                pdr    [];  // [coords] — permutation of dimensions (pup)
  int                mask   [];  // [coords] — 0: noise, 1: parent 1, 2: parent 2
 };

Initialization prepares the algorithm for a new run. First, it calls the base class's StandardInit method (which allocates the population a[] and stores the search bounds), then populates the proposal buffer cP[] with random coordinates for the initial population. On the first call to Moving, these coordinates will be transferred to the working coordinates c[]. Here, pLeave and probPup are calculated, all auxiliary buffers are allocated, and all ages and pup flags are reset to zero. The method concludes by randomly dividing the population into packs: the indices 0..popSize−1 are shuffled using the Fisher–Yates algorithm and placed in packs.

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

//--- initial random population in cP[] (Eq. 2)
  for(int i = 0; i < popSize; i++)
    for(int c = 0; c < coords; c++)
      a [i].cP [c] = u.RNDfromCI(rangeMin [c], rangeMax [c]);

//--- parameters that depend on nCoy and coords
  pLeave  = 0.005 * (double)nCoy * (double)nCoy;          // Eq. 4
  probPup = (coords > 0) ? (1.0 - 1.0 / (double)coords) * 0.5 : 0.0; // (1-Ps)/2

//--- buffers
  ArrayResize(snap_c, popSize * coords);
  ArrayResize(snap_f, popSize);
  ArrayResize(packs,  nPacks * nCoy);
  ArrayResize(ages,   popSize);
  ArrayResize(isPup,  popSize);
  ArrayResize(medBuf, nCoy);
  ArrayResize(tend,   coords);
  ArrayResize(pdr,    coords);
  ArrayResize(mask,   coords);

  ArrayInitialize(ages, 0);
  ArrayInitialize(isPup, false);

//--- random distribution of coyotes among packs (Fisher–Yates)
  for(int i = 0; i < popSize; i++)
    packs [i] = i;
  for(int j = popSize - 1; j > 0; j--)
   {
    int r     = u.RNDminusOne(j + 1);
    int tmp   = packs [j];
    packs [j] = packs [r];
    packs [r] = tmp;
   }

  return true;
 }

Moving generates new candidates for all agents in a single epoch without calling the objective function (which is evaluated by the outer loop after returning from the method). On the first run (revision == false), the method simply transfers the initial population from cP[] to the working coordinates c[], snapping the coordinates to the grid and enforcing the bounds, and then exits — this allows the test bench to evaluate the initial population.

In subsequent epochs, the method proceeds as follows: it takes a snapshot of the current population in snap_c / snap_f; with probability pLeave, it swaps two randomly selected coyotes, one from each of two randomly selected packs (migration, Eq. 4); clears the pup flags. It then determines the alpha for each pack (the coyote with the maximum snapshot fitness, Eq. 5), calculates the social tendency as the coordinate-wise median of the pack members (Eq. 6) and assigns the pup slot to the coyote with the lowest fitness score; in the event of a tie, the oldest one is chosen.

After that, each coyote in the pack is assigned a new candidate. The coyote in the pup slot is reborn (Eq. 7, Alg. 1): two distinct parents are selected, the dimensions are shuffled, and, according to a mask, each coordinate is taken from the first parent, taken from the second parent, or generated at random (mutation with probability 1/coords), with each parent guaranteed to contribute at least one coordinate. The remaining coyotes make a social move (Eq. 12): attraction to the alpha and attraction to the pack's social tendency are added to the snapshot position, scaled by the scalars r1 and r2, respectively, randomly sampled for each agent. All new coordinates are adjusted to satisfy the search bounds and align with the search grid.

//+--------------------------------------------------------------------+
//|                            Moving                                  |
//|                                                                    |
//|  Epoch structure:                                                  |
//|    1. Population snapshot;                                         |
//|    2. Infrequent exchange of coyotes between packs (Eq. 4);        |
//|    3. For each pack: alpha, social tendency (median), pup slot;    |
//|    4. social moves (Eq. 12) for all slots except the pup slot;     |
//|    5. Into the pup slot: crossover of two parents + noise (Alg. 1).|
//+--------------------------------------------------------------------+
void C_AO_COA_Coyote::Moving()
 {
//--- first pass: cP -> c, so that the outer loop evaluates FF
  if(!revision)
   {
    for(int i = 0; i < popSize; i++)
      for(int c = 0; c < coords; c++)
        a [i].c [c] = u.SeInDiSp(a [i].cP [c], rangeMin [c], rangeMax [c], rangeStep [c]);
    return;
   }

//--- snapshot of the current population
  for(int i = 0; i < popSize; i++)
   {
    for(int c = 0; c < coords; c++)
      snap_c [i * coords + c] = a [i].c [c];
    snap_f [i] = a [i].f;
   }

//--- exchange of coyotes between packs (Eq. 4)
  if(nPacks > 1 && u.RNDprobab() < pLeave)
   {
    int p1 = u.RNDminusOne(nPacks);
    int p2 = p1;
    while(p2 == p1)
      p2 = u.RNDminusOne(nPacks);

    int c1 = u.RNDminusOne(nCoy);
    int c2 = u.RNDminusOne(nCoy);

    int tmp = packs [p1 * nCoy + c1];
    packs [p1 * nCoy + c1] = packs [p2 * nCoy + c2];
    packs [p2 * nCoy + c2] = tmp;
   }

  ArrayInitialize(isPup, false);

//--- operations within each pack
  for(int p = 0; p < nPacks; p++)
   {
    int base = p * nCoy;

    //--- pack alpha (Eq. 5): maximum fitness (the test bench performs maximization)
    int aIdx = packs [base];
    for(int k = 1; k < nCoy; k++)
     {
      int idx = packs [base + k];
      if(snap_f [idx] > snap_f [aIdx])
        aIdx = idx;
     }

    //--- social tendency (Eq. 6): the coordinate-wise median of the pack
    for(int c = 0; c < coords; c++)
     {
      for(int k = 0; k < nCoy; k++)
        medBuf [k] = snap_c [packs [base + k] * coords + c];
      ArraySort(medBuf);
      tend [c] = (nCoy % 2 == 1) ? medBuf [nCoy / 2]
                 : 0.5 * (medBuf [nCoy / 2 - 1] + medBuf [nCoy / 2]);
     }

    //--- pup slot: lowest fitness; ties broken by greater age
    int wK = 0;
    for(int k = 1; k < nCoy; k++)
     {
      int idx  = packs [base + k];
      int wIdx = packs [base + wK];
      if(snap_f [idx] <  snap_f [wIdx] ||
         (snap_f [idx] == snap_f [wIdx] && ages [idx] > ages [wIdx]))
        wK = k;
     }
    int pupIdx = packs [base + wK];
    isPup [pupIdx] = true;

    //--- movement of the coyotes in the pack
    for(int k = 0; k < nCoy; k++)
     {
      int i = packs [base + k];

      if(i == pupIdx)
       {
        //--- THE BIRTH OF A PUP (Eq. 7, Alg. 1)
        //    two distinct parents selected at random from the pack
        int k1 = u.RNDminusOne(nCoy);
        int k2 = k1;
        while(k2 == k1)
          k2 = u.RNDminusOne(nCoy);
        int par1 = packs [base + k1];
        int par2 = packs [base + k2];

        //--- dimension permutation (Fisher–Yates)
        for(int c = 0; c < coords; c++)
          pdr [c] = c;

        for(int j = coords - 1; j > 0; j--)
         {
          int r   = u.RNDminusOne(j + 1);
          int tmp = pdr [j];
          pdr [j] = pdr [r];
          pdr [r] = tmp;
         }

        //--- mask: one coordinate guaranteed from each
        //    parent; the remaining coordinates come from parent 1 / parent 2 / noise
        ArrayInitialize(mask, 0);
        mask [pdr [0]] = 1;
        if(coords > 1)
          mask [pdr [1]] = 2;
        for(int j = 2; j < coords; j++)
         {
          double r = u.RNDprobab();
          if(r < probPup)
            mask [pdr [j]] = 1;
          else
            if(r > 1.0 - probPup)
              mask [pdr [j]] = 2;
          // otherwise 0 — uniform noise along the coordinate
         }

        for(int c = 0; c < coords; c++)
         {
          double v;
          if(mask [c] == 1)
            v = snap_c [par1 * coords + c];
          else
            if(mask [c] == 2)
              v = snap_c [par2 * coords + c];
            else
              v = u.RNDfromCI(rangeMin [c], rangeMax [c]);

          a [i].c [c] = u.SeInDiSp(v, rangeMin [c], rangeMax [c], rangeStep [c]);
         }
       }
      else
       {
        //--- SOCIAL MOVE (Eq. 12) 
        //    rc1, rc2 — distinct, neither equal to k
        int k1 = k;
        while(k1 == k)
          k1 = u.RNDminusOne(nCoy);
        int k2 = k;
        while(k2 == k || k2 == k1)
          k2 = u.RNDminusOne(nCoy);
        int rc1 = packs [base + k1];
        int rc2 = packs [base + k2];

        //--- r1, r2 — scalars per agent, as in the original
        double r1 = u.RNDprobab();
        double r2 = u.RNDprobab();

        for(int c = 0; c < coords; c++)
         {
          double v = snap_c [i * coords + c] +
                     r1 * (snap_c [aIdx * coords + c] - snap_c [rc1 * coords + c]) +
                     r2 * (tend [c]                   - snap_c [rc2 * coords + c]);

          a [i].c [c] = u.SeInDiSp(v, rangeMin [c], rangeMax [c], rangeStep [c]);
         }
       }
     } // k
   } // p
 }

Revision is called after the outer loop has computed the fitness of all new candidates. First, the method updates the agents' personal bests and the global best (fB, cB). On the first run, it simply sets the "revision" flag and exits — there is not a working population for comparison yet.

In subsequent epochs, adaptation is performed (Eq. 14): a candidate is accepted only if its fitness shows a strict improvement over the snapshot; otherwise, the agent's coordinates and fitness are rolled back (greedy acceptance applies to both social moves and pups). Finally, all coyotes' ages are incremented by one, while an accepted pup's age is reset to zero — this maintains continuous turnover within the pack, preventing stalled veterans from occupying a slot indefinitely.

//+---------------------------------------------------------------------+
//|                           Revision                                  |
//|                                                                     |
//|  Standard fB/cB update + adaptation (Eq. 14): candidate             |
//|  is accepted only upon STRICT improvement; otherwise, rollback to   |
//|  the snapshot (applies to both social moves and the pup). Age of all|
//|  coyotes increases by 1 per epoch; an accepted pup resets the       |
//|  slot's age.                                                        |
//+---------------------------------------------------------------------+
void C_AO_COA_Coyote::Revision()
 {
//--- update personal and global bests
  for(int i = 0; i < popSize; i++)
   {
    if(a [i].f > a [i].fB)
     {
      a [i].fB = a [i].f;
      ArrayCopy(a [i].cB, a [i].c, 0, 0, coords);
     }
    if(a [i].f > fB)
     {
      fB = a [i].f;
      ArrayCopy(cB, a [i].c, 0, 0, coords);
     }
   }

  if(!revision)
   {
    revision = true;
    return;
   }

//--- adaptation (Eq. 14): acceptance only upon strict improvement
  for(int i = 0; i < popSize; i++)
   {
    bool accepted = (a [i].f > snap_f [i]);

    if(!accepted)
     {
      for(int c = 0; c < coords; c++)
        a [i].c [c] = snap_c [i * coords + c];
      a [i].f = snap_f [i];
     }

    //--- age
    ages [i]++;
    if(isPup [i] && accepted)
      ages [i] = 0;
   }
 }

The test bench's outer loop repeats the sequence: Moving → fitness calculation → Revision. Moving prepares candidates from a single snapshot; the test bench evaluates them with exactly popSize calls to the objective function; Revision decides their fate and updates the best solutions. The division into two methods is a requirement of the series' architecture: the fitness evaluation has been moved outside the algorithm, which allows for a uniform comparison of all algorithms on a common test bench.


Test Results

On the full standard test bench, COA scored 63.43%, a high score.

COA(Coyote)|Coyote Optimization Algorithm|15.0|5.0|
=============================
5 Hilly's; Func runs: 10000; result: 0.8890955934993101
25 Hilly's; Func runs: 10000; result: 0.7068146139756495
500 Hilly's; Func runs: 10000; result: 0.32718611958402094
=============================
5 Forest's; Func runs: 10000; result: 0.9946702798798231
25 Forest's; Func runs: 10000; result: 0.8535899004786041
500 Forest's; Func runs: 10000; result: 0.15152116248097336
=============================
5 Megacity's; Func runs: 10000; result: 0.8853333333333333
25 Megacity's; Func runs: 10000; result: 0.7104000000000001
500 Megacity's; Func runs: 10000; result: 0.18981333333333356
=============================
All score: 5.70842 (63.43%)

The composite anti-cheat test yields correspondingly high results:

COA(Coyote)|Coyote Optimization Algorithm|20.0|5.0|
=============================
Composite test: Hilly + Forest + Megacity + Peaks + Skin
Coordinates: 10; Epochs: 500; Repeats: 10
=============================
Run 1/10: 0.9866664583248237
Run 2/10: 0.9857396707649635
Run 3/10: 0.9954235254499133
Run 4/10: 0.9827156480516207
Run 5/10: 0.9866665766231328
Run 6/10: 0.9999999744010715
Run 7/10: 0.9989488644865328
Run 8/10: 0.9999999476895154
Run 9/10: 0.9866620346820829
Run 10/10: 0.9728312966736997
=============================
Average result: 0.9895653997 (98.96%)
=============================

Visualization of the algorithm's operation on a test bench, as well as on some other functions.

Hilly

COA on the Hilly test function

Forest

COA on the Forest test function

Megacity

COA on the Megacity test function

Shaffer

COA on the Shaffer test function

GoldsteinPrice

COA on the GoldsteinPrice test function

Based on the results, the COA algorithm ranks 11th in our ranking of the best optimization methods.

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) 1,000 p (500 F) 10 p (5 F) 50 p (25 F) 1,000 p (500 F) 10 p (5 F) 50 p (25 F) 1,000 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 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
38 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
39 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
40 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
41 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
42 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
43 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
44 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
45 ACOm ant colony optimization_M 0.71885 0.48410 0.30990 1.51285 0.75792 0.48639 0.11871 1.36302 0.83600 0.48667 0.16148 1.48415 4.360 48.44
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


Conclusions

Let's recall the problem that brought the trader to this article. Premature convergence is the main enemy of anyone seeking not just a single overoptimized maximum, but a stable region of good parameters. The question that brought the trader here was: Is there a mechanism capable of exploring multiple regions of space in parallel without sacrificing convergence speed?

The Coyote Optimization Algorithm answers this question through its structure. Its answer is not a single swarm breathing as one, but an archipelago of isolated packs, each with its own leader and culture, occasionally exchanging individuals. The division of labor among the operators turned out to be exceptionally clear-cut: the pack structure maintains diversity, the social move refines solutions, the median-based social tendency gives the pack an outlier-resistant reference point, pup crossover adds combinatorial leaps into corners of space unreachable by smooth movement, and the age mechanism continually renews pack membership. No operator duplicates the role of another.

On the full standard test bench, COA scored 63.43%, placing it in the upper part of the series' ranking table. COA is a powerful optimizer specifically for the range of dimensionalities typical of trading system optimization, where there are usually dozens, rather than hundreds, of parameters.

The most important result, however, is not the absolute number, but its credibility. The composite anti-cheat test (five different functions — Hilly, Forest, Megacity, Peaks, and Skin — with optima shifted off the diagonal) yielded a score of 98.96%. This is of fundamental importance. COA passed it flawlessly, proving that its high score on the standard test bench was earned through genuine search rather than being an artifact of the test functions' geometry. The algorithm is honest.

The reader now has a working implementation of C_AO_COA_Coyote in the series' standard architecture, explained method by method and supported by pseudocode; an understanding of COA's social mechanics sufficient to explain them to a colleague in simple terms; and a credible evaluation validated by the anti-cheat test that places the algorithm in the upper part of the rankings. The original practical question has also been answered: the pack structure really does maintain several search regions in parallel, rather than merely serving as an attractive wrapper around a conventional swarm — and combining this structure with built-in crossover counters premature convergence, the very problem that started it all.

The methodological lesson of the series itself is worth noting separately. A high score on the standard test bench guarantees nothing on its own until it has been validated by the composite anti-cheat test. It is precisely this discipline — not believing a promising result until it has withstood the test on off-diagonal problems — that distinguishes an honest comparison of algorithms from the promotional claims of their authors. COA has passed this test and deservedly ranks highly.

For a trader choosing an optimizer for a system with dozens of parameters and a rugged profit landscape, COA is a solid, reliable candidate that does not converge prematurely on the first local peak and maintains diversity where a typical swarm loses it.

tab

Figure 2. Color-coded ranking of algorithms based on the corresponding tests

chart

Figure 3. A histogram of the algorithm test results (on a scale from 0 to 100, with higher scores indicating better results, where 100 is the theoretical maximum); the archive contains a script for calculating the ranking table


Pros and cons of the COA algorithm:

Pros:

  1. A small number of external parameters.
  2. Good convergence.

Cons:

  1. With a small population, the variation in results increases.

An archive containing the latest versions of the algorithm source code is attached to the article. The author of this article does not guarantee absolute accuracy in the descriptions of canonical algorithms; many of them have been modified to improve their search capabilities. The conclusions and judgments presented in the articles are based on the results of the experiments conducted.



Software used in this article

# Name Type Description
1 #C_AO.mqh
Include file
Parent class for population-based optimization algorithms
2 #C_AO_enum.mqh
Include file
Enumeration of population-based optimization algorithms
3 TestFunctions.mqh
Include file
Test function library
4
TestStandFunctions.mqh
Include file
Test bench function library
5 TestStand3D.mqh Include file 3D visualization panel for the test bench
6 Utilities.mqh
Include file
Utility function library
7 CalculationTestResults.mqh
Include file
Script for calculating results for the comparison table
8 Test_AO_All.mq5
Script Unified test bench for all population-based optimization algorithms
9 Test_AO_AntiCheat Script Test for cheating in optimization algorithms
10 Simple use of population optimization algorithms.mq5
Script
Simple example of using population-based optimization algorithms without visualization
11 Test_AO_COA.mq5
Script Test bench for COA

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

Attached files |
COA_Coyote.zip (415.27 KB)
Self-Learning SuperTrend: An Adaptive Machine Learning-Based Trend Indicator Self-Learning SuperTrend: An Adaptive Machine Learning-Based Trend Indicator
The classic SuperTrend loses accuracy when the market regime changes because it uses fixed ATR parameters and a fixed multiplier. The article examines the architecture of ML SuperTrend Pro v2.00 written entirely in MQL5: a background test matrix with adaptive parameter updates, a regime grid as a context detector, an accuracy layer consisting of five filters, and Parabolic-style rendering with well-designed buffers. The article presents the principles of L1 regularization, the results of a comparison with the classic SuperTrend, and practical recommendations for deployment and integration via iCustom.
Measuring Spread Costs Before a Market Order in MQL5 Measuring Spread Costs Before a Market Order in MQL5
The article implements a MetaTrader 5 Expert Advisor that provides spread-only pre-trade estimates in points for a planned Buy or Sell. It compares a current-spread result with a historical scenario that uses the maximum of the live spread and a selected percentile from recent Bid/Ask ticks, and shows both on a collapsible panel. Manual market execution is available without SL/TP, with readiness checks kept separate from the estimates.
Features of Experts Advisors Features of Experts Advisors
Creation of expert advisors in the MetaTrader trading system has a number of features.
Estimate the Probability of Large-range Sessions (Part 1): Build and Check EURUSD Forecasts Estimate the Probability of Large-range Sessions (Part 1): Build and Check EURUSD Forecasts
We estimate the probability, at broker time 08:00, that EURUSD's 08:00–16:00 range exceeds 1.25 times the mean of the previous 20 complete, valid target ranges. A two-feature SPSS logistic regression model employs the completed 00:00–08:00 H1 window. Coefficients from 2020–2023 sessions are fixed for the 2024 holdout comparison against constant and 60-outcome rolling frequency baselines, using Brier scores and log losses. MQL5 scripts allow readers to extract broker data and recreate the probabilities in MetaTrader 5.