Ecological Cycle Optimizer (ECO)
Table of Contents
Introduction
Over billions of years, nature has refined the mechanisms of survival, adaptation, and self-organization in living systems. These mechanisms consistently attract the attention of researchers in the field of computational intelligence who seek to translate effective natural strategies into optimization algorithms. Genetic algorithms draw inspiration from evolution, swarm-based methods from the collective behavior of insects and birds, and immune system algorithms model the body's defense mechanisms.
However, until recently, one of the fundamental processes of the biosphere — the ecological cycle, a continuous circulation of energy and matter between the various trophic levels of an ecosystem — had been largely overlooked. Every stable ecosystem contains a complex network of interactions: producers (plants) convert solar energy into organic matter; herbivores consume producers; carnivores prey on herbivores; omnivores occupy intermediate niches; and decomposers close the cycle by breaking down organic matter and returning nutrients to the environment.
This elegant system of balance and interdependence formed the basis of the Ecological Cycle Optimizer (ECO) algorithm, presented in 2025 by a group of Chinese researchers led by Boyu Ma and Jiaxiao Shi. The authors proposed viewing a population of search agents as an ecosystem, in which each group of individuals plays its own role in the overall optimization cycle.
In the ECO algorithm, producers are elite solutions that accumulate “energy” in the form of a high objective function value. Herbivores gravitate toward producers, adopting their successful traits. Carnivores, in turn, orient themselves toward herbivores, while omnivores are able to obtain information from all trophic levels. The cycle concludes with the decomposition stage, during which all solutions undergo transformation — much like decomposers break down organic matter, creating the foundation for new growth.
A key feature of ECO is the adaptive predation coefficient, which regulates the intensity of interactions between groups. In the early stages of optimization, this coefficient is high, which promotes active information exchange and global exploration of the search space. As the algorithm approaches the optimum, the coefficient decreases, switching the algorithm to a mode of local exploitation of the promising regions that have been identified.
Three decomposition modes — optimal, local random, and global random — provide a variety of search strategies and help avoid premature convergence. Greedy selection during the revision phase ensures that the quality of solutions does not deteriorate from iteration to iteration.
In this article, we will examine the mathematical model of the ECO algorithm in detail, analyze the role of each component of the ecological cycle in the optimization process, and present testing results on a standard set of functions, enabling us to evaluate the algorithm’s effectiveness and its place among other metaheuristic methods.
Algorithm Implementation
The ECO algorithm simulates the energy cycle in a natural ecosystem. Imagine a meadow where grasses (producers) grow, hares (herbivores) graze, foxes (carnivores) hunt, bears (omnivores) feed, and bacteria and fungi (decomposers) break down organic matter. Each group plays its own role, and together they form a stable system.
In the ECO algorithm, the population of search agents is divided into analogous groups. Each agent represents a candidate solution to the optimization problem, and its “fitness” is the value of the objective function.
The population is divided into four groups in the following proportions:
| Group | Proportion | Role in the algorithm |
|---|---|---|
| Producers | 20% | Best solutions; source of information |
| Herbivores | 30% | Follow the producers |
| Carnivores | 30% | Follow the herbivores |
| Omnivores | 20% | Use information from all groups |
Example: For a population size of 50 agents, we get: 10 producers, 15 herbivores, 15 carnivores, and 10 omnivores.
The key parameter of the algorithm is the predation coefficient G, which controls the intensity of agent movement: G = 1 + 2 × rand × exp(-9 × (t/T)³) × sign, where: t is the current iteration; T is the maximum number of iterations; rand is a random number between 0 and 1; sign is a randomly selected sign (+1 or -1).
At the beginning of the optimization (t ≈ 0), the exponential term is close to 1, and the coefficient G can range from -1 to 3. Agents make large leaps as they explore the search space. Toward the end of the optimization (t → T), the exponential term approaches 0, G approaches 1, and the movements become more cautious — the algorithm transitions to local refinement.
Example: iteration 10 of 1,000: G ≈ 1 + 2 × 0.7 × 0.99 × (+1) = 2.39 — the agent can “jump over” the target; iteration 900 of 1,000: G ≈ 1 + 2 × 0.7 × 0.001 × (+1) = 1.001 — the agent moves almost exactly toward the target.
The producers’ strategy. Producers are the elite of the population. After each iteration, the entire population is sorted by fitness value, and the top 20% automatically become producers.
For example, if we have 50 agents with fitness values ranging from 10 to 100, then the 10 agents with the highest values (say, from 85 to 100) will become producers. They do not move actively — their role is to serve as “beacons” for the other groups.
The herbivores’ strategy. Herbivores move toward the producers, trying to “consume” good solutions. For each herbivore, 3 producers are selected using roulette-wheel selection (the best ones have a higher chance of being selected). The new position is calculated: new position = current + G × (r₁ × (prod₁ - current) + r₂ × (prod₂ - current) + r₃ × (prod₃ - current)), where r₁, r₂, and r₃ are random numbers from 0 to 1.
Example for one-dimensional space: The herbivore is located at x = 20. Producers have been selected at positions 80, 90, and 75. Random numbers: r₁ = 0.6, r₂ = 0.3, r₃ = 0.4. The coefficient G is 1.5. We compute: movement = 0.6 × (80 - 20) + 0.3 × (90 - 20) + 0.4 × (75 - 20) = 36 + 21 + 22 = 79; therefore, the new position will be 20 + 1.5 × 79 = 138.5. The herbivore moved toward the producers, and because G > 1, it “leaped” beyond the average position of the targets.
Carnivores’ strategy. Carnivores hunt herbivores using a similar pattern: three herbivores are selected using roulette-wheel selection, and the carnivore moves toward them, taking the predation coefficient G into account. Herbivores have already shifted toward good regions, following the producers. Carnivores “hunt” in these same promising zones, but with some offset, which helps explore the neighborhoods of the solutions found.
The omnivores’ strategy. Omnivores are versatile explorers. They receive information from all groups: 1 producer, 1 herbivore, and 2 carnivores are selected. The omnivore moves toward a combination of these targets: new position = current + G × (r₁ × (prod - current) + r₂ × (herb - current) + r₃ × (carn₁ - current) + r₄ × (carn₂ - current)). Omnivores can discover connections between different areas of the search space that other groups might have overlooked.
Roulette-wheel selection. When selecting movement targets, roulette-wheel selection is used — agents with better fitness are more likely to be selected. For example, three producers with fitness values of 100, 80, and 60. Selection probabilities (after normalization): producer 1: 100/240 = 41.7%; producer 2: 80/240 = 33.3%; producer 3: 60/240 = 25.0%. This does not mean that the worst agent will never be chosen — it is simply chosen less often. This mechanism preserves search diversity.
Decomposition strategy. After all groups have moved, a key phase begins — decomposition. Just as decomposers break down organic matter in nature, the algorithm transforms all solutions in one of three ways:
Mode 1: optimal decomposition (50% probability). The agent shifts toward the neighborhood of the best solution: neighborhood = best × rand_vector; new position = neighborhood + coef × (neighborhood - current), where coef ∈ [-0.2, 0.2] is a small random offset.
Example: best position: [100, 50]; current position: [70, 40]; rand_vector = [0.9, 0.8]; coef = 0.1; neighborhood = [100 × 0.9, 50 × 0.8] = [90, 40]; the new position is then calculated as [90 + 0.1 × (90 - 70), 40 + 0.1 × (40 - 40)] = [92, 40]. The agent has moved closer to the best solution with a small variation.
Mode 2: local random decomposition (25% probability); the agent takes a random step in an arbitrary direction, but the step size is proportional to the distance to the best solution: distance = ||best - current||, random direction = normalized random vector; then new position = current + rand × distance × random direction. The idea is that if the agent is far from the best solution, it takes a large random step. If it is close, it takes a small one. This allows exploration proportional to how “unexplored” the region is.
Mode 3: global random decomposition (25% probability). The agent can move to a completely new region of the search space: H = (1 - t/(1.5T))^(5t/T) × cos(π × rand); new position = weight × current + (1 - weight) × random walk. The coefficient “H” decreases over time, so large jumps are possible at the beginning of optimization, while only local adjustments occur toward the end. Why three modes are used: this combination provides a balance between exploitation (Mode 1), local exploration (Mode 2), and global exploration (Mode 3).
Revision (greedy selection). After decomposition, the fitness of the new positions is calculated, and greedy selection is applied: if the new fitness is greater than the old fitness, accept the new position; otherwise, roll back to the old position.
For example: the agent was at position x = 50 with a fitness of 80; after decomposition, it moved to x = 65 with a fitness of 75. The new fitness (75) is worse than the old fitness (80) → the agent returns to x = 50. This mechanism ensures that the quality of the best solution found never deteriorates. The algorithm can experiment with risky moves, knowing that unsuccessful attempts will be undone.
Boundary handling. If an agent moves outside the allowed search area, its position is replaced with a random value within the boundaries: if position < minimum or position > maximum: position = random value(minimum, maximum). This approach, unlike simple “reflection” from the boundary, helps preserve population diversity.
Let’s summarize. Each iteration of the ECO algorithm consists of the following steps:
- Update coefficient G — it decreases over time
- Sort the population by fitness
- Identify the producers — the top 20% after sorting
- Move the herbivores toward the producers
- Move the carnivores toward the herbivores
- Move the omnivores toward all groups
- Save the current positions for a possible rollback
- Perform decomposition of all agents (one of three modes)
- Calculate the fitness of the new positions
- Perform a revision — roll back unsuccessful moves

Figure 1. Workflow of the methods within the algorithm.
The diagram shows the groups of organisms:
Producers (green) — the best solutions after sorting, toward which herbivores move
Herbivores (light green) — follow producers and are targets for carnivores
Carnivores (red) — follow herbivores
Omnivores (orange) — receive information from all groups
Decomposers (brown) — transform all solutions in three ways
Central element Sorting (blue) — sorting by fitness; the best become producers
Interaction flows:
Solid arrows — direction of attraction (attract)
Dashed arrows — all solutions undergo decomposition
Blue arrows — cycle through revision and sorting
Information blocks:
Formula for predation coefficient G
Group proportions in the population (20/30/30/20%)
Three decomposition modes with probabilities
INITIALIZATION
Determine group sizes based on the specified proportions:
Producers: 20% of the population
Herbivores: 30% of the population
Carnivores: 30% of the population
Omnivores: 20% of the population
Randomly initialize the positions of all agents in the search space
Save each agent’s initial position as its “previous” position for later comparison
Calculate the fitness of all agents
MAIN OPTIMIZATION LOOP
For each iteration t from 1 to MaxIterations:
Step 1: Update predation coefficient G
For each coordinate, calculate the predation coefficient:
G[c] = 1 + 2 × rand × exp(-9 × (t/T)³) × random_sign(±1), where T is the maximum number of iterations.
Coefficient G decreases over time, ensuring a transition from global exploration to local exploitation.
Step 2: Producer strategy (selection of the best)
Sort the entire population in descending order of fitness
The best agents automatically become producers (they occupy the first positions in the sorted array)
Update the global best solution if an improvement is found
Step 3: Herbivore strategy (movement toward producers)
For each herbivore agent i:
Select 3 producers using roulette-wheel selection (probability is proportional to fitness)
Generate 3 random coefficients r₁, r₂, r₃ ∈ [0, 1]
Calculate the new position:
new_position = current_position + G × (r₁×(producer₁ - current) + r₂×(producer₂ - current) + r₃×(producer₃ - current))
Step 4: Carnivore strategy (movement toward herbivores)
For each carnivore agent i:
Select 3 herbivores using roulette-wheel selection
Generate 3 random coefficients r₁, r₂, r₃ ∈ [0, 1]
Calculate the new position:
new_position = current_position + G × (r₁×(herbivore₁ - current) + r₂×(herbivore₂ - current) + r₃×(herbivore₃ - current))
Step 5: Omnivore strategy (movement toward all groups)
For each omnivore agent i:
Select using the roulette wheel: 1 producer, 1 herbivore, and 2 carnivores
Generate 4 random coefficients r₁, r₂, r₃, r₄ ∈ [0, 1]
Calculate the new position:
new_position = current_position + G × (r₁×(producer - current) + r₂×(herbivore - current) + r₃×(carnivore₁ - current) + r₄×(carnivore₂ - current))
Step 6: Saving positions before decomposition
For all agents, save the current positions and fitness as “previous” values for subsequent comparison during the revision stage.
Step 7: Decomposer strategy (three modes)
For each agent i in the population, randomly select one of three modes:
Mode A: Optimal decomposition (50% probability)
Generate a coefficient: coef = 0.4 × rand - 0.2 (range [-0.2, 0.2])
For each coordinate:
best_neighborhood = best_position[c] × rand[c]
new_position[c] = best_neighborhood + coef × (best_neighborhood - current[c])
Mode B: Local random decomposition (25% probability)
Calculate the distance to the best agent: dist = ||best - current||
Generate a random unit direction vector
Calculate the new position:
new_position = current_position + rand × dist × unit_vector
Mode C: Global random decomposition (25% probability)
Calculate the decaying coefficient: H = (1 - t/(1.5×T))^(5t/T) × cos(π × rand)
Find the minimum search-range width
Calculate the random walk: rand_walk = (2/3) × H × rand × min(rangeMin - rangeMax)
Calculate the new position:
weight = rand
new_position = weight × current_position + (1 - weight) × rand_walk
Step 8: Boundary checking
For each agent and each coordinate:
If the position is out of bounds, assign a random value within the valid range
Apply discretization according to the search step
Step 9: Fitness calculation
Calculate the objective function value for all agents.
Step 10: Revision (greedy selection)
For each agent i:
Compare the new fitness with the previous fitness
If the new fitness is worse than or equal to the previous one, roll back the position and fitness to their previous values
Update the global best solution if an improvement is found
TERMINATION
Return the global best solution found and its fitness.
The class inherits from the base class "C_AO". Configuration parameters:
- Population size (popSize) — determines the total number of agents in the simulated ecosystem.
- Consumer ratios, organism shares:
- ratioProd (producers): food-producing organisms (e.g., plants).
- ratioHerb (herbivores): organisms that feed on producers.
- ratioCarn (carnivores): organisms that feed on other animals.
- ratioOmni (omnivores): organisms that feed on both plants and animals.
-
State and internal variables:
- maxIter and currIter track the maximum number of iterations (simulation steps) and the current iteration.
- numProd, numHerb, numCarn, numOmni: store the actual number of agents of each type (producers, herbivores, carnivores, omnivores) based on popSize and the ratios.
- prodEnd, herbEnd, carnEnd: specify indices in a common array of agents, dividing it into ecological groups.
- huntCoef: an array of coefficients representing a predation model or interactions between carnivores and their prey.
- aT: a temporary array used for sorting or temporary storage of agents.
-
Methods:
- SetParams — allows the internal variables popSize, ratioProd, ratioHerb, ratioCarn, and ratioOmni to be updated based on the values set in the "params" structure.
- Init — initialization; accepts parameter ranges and steps, as well as the number of epochs. It presumably initializes the ecosystem’s starting state.
- Moving — simulates the movement or behavior of agents during a single iteration.
- Revision — performs a "revision" or update of the ecosystem state after a single step.
- SelectFromGroup (private) — a helper method for selecting a specified number of agents from a given group (index range).
- VectorNorm (private) — a helper method for calculating a certain norm or distance between two agents.
The C_AO_ECOc class is a toolkit for simulating ecosystem dynamics. It allows various types of organisms to be modeled (producers and consumers at different trophic levels), along with their interactions, including food consumption and predation. The parameters allow the population composition and agent behavior to be configured flexibly, while the Moving and Revision methods implement the simulation logic.
//———————————————————————————————————————————————————————————————————— class C_AO_ECOc : public C_AO { public: ~C_AO_ECOc () { }2 C_AO_ECOc () { ao_name = "ECOc"; ao_desc = "Ecological Cycle Optimizer"; ao_link = "https://www.mql5.com/en/articles/20611"; popSize = 50; ratioProd = 0.2; ratioHerb = 0.3; ratioCarn = 0.3; ratioOmni = 0.2; ArrayResize (params, 5); params [0].name = "popSize"; params [0].val = popSize; params [1].name = "ratioProd"; params [1].val = ratioProd; params [2].name = "ratioHerb"; params [2].val = ratioHerb; params [3].name = "ratioCarn"; params [3].val = ratioCarn; params [4].name = "ratioOmni"; params [4].val = ratioOmni; } void SetParams () { popSize = (int)params [0].val; ratioProd = params [1].val; ratioHerb = params [2].val; ratioCarn = params [3].val; ratioOmni = params [4].val; } bool Init (const double &rangeMinP [], const double &rangeMaxP [], const double &rangeStepP [], const int epochsP); void Moving (); void Revision (); //------------------------------------------------------------------ double ratioProd; double ratioHerb; double ratioCarn; double ratioOmni; private: //————————————————————————————————————————————————————————— int maxIter; int currIter; int numProd; int numHerb; int numCarn; int numOmni; // Group indices in the overall array a[] int prodEnd; int herbEnd; int carnEnd; // Predation coefficient double huntCoef []; // Temporary array for sorting S_AO_Agent aT []; // Helper methods void SelectFromGroup (int startIdx, int endIdx, int selCount, int &indices []); double VectorNorm (int idx1, int idx2); }; //————————————————————————————————————————————————————————————————————
The Init method initializes an object of the C_AO_ECOc class before starting the optimizer. First, the StandardInit method is called, with the parameters rangeMinP, rangeMaxP, and rangeStepP passed to it. This method sets the bounds and steps for the coordinates in the search space. If standard initialization fails, the method returns 'false'.
This method prepares the optimizer for operation by configuring the initial parameters, assigning agents to ecological groups, and allocating the necessary memory. It serves as the entry point for launching the optimization algorithm.
//———————————————————————————————————————————————————————————————————— bool C_AO_ECOc::Init (const double &rangeMinP [], const double &rangeMaxP [], const double &rangeStepP [], const int epochsP) { if (!StandardInit (rangeMinP, rangeMaxP, rangeStepP)) return false; //------------------------------------------------------------------ maxIter = epochsP; currIter = 0; // Calculate group sizes numProd = (int)MathRound (popSize * ratioProd); numHerb = (int)MathRound (popSize * ratioHerb); numCarn = (int)MathRound (popSize * ratioCarn); numOmni = popSize - numProd - numHerb - numCarn; if (numProd < 1) numProd = 1; if (numHerb < 1) numHerb = 1; if (numCarn < 1) numCarn = 1; if (numOmni < 1) numOmni = 1; // Adjust numOmni if the sum is not equal to popSize numOmni = popSize - numProd - numHerb - numCarn; // Group indices (start of group = end of previous group) prodEnd = numProd; herbEnd = prodEnd + numHerb; carnEnd = herbEnd + numCarn; // Allocate memory ArrayResize (huntCoef, coords); ArrayResize (aT, popSize); ArrayResize (u.roulette, popSize); for (int i = 0; i < popSize; i++) aT [i].Init (coords); return true; } //————————————————————————————————————————————————————————————————————
The Moving method simulates the evolution and behavior of agents in an ecosystem over the course of a single iteration of the algorithm. It is divided into several logical blocks, each of which reflects a specific "strategy."
If the algorithm is in its first iteration, the positions of all agents are initialized randomly within the specified ranges, taking into account the discretization.
The current iteration counter is incremented. The relative values of the current and maximum iterations are calculated. A new predation coefficient is calculated for each coordinate. This coefficient changes dynamically depending on the optimization stage and a random factor. It can be either positive or negative.
Producers' strategy. The entire array of agents "a" is sorted by fitness value "f" in descending order. This is done using the helper method u.Sorting, which uses a temporary array aT. If, after sorting, the best agent has better fitness than the current global maximum fB, then fB and the best position cB are updated.
Herbivores' strategy (movement toward producers). Three producers are selected from the producer group using roulette-wheel selection. Each herbivore agent (a[i] from prodEnd to herbEnd) moves. Its new position is calculated as a combination of its current position and the weighted sum of the positions of the selected producers. The weighting coefficients "r1," "r2," and "r3" are random, and the overall movement vector is multiplied by huntCoef[c]. This mimics the search for and consumption of "food" (producers).
Carnivores' strategy (movement toward herbivores). As with the herbivores, three random herbivore agents "selHerb" are selected. Each carnivore (a[i] from herbEnd to carnEnd) moves toward the positions of the selected herbivores. The movement strategy is similar to that of herbivores, but is aimed at a different group.
Omnivore Strategy (movement toward all groups). Omnivorous agents (a[i] from carnEnd to popSize) also move. Their movement is a combination of movement toward a producer, one herbivore, and two carnivores. This reflects their ability to feed on a variety of sources.
Decomposer Strategy (solution search). Before applying the decomposer strategy, the current positions and fitness values of all agents are saved in cP and fP, respectively.
- Optimal decomposition (rnd < 0.5) — the agent moves from its previous position toward a position determined by a combination of a random coefficient and the position of the best agent.
- Local random decomposition (0.5 <= rnd < 0.75) — the agent moves in the direction specified by a random unit vector over a distance that depends on the agent’s previous position, as well as on the distance to the best agent and a random multiplier.
- Global random decomposition (rnd >= 0.75) — the agent moves based on a weighted combination of its previous position and a certain random walk, whose magnitude depends on the optimization stage (t/T) and random multipliers. This type of movement becomes less active as the optimization nears its end.
After applying any of the movement strategies (herbivores, carnivores, omnivores, decomposers), a boundary check is performed for each agent to determine whether its new coordinates have fallen outside the allowable ranges. If this happens, its position is randomly regenerated within the range and then discretized using "u.SeInDiSp" according to the specified step size.
In general, the Moving method follows this logic:
Step 1: Randomly initialize the entire population.
Next steps:
- dynamically changes the “strength” of predation,
- sorts agents by solution quality, updating the best result found,
- simulates foraging and predation, in which each group (herbivores, carnivores, omnivores) “moves toward” agents from lower-level or adjacent groups,
- uses various “decomposition” strategies (essentially, methods for solution search and improvement), which can be either more targeted (toward the best agent) or more random, with varying degrees of global scope and activity depending on the optimization stage,
- forces all agents to remain within the specified search boundaries.
This method simulates an ecosystem in which various "species" (groups of agents) interact, and it also employs an optimization mechanism (decomposer strategies) to find the best solution.
//———————————————————————————————————————————————————————————————————— void C_AO_ECOc::Moving () { //------------------------------------------------------------------ // First iteration: random initialization if (!revision) { for (int i = 0; i < popSize; i++) { for (int c = 0; c < coords; c++) { a [i].c [c] = u.RNDfromCI (rangeMin [c], rangeMax [c]); a [i].c [c] = u.SeInDiSp (a [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]); // Save to cP for comparison in Revision a [i].cP [c] = a [i].c [c]; } a [i].f = -DBL_MAX; a [i].fP = -DBL_MAX; } revision = true; return; } //------------------------------------------------------------------ currIter++; double t = (double)currIter; double T = (double)maxIter; // Update the predation coefficient G for (int c = 0; c < coords; c++) { int sign = (u.RNDprobab () < 0.5) ? -1 : 1; huntCoef [c] = 1.0 + 2.0 * u.RNDfromCI (0.0, 1.0) * MathExp (-9.0 * MathPow (t / T, 3)) * sign; } //================================================================== // (1) The producers' strategy: sorting the population by fitness //================================================================== // Use the ready-made sorting function from the utilities u.Sorting (a, aT, popSize); // Update the best solution if (a [0].f > fB) { fB = a [0].f; ArrayCopy (cB, a [0].c, 0, 0, coords); } //================================================================== // (2) Herbivores’ strategy—moving toward producers //================================================================== int selProd []; SelectFromGroup (0, prodEnd, 3, selProd); for (int i = prodEnd; i < herbEnd; i++) { double r1 = u.RNDfromCI (0.0, 1.0); double r2 = u.RNDfromCI (0.0, 1.0); double r3 = u.RNDfromCI (0.0, 1.0); for (int c = 0; c < coords; c++) { double move = r1 * (a [selProd [0]].c [c] - a [i].c [c]) + r2 * (a [selProd [1]].c [c] - a [i].c [c]) + r3 * (a [selProd [2]].c [c] - a [i].c [c]); a [i].c [c] = a [i].c [c] + huntCoef [c] * move; } } //================================================================== // (3) The carnivores' strategy—moving toward herbivores //================================================================== int selHerb []; SelectFromGroup (prodEnd, herbEnd, 3, selHerb); for (int i = herbEnd; i < carnEnd; i++) { double r1 = u.RNDfromCI (0.0, 1.0); double r2 = u.RNDfromCI (0.0, 1.0); double r3 = u.RNDfromCI (0.0, 1.0); for (int c = 0; c < coords; c++) { double move = r1 * (a [selHerb [0]].c [c] - a [i].c [c]) + r2 * (a [selHerb [1]].c [c] - a [i].c [c]) + r3 * (a [selHerb [2]].c [c] - a [i].c [c]); a [i].c [c] = a [i].c [c] + huntCoef [c] * move; } } //================================================================== // (4) The omnivores' strategy—moving toward all groups //================================================================== int selProd1 []; int selHerb1 []; int selCarn2 []; SelectFromGroup (0, prodEnd, 1, selProd1); SelectFromGroup (prodEnd, herbEnd, 1, selHerb1); SelectFromGroup (herbEnd, carnEnd, 2, selCarn2); for (int i = carnEnd; i < popSize; i++) { double r1 = u.RNDfromCI (0.0, 1.0); double r2 = u.RNDfromCI (0.0, 1.0); double r3 = u.RNDfromCI (0.0, 1.0); double r4 = u.RNDfromCI (0.0, 1.0); for (int c = 0; c < coords; c++) { double move = r1 * (a [selProd1 [0]].c [c] - a [i].c [c]) + r2 * (a [selHerb1 [0]].c [c] - a [i].c [c]) + r3 * (a [selCarn2 [0]].c [c] - a [i].c [c]) + r4 * (a [selCarn2 [1]].c [c] - a [i].c [c]); a [i].c [c] = a [i].c [c] + huntCoef [c] * move; } } //================================================================== // (5) The decomposers' strategy //================================================================== // Save the current positions in cP before decomposition for (int i = 0; i < popSize; i++) { for (int c = 0; c < coords; c++) { a [i].cP [c] = a [i].c [c]; } a [i].fP = a [i].f; } // Best agent—index 0 after sorting int bestIdx = 0; // Decomposition for (int i = 0; i < popSize; i++) { double rnd = u.RNDfromCI (0.0, 1.0); if (rnd < 0.5) { //--- Optimal decomposition --- double coef = 0.4 * u.RNDfromCI (0.0, 1.0) - 0.2; for (int c = 0; c < coords; c++) { double randC = u.RNDfromCI (0.0, 1.0); double bestNeighbor = a [bestIdx].cP [c] * randC; a [i].c [c] = bestNeighbor + coef * (bestNeighbor - a [i].cP [c]); } } else if (rnd < 0.75) { //--- Local random decomposition --- double dist = VectorNorm (bestIdx, i); // Generate a random unit vector double randDir []; ArrayResize (randDir, coords); double norm = 0.0; for (int c = 0; c < coords; c++) { randDir [c] = 2.0 * u.RNDfromCI (0.0, 1.0) - 1.0; norm += randDir [c] * randDir [c]; } norm = MathSqrt (norm) + 1e-10; double randMult = u.RNDfromCI (0.0, 1.0); for (int c = 0; c < coords; c++) { randDir [c] /= norm; a [i].c [c] = a [i].cP [c] + randMult * dist * randDir [c]; } } else { //--- Global random decomposition --- double tRatio = t / (1.5 * T); if (tRatio > 1.0) tRatio = 1.0; double H = MathPow (1.0 - tRatio, 5.0 * t / T) * MathCos (M_PI * u.RNDfromCI (0.0, 1.0)); // min(Low - Up) double minRange = rangeMin [0] - rangeMax [0]; for (int c = 1; c < coords; c++) { double diff = rangeMin [c] - rangeMax [c]; if (diff < minRange) minRange = diff; } double randWalk = (2.0 / 3.0) * H * u.RNDfromCI (0.0, 1.0) * minRange; double weight = u.RNDfromCI (0.0, 1.0); for (int c = 0; c < coords; c++) { a [i].c [c] = weight * a [i].cP [c] + (1.0 - weight) * randWalk; } } // Boundary checking and discretization for (int c = 0; c < coords; c++) { if (a [i].c [c] < rangeMin [c] || a [i].c [c] > rangeMax [c]) { a [i].c [c] = u.RNDfromCI (rangeMin [c], rangeMax [c]); } a [i].c [c] = u.SeInDiSp (a [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]); } } } //————————————————————————————————————————————————————————————————————
The Revision method is used for "self-correction" of the population. It ensures that no agent remains in a worse state than it was before its last action (by rolling back). At the same time, it tracks and saves the best solution found across the entire population during the algorithm's execution. This is a classic aspect of many evolutionary algorithms, where it is important to preserve the best achievements and reject steps that lead to deterioration.
//———————————————————————————————————————————————————————————————————— void C_AO_ECOc::Revision () { for (int i = 0; i < popSize; i++) { // If the new position is worse, roll back to the previous one if (a [i].f <= a [i].fP) { a [i].f = a [i].fP; ArrayCopy (a [i].c, a [i].cP, 0, 0, coords); } // Update the global best solution if (a [i].f > fB) { fB = a [i].f; ArrayCopy (cB, a [i].c, 0, 0, coords); } } } //————————————————————————————————————————————————————————————————————
The "SelectFromGroup" method implements roulette-wheel selection to choose "selCount" indices from a group of agents (from startIdx to endIdx).
- Preparation: initializes the output array "indices" and determines the group size. Handles cases where the group is empty or too small.
- Weight calculation: finds the minimum fitness in the group and uses it to convert the agents' fitness values into positive weights.
- Roulette wheel construction: creates a "roulette wheel" (arrays of intervals), where each interval corresponds to an agent and its size is proportional to that agent's weight (taking into account the minimum fitness).
- Selection: generates a random number on the roulette wheel selCount times and selects the agent whose interval contains that number. The result is stored in "indices".
//———————————————————————————————————————————————————————————————————— void C_AO_ECOc::SelectFromGroup (int startIdx, int endIdx, int selCount, int &indices []) { ArrayResize (indices, selCount); int groupSize = endIdx - startIdx; if (groupSize <= 0) { for (int s = 0; s < selCount; s++) indices [s] = startIdx; return; } if (groupSize < selCount) { for (int s = 0; s < selCount; s++) indices [s] = startIdx + (s % groupSize); return; } // Prepare the roulette wheel for the group double minFit = a [startIdx].f; for (int i = startIdx + 1; i < endIdx; i++) { if (a [i].f < minFit) minFit = a [i].f; } // Fill the roulette wheel structure double cumSum = 0.0; for (int i = 0; i < groupSize; i++) { u.roulette [i].start = cumSum; double prob = a [startIdx + i].f - minFit + 1e-10; cumSum += prob; u.roulette [i].end = cumSum; } // Selection for (int s = 0; s < selCount; s++) { double r = u.RNDfromCI (0.0, cumSum); indices [s] = startIdx; for (int i = 0; i < groupSize; i++) { if (r >= u.roulette [i].start && r < u.roulette [i].end) { indices [s] = startIdx + i; break; } } } } //————————————————————————————————————————————————————————————————————
The VectorNorm method calculates the Euclidean distance between two points (the coordinates of idx1 and idx2 agents) in a "coords"-dimensional space.
- Summation of squared differences: iterates over all "coords" dimensions, calculates the difference between the corresponding coordinates of the two agents, squares that difference, and accumulates the sum.
- Square root extraction: returns the square root of the resulting sum, which is the length of the vector (the Euclidean distance).
//———————————————————————————————————————————————————————————————————— double C_AO_ECOc::VectorNorm (int idx1, int idx2) { double sum = 0.0; for (int c = 0; c < coords; c++) { double diff = a [idx1].cP [c] - a [idx2].cP [c]; sum += diff * diff; } return MathSqrt (sum); } //————————————————————————————————————————————————————————————————————
Test Results
ECOc|Ecological Cycle Optimizer|50.0|0.2|0.3|0.3|
=============================
5 Hilly's; Func runs: 10000; result: 0.7030067722054903
25 Hilly's; Func runs: 10000; result: 0.3712282206716303
500 Hilly's; Func runs: 10000; result: 0.3380465748120792
=============================
5 Forest's; Func runs: 10000; result: 0.4856459718015104
25 Forest's; Func runs: 10000; result: 0.2948314767505128
500 Forest's; Func runs: 10000; result: 0.1987204244277699
=============================
5 Megacity's; Func runs: 10000; result: 0.5892307692307692
25 Megacity's; Func runs: 10000; result: 0.3683076923076923
500 Megacity's; Func runs: 10000; result: 0.35736923076923105
=============================
Overall score: 3.70639 (41.18%)
Visualization of the algorithm running on various test functions.

ECOc on the Hilly test function

ECOc on the Forest test function

ECOc on the Megacity test function

ECOc on the standard Shaffer test function

ECOc on the standard Peaks test function
| No. | 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 | DOAdingom | dingo_optimization_algorithm_M | 0.47968 | 0.45367 | 0.46369 | 1.39704 | 0.94145 | 0.87909 | 0.91454 | 2.73508 | 0.78615 | 0.86061 | 0.84805 | 2.49481 | 6.627 | 73.63 |
| 2 | 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 |
| 3 | 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 |
| 4 | AMOm | animal migration optimization 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 |
| 5 | (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 |
| 6 | 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 |
| 7 | 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 |
| 8 | 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 |
| 9 | 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 |
| 10 | 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 |
| 11 | 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 |
| 12 | 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 |
| 13 | EOm | extremal_optimization_M | 0.76166 | 0.77242 | 0.31747 | 1.85155 | 0.99999 | 0.76751 | 0.23527 | 2.00277 | 0.74769 | 0.53969 | 0.14249 | 1.42987 | 5.284 | 58.71 |
| 14 | 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 |
| 15 | 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 |
| 16 | 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 |
| 17 | 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 |
| 18 | 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 |
| 19 | 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 |
| 20 | 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 |
| 21 | 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 |
| 22 | BSA | backtracking_search_algorithm | 0.97309 | 0.54534 | 0.29098 | 1.80941 | 0.99999 | 0.58543 | 0.21747 | 1.80289 | 0.84769 | 0.36953 | 0.12978 | 1.34700 | 4.959 | 55.10 |
| 23 | 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 |
| 24 | 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 |
| 25 | BO | bonobo_optimizer | 0.77565 | 0.63805 | 0.32908 | 1.74278 | 0.88088 | 0.76344 | 0.25573 | 1.90005 | 0.61077 | 0.49846 | 0.14246 | 1.25169 | 4.895 | 54.38 |
| 26 | 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 |
| 27 | 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 |
| 28 | DOA | dream_optimization_algorithm | 0.85556 | 0.70085 | 0.37280 | 1.92921 | 0.73421 | 0.48905 | 0.24147 | 1.46473 | 0.77231 | 0.47354 | 0.18561 | 1.43146 | 4.825 | 53.62 |
| 29 | 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 |
| 30 | DEA | dolphin_echolocation_algorithm | 0.75995 | 0.67572 | 0.34171 | 1.77738 | 0.89582 | 0.64223 | 0.23941 | 1.77746 | 0.61538 | 0.44031 | 0.15115 | 1.20684 | 4.762 | 52.91 |
| 31 | 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 |
| 32 | 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 |
| 33 | 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 |
| 34 | 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 |
| 35 | (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 |
| 36 | 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 |
| 37 | 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 |
| 38 | 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 |
| 39 | WOAm | whale 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 |
| 40 | 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 |
| 41 | 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 |
| 42 | 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 |
| 43 | 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 |
| 44 | 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 |
| 45 | DA_duelist | duelist_algorithm | 0.92782 | 0.53778 | 0.27792 | 1.74352 | 0.86957 | 0.47536 | 0.18193 | 1.52686 | 0.62153 | 0.33569 | 0.11715 | 1.07437 | 4.345 | 48.28 |
| ECOc | ecological_cycle_optimizer | 0.70300 | 0.37122 | 0.33804 | 1.41226 | 0.48564 | 0.29483 | 0.19872 | 0.97919 | 0.58923 | 0.36830 | 0.35736 | 1.31489 | 3.706 | 41.18 | |
| 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 | |
Conclusions
The ECO algorithm can be recommended for problems where convergence stability is important and moderate computational costs are acceptable. For problems requiring maximum efficiency, algorithms from the top of our ranking should be considered. An original and intuitive concept based on ecological interactions. An adaptive predation coefficient G that ensures a smooth transition from global exploration to local exploitation. A greedy selection (revision) mechanism that guarantees monotonic improvement of the best solution. Three decomposition modes that add variety to search strategies. High computational complexity due to the large number of copying and sorting operations in each iteration.

Figure 2. Color coding of the algorithms in the corresponding tests

Figure 3. Histogram of algorithm test results (on a scale from 0 to 100; the higher the score, the better, where 100 is the maximum possible theoretical result; the archive contains a script for calculating the ranking table)
Pros and cons of the ECOc algorithm:
Pros:
- It handles certain types of problems well.
Cons:
- It has a harder time handling discrete functions.
An archive containing the latest versions of the algorithm code is attached to the article. The author of this article is not responsible for the absolute accuracy of the descriptions of the canonical algorithms; changes have been made to many of them to improve their search capabilities. The conclusions and judgments presented in the articles are based on the results of the experiments conducted.
Programs used in the article
| # | Name | Type | Description |
|---|---|---|---|
| 1 | #C_AO.mqh | Include file | Parent class of population-based optimization algorithms |
| 2 | #C_AO_enum.mqh | Include file | Enumeration of population-based optimization algorithms |
| 3 | TestFunctions.mqh | Include file | Library of test functions |
| 4 | TestStandFunctions.mqh | Include file | Library of test bench functions |
| 5 | Utilities.mqh | Include file | Library of utility functions |
| 6 | CalculationTestResults.mqh | Include file | Script for calculating results for the comparison table |
| 7 | Testing AOs.mq5 | Script | A unified test bench for all population-based optimization algorithms |
| 8 | Simple use of population optimization algorithms.mq5 | Script | A simple example of using population-based optimization algorithms without visualization |
| 9 | Test_AO_ECOc.mq5 | Script | Test bench for ECO |
Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/20611
Warning: All rights to these materials are reserved by MetaQuotes Ltd. Copying or reprinting of these materials in whole or in part is prohibited.
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.
Features of Custom Indicators Creation
Neural Networks in Trading: Unraveling Structural Components (Encoder)
Features of Experts Advisors
Tables in the MVC Paradigm in MQL5: Symbol Correlation Table
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use