forex_tutorial - morinim/vita GitHub Wiki

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.x environment.
With a Unix operating system
cd src
make forexshould be enough. With Windows take a look at the specific walkthrough.
At the end of a successful compilation, group the core files in an ad-hoc directory (e.g. forex_dir).

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\driver.py c:\forex_dir |
cp examples/forex/driver.py ~/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 |
|---|---|
![]() |
![]() |
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):

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):
driver.pyforex
Please note that:
| Windows | Heterogeneous system |
|---|---|
Both driver.py and forex run on Windows. |
driver.py runs on Windows, forex is on the host operating system. |
If you want to stop the system, getting the partial results, press:
-
.to shutdownforex(it may require some time) and -
CTRL + Cto haltdriver.py
The general idea follows:
-
forexmanages the evolutionary process and maintains a population of EAs; - fitness evaluation is left to
metatrader; -
driver.pydoes the dirty job of coordinatingforexandmetatrader.
Both the forex executable and the driver.py script keep polling the shared folder till specific files arrive (and remove them as soon as possible). Details...
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.
Specifically:
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.
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):
- net profit after testing (
STAT_PROFIT) - number of short trades (
STAT_SHORT_TRADES) - number of long trades (
STAT_LONG_TRADES) - maximum balance drawdown in monetary terms (
STAT_BALANCE_DD) - 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 trade_simulator.cc 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.
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_moneydomain (they have real values).NOTE: the candle index (
I) is in the[1;3]range. Only for theopenterminal it can also be0. -
black_candle<medium_tf, 1>istrueif 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_logicdomain (they have boolean value).
Examples of functions are:
-
addandsub. Both are just the standard arithmetic operators taking twoc_moneyvalues and returning a new one. -
lt_mis the "less than" operator. It compares twoc_moneyvalues and returns ac_logicvalue. -
l_andandl_orare the standard logic operator (arguments and result of thec_logictype).
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;
p.env.team.individuals = 2; // DO NOT CHANGE
p.env.alps.age_gap = 10;
p.env.cache_size = 20;p.env.team.individuals 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 thetemplate.mq5and scored by MT5.vita::out::mql_language_fis 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];
produces:
open(0,1) - close(0, 1) < open(0,2) - close(0,2)now
template.mq5contains 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>
{
public:
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:
-
teamthe type of individuals making up the managed population.fxs::teamis just:using team = vita::team<vita::i_mep>; -
vita::alps_esis the evolutionary strategy. The ALPS (Age Layered Population Structure) paradigm is a metaheuristic for overcoming premature convergence.
trade_simulator ts;
engine.training_evaluator<fxs::evaluator>(&ts);trade_simulator is the problem-specific evaluator (aka fitness function). It acts as an adapter / wrapper (the real job is performed by MT5).

