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 forex
should 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.py
forex
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 + C
to haltdriver.py
The general idea follows:
-
forex
manages the evolutionary process and maintains a population of EAs; - fitness evaluation is left to
metatrader
; -
driver.py
does the dirty job of coordinatingforex
andmetatrader
.
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_money
domain (they have real values).NOTE: the candle index (
I
) is in the[1;3]
range. Only for theopen
terminal it can also be0
. -
black_candle<medium_tf, 1>
istrue
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
andsub
. Both are just the standard arithmetic operators taking twoc_money
values and returning a new one. -
lt_m
is the "less than" operator. It compares twoc_money
values and returns ac_logic
value. -
l_and
andl_or
are the standard logic operator (arguments and result of thec_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;
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.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];
produces:
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>
{
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:
-
team
the type of individuals making up the managed population.fxs::team
is just:using team = vita::team<vita::i_mep>;
-
vita::alps_es
is 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).