Manlio Morini edited this page Sep 11, 2018 · 158 revisions

A genetic programming approach to Forex Expert Advisor generation

A genetic programming approach to Forex Expert Advisor generation

Here we use Genetic Programming to evolve a simple software agent, more specifically a MQL5 Expert Advisor (EA), operating on the Forex exchange market.

NOTE. This is just an example: GP can produce a robust expert advisor but it's a complex task. The following EAs are not production-ready and shouldn't be used for real trading.


Besides the Vita framework we need two additional programs:

  • Metatrader 5 (MT5) for back-testing. It's a well known trading platform that supports demo-accounts and has a built-in strategy tester;
  • a working Python v3.6 environment.

How to build and run the example

With a Unix operating system

cd src
make forex

should be enough. With Windows take a look at the specific walkthrough.

At the end of a successful compilation process group the core files in an ad-hoc directory (e.g. forex_dir).

Core files of the example: forex,, forex.xml

It's easily done through the GUI or the command line:

Windows Unix
mkdir c:\forex_dir mkdir ~/forex_dir
copy build\examples\forex.exe c:\forex_dir cp build/examples/forex ~/forex_dir
copy examples\forex\ c:\forex_dir cp examples/forex/ ~/forex_dir
copy examples\forex\forex.xml c:\forex_dir cp examples/forex/forex.xml ~/forex_dir
copy examples\forex\template.mq5 c:\forex_dir cp examples/forex/template.mq5 ~/forex_dir

Since MT5 is a Windows program, different deployments for Windows-only vs heterogeneous system are required:

Windows-only Heterogeneous system
Deployment diagram for a Windows-only system Deployment diagram for a mixed system
forex example compiled for Windows. Further details here. A virtual machine on a single workstation. Nothing prevents the use of two distinct workstations. How to share folders between Host and Guest OS depends on the VM (e.g. VirtualBox). The name of the shared folder / network drive (here E:) isn't very important.

Edit the data_dir parameter of the forex.xml configuration file to match MT5 data folder (use the command "Open Data Folder" in the MT5 File menu to discover the correct value):

Open Data Folder in Metatrader 5

If MT5 is installed in the default path this is enough (otherwise also check metaeditor and terminal path).

To start the evolutionary process launch (in this order):

  2. forex

Please note that:

Windows Heterogeneous system
Both and forex run on Windows. runs on Windows, forex is on the host operating system.

If you want to stop the system, getting the partial results, press:

  • . to shutdown forex (it may require some time) and
  • CTRL + C to halt

Evolutionary process


The general idea follows:

  • forex manages the evolutionary process and maintains a population of EAs;
  • fitness evaluation is left to metatrader;
  • does the dirty job of coordinating forex and metatrader.

Both the forex executable and the script keep polling the shared folder till specific files arrive (and remove them as soon as possible). Details...

Structure of an EA

The general structure of the EA is contained in the template.mq5 file:

  • it's based on a netting system, meaning that you can only have one common position for a symbol at the same time:
  • if there's an open position, executing a deal in the same direction increases the volume of the position;
  • if a deal is executed in the opposite direction, the volume of the existing position can be decreased. The position can be closed (when the deal volume is equal to the position volume) or reversed (if the volume of the opposite deal is greater than the current position);
  • the EA is active when a new bar is formed;
  • entry / exit conditions are based on the Japanese candlesticks / candlestick patterns.

Vita evolves a population of entry/exit conditions. These conditions are merged with the template.mq5 file to produce the actual EA.


bool buy_pattern() {return false;}

bool sell_pattern() {return false;}

are replaced / filled with something like:

bool buy_pattern()
  return (low(1,2) < high(1,2) - open(0,1) - high(1,2)
          && close(2,2) < close(2,1))
         || (close(2,1) < low(1,1) + open(1,2));

bool sell_pattern()
  return white_candle(0,2) && long_white_candle(1,2);

You can change / adapt the template file to your needs but remember:

  • do not touch the buy_pattern() / sell_pattern() functions: they're placeholder and subject to regular expression replacement;
  • use them somewhere! You can completely change the logic of the EA, nevertheless without calls to buy_pattern() / sell_pattern() the entire evolutionary process is meaningless.

Output of an EA

The OnDeinit() function in the template.mq5 file is responsible for creating the result.txt output file. Each line of this file contains various statistics (see Testing Statistics for further details):

  1. net profit after testing (STAT_PROFIT)
  2. number of short trades (STAT_SHORT_TRADES)
  3. number of long trades (STAT_LONG_TRADES)
  4. maximum balance drawdown in monetary terms (STAT_BALANCE_DD)
  5. maximum balance drawdown as a percentage (STAT_BALANCE_DDREL_PERCENT)

(some of) These values are used by the GP-engine to calculate the fitness value (run and fitnessx functions inside the file):

vita::fitness_t trade_simulator::run(const vita::team<vita::i_mep> &prg)
  // ...

  double fit(/* function of profit, drawdown... */);

  return {fit, profit, drawdown, trades};

As known from the previous examples, Vita uses standardized fitness meaning that a greater numerical value is always a better value.

It's interesting to observe that the profit value is both the raw fitness (the fitness stated in the natural terminology of the problem) and a valid standardized fitness (more profit is better!).

So the simple:

  return {profit};

would be enough for starting but

  return {profit - drawdown};

is a better expression because it creates more of an aversion to loss than raw profit evaluation (actually the expression used is even more complex).

The function returns:

  return {fit, profit, drawdown, trades};

but, actually, the only important component is the first (fit); the other components are just informative indicators printed during the evolution.

Overview of the source code

int main()
  vita::problem p;

The problem class aggregates parameters (environment) and symbols (symbol_set) pertaining to the problem. The constructor left the parameters in an undefined state (i.e. auto-tune before search). User can force some values as needed.

  if (!setup_symbols(&p.sset))
    return EXIT_FAILURE;

setup_symbols inserts into the symbol set (p.sset) the building blocks (symbols) of the EA: terminals and functions.

Examples of terminal symbols (derived from vita::terminal) are:

  • open<short_tf, 0> is the opening value of the last candle available for the shorter time frame. close<TF, I>, high<TF, I>, low<TF, I> are other terminals with the obvious meaning.

    These terminals are in the c_money domain (they have real values).

    NOTE: the candle index (I) is in the [1;3] range. Only for the open terminal it can also be 0.

  • black_candle<medium_tf, 1> is true if the last fully formed candle in the medium time frame is a black candle. white_candle<TF, I>, doji<TF, I>, bearish_harami<TF, I>, bullish_harami<TF, I> are similar terminals.

    These terminals are in the c_logic domain (they have boolean value).

Examples of functions are:

  • add and sub. Both are just the standard arithmetic operators taking two c_money values and returning a new one.
  • lt_m is the "less than" operator. It compares two c_money values and returns a c_logic value.
  • l_and and l_or are the standard logic operator (arguments and result of the c_logic type).

A simpler implementation could avoid all the type-related difficulties, but we prefer to take advantage of the strongly typed genetic programming features of Vita because they help the search and produce simpler-to-understand buy/sell patterns.

Various evolution-related parameters to play with.

  p.env.individuals = 30;
  p.env.min_individuals = 8;
  p.env.mep.code_length = 200;
  p.env.generations = 400;
  p.env.layers = 6; = 2;  // DO NOT CHANGE
  p.env.alps.age_gap = 10;
  p.env.cache_size = 20; cannot be changed because our population is a collection of teams each made up of two members: the first one codifies the buy-pattern, the second one the sell-pattern. Additional members aren't used in the evaluation function.

Among the other parameters

  p.env.stat.dynamic_file    = "dynamic.txt";
  p.env.stat.layers_file     = "layers.txt";
  p.env.stat.population_file = "population.txt";
  p.env.stat.summary_file    = "summary.txt";
  p.env.stat.ind_format = vita::out::mql_language_f;
  p.env.misc.serialization_file = "cache.txt";

some deserve a short description:

  • p.env.stat.ind_format. To obtain the fitness value, a team has to be translated in MQL5 code, merged in the template.mq5 and scored by MT5. vita::out::mql_language_f is the modifier controlling the output language. For instance:

    fxs::team t;
    // say t[0] contains:
    // lt_m( sub(open<0,1>(), close<0,1>()), sub(open<0,2>(), close<0,2>()) )
    std::cout << vita::out::mql_language_f << t[0];


    open(0,1) - close(0, 1) < open(0,2) - close(0,2)

    now template.mq5 contains the implementation of the translation of symbols used in the engine and... it all hangs together nicely.

  • p.env.misc.serialization_name. Setting this parameter allows to interrupt the evolution (the user press the . key) and resume it without recalculate everything (the library saves the cache in an external file so that almost all the fitness value calculated so far could be reused later).

    WARNING. Changing the symbol set, the signature of the individuals / teams changes. Altering the evaluator the fitness changes. IN BOTH CASES THE FILE CONTAINING THE CACHE CANNOT BE USED ANYMORE.

  fxs::search engine(p);

fxs::search is just a shortcut for vita::search<team, vita::alps_es>:

class search : public vita::search<team, vita::alps_es>
  using vita::search<team, vita::alps_es>::search;

The search of solutions is driven by the vita::search class that uses p to access problem-specific parameters. The two template arguments specify:

  • team the type of individuals making up the managed population. fxs::team is just:

    using team = vita::team<vita::i_mep>;

    in other words a team of (two) i_mep individuals.

  • vita::alps_es is the evolutionary strategy. The ALPS (Age Layered Population Structure) paradigm is a metaheuristic for overcoming premature convergence.

  trade_simulator ts;

trade_simulator is the problem-specific evaluator (fitness function). It acts as an adapter / wrapper (the real job is performed by MT5).

You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.