MQL's OOP notes: On The Fly Self-Optimization of Expert Advisers: part 1

Ideally, MetaTrader could provide automatic self-optimization of EAs by design. The built-in tester does work, and only thing which is missing is an API, exposing the settings of the tester for MQL code. For example, there could be a couple of functions - TesterSetSettings, TesterGetSettings, accepting a structure with date ranges (how many bars from history to use), optimization mode (open prices, control points, ticks), spread, method and goal of optimization (straightforward/genetic, balance/profit-factor/drawdown/etc.), and a reference to a file with parameters optimization set (which parameters to optimize, in which ranges, with which increments). This set-file can be prepared by user only once (just by clicking Save button on Inputs tab of the optimization dialog), and then he can start optimization by preferred schedule from MQL code by calling a dedicated method like this: TesterStartOptimization. The results of optimization could be then accessed via similar API, and EA could select a better parameter set on the fly. Unfortunately, neither MT4 nor MT5 can afford this.
The library will work in MetaTrader 4. I have many reasons to choose it over MetaTrader 5 - its API is more simple and I use it more - just to name a few. If you like MetaTrader 5, you can develop similar library - it should be easy considering the fact that this library provides a detailed roadmap.
Interface Design
The code of ordinary EA is running in the global context, where API functions such as OrderSend, OrderClose, iOpen or iClose are defined and provided by the platform. If we move the code inside a class and define our own methods imitating OrderSend, OrderClose, iOpen, iClose and all the others, inside the class, then the original algorithm will work as usual, without noticing the deception.
{
public:
int OrdersTotal()
{
return 0; // stub
}
virtual void trade() = 0;
};
class EAContext: public LibraryContext
{
public
virtual void trade()
{
int n = OrdersTotal(); // will execute LibraryContext::OrdersTotal
}
};
This is just a simplified scheme for explanation. In reality we'll need to use slightly different hierarchy of classes.
Interface blueprint
Which information do we need to control an optimization process from within EA? Here is the main points:
- a simple way to enable/disable the library as a whole
- history depth to use for virtual trading (ending datetime is always now)
- how often to repeat optimization (for example, if optimization runs on 2 months history, it probably makes sense to update it every week)
- initial deposit size (remember, some EAs prefer to take risks only on a part of account balance)
- work spread (current spread can be inappropriate if optimization is performed on weekends)
- optimization goal (which value to optimize: balance, profit factor, sharpe ratio, etc)
- and finally, parameters - an array of named entities (inputs) with a range of values to probe, and increments
{
public:
virtual void setEnabled(const bool enabled) = 0;
virtual void setHistoryDepth(const int size, const TIME_UNIT unit) = 0;
virtual void setReoptimizationPeriod(const int size, const TIME_UNIT unit) = 0;
virtual void setDeposit(const double deposit) = 0;
virtual void setSpread(const int points = 0) = 0;
virtual void setOptimizationCriterion(const PERFORMANCE_ESTIMATOR type) = 0;
virtual void addParameter(const string name, const double min, const double max, const double step) = 0;
};
All the methods are pure virtual, because we're defining an abstract interface. If it wouldn't be so, then after the header is included into EA source code, compiler would give the error "function must have a body" for every non-pure virtual method. Compiler needs a complete implementation of every concrete method (function) or requires an import directive pointing to a library, where to get it from. But in MQL it's not possible to import classes. Libraries can only export plain old functions. This is why we need to introduce a so-called factory function, which can create an instance of our class and provide it for EA.
Optimystic *createInstance();
#import
This "says" to compiler: this function will return a runtime object with specified virtual functions, but their implementation is not your bussiness at the moment.
{
UNIT_BAR,
UNIT_DAY
};
We can specify time ranges either in bars or in days.
{
ESTIMATOR_DEFAULT = -1,
ESTIMATOR_NET_PROFIT, // default
ESTIMATOR_PROFIT_FACTOR,
ESTIMATOR_SHARPE,
ESTIMATOR_DRAWDOWN_PERCENT,
ESTIMATOR_WIN_PERCENT,
ESTIMATOR_AVERAGE_PROFIT,
ESTIMATOR_TRADES_COUNT,
ESTIMATOR_CUSTOM
};
These constants allow us to choose one of most popular optimization criteria. I think their names are self-explaining.
{
public:
virtual string getName() const = 0;
virtual double getMin() const = 0;
virtual double getMax() const = 0;
virtual double getStep() const = 0;
virtual double getValue() const = 0;
virtual double getBest() const = 0;
virtual bool getEnabled() const = 0;
virtual void setEnabled(const bool e) = 0;
};
The first 4 methods just returns the properties that we passed to the library via addParameter method. getValue is a current value, and getBest is an optimal value (if any). Also we should have an option to exclude (disable) specific parameter from optimization temporary. This will allow us to add all parameters to the library in a batch and then play with them without recompiling EA - just by switching inputs.
{
public:
virtual OptimysticParameter *getParameter(const int i) const = 0;
OptimysticParameter *operator[](int i) const
{
return getParameter(i);
}
The method getParameter is also pure virtual (and its implementation is postponed for a while), but the helper overload of operator[] is defined inline using the former method.
Finally, let's add a method which will be an entry point for the library on every tick.
It should be called inside EA's OnTick event handler.
{
protected:
datetime TimeCurrent()
{
//...
}
double AccountBalance()
{
//...
}
int OrdersTotal()
{
//...
}
int OrderSend(...)
{
//...
}
// ...
public:
virtual void trade() = 0;
};
The class contains not only the callback trade, but also stubs for most of standard API functions. It's not yet important what will be inside them. The main point to note here, is that from the method trade's point of view, all standard names will lead to the protected substitutes.
If you remember, we created OptimysticCallback in order to provide the callback method in EA, which would be called from the library. It means that we need to pass a reference to the object OptimysticCallback into the library somehow. The best solution is to add it as parameter into our factory function:
Optimystic *createInstance(OptimysticCallback &callback);
#import
Interface refinement
input int ReoptimizationPeriod = 100;
extern int MAPeriod = 10;
input int MAPeriodMin = 5;
input int MAPeriodMax = 25;
input int MAPeriodStep = 5;
#include <Optimystic.mqh>
class MyOptimysticCallback: public OptimysticCallback
{
public:
virtual void trade() // override
{
// some code analyzing iMA with MAPeriod
// the code of this method was originally placed in EA's OnTick
// ...
OrderSend(...);
}
};
MyOptimysticCallback myc; // instantiate MyOptimysticCallback object
Optimystic *p;
void OnInit()
{
p = createInstance(GetPointer(myc));
p.setHistoryDepth(HistoryDepth, UNIT_BAR);
p.setReoptimizationPeriod(ReoptimizationPeriod, UNIT_BAR);
p.addParameter("MAPeriod", MAPeriodMin, MAPeriodMax, MAPeriodStep);
}
void OnDeinit(const int reason)
{
delete p;
}
void OnTick()
{
p.onTick();
}
The control flow is normally as follows:
- EA creates a custom callback object derived from OptimysticCallback;
- EA creates an instance of Optimystic and passes the callback object to it;
- EA adjusts settings of the library;
- On every tick EA calls the library via Optimystic::onTick method;
- The library does some work behind the scenes and calls MyOptimysticCallback::trade method once for current datetime and parameter set;
- EA trades using OrderSend inside MyOptimysticCallback::trade;
- As far as the library is in real trading mode, OrderSend goes to the market;
- The library does some work behind the scenes and calls MyOptimysticCallback::trade method many times for different bars in past and different parameter sets;
- EA trades using OrderSend inside MyOptimysticCallback::trade;
- As far as the library is in optimization mode, OrderSend performs virtual trades;
- The library calculates performance indicators for all trades and finds best parameter set;
- ...
{
public:
virtual bool onBeforeOptimization() = 0;
virtual bool onApplyParameters() = 0;
virtual void trade(const int bar, const double Ask, const double Bid) = 0;
virtual void onReadyParameters()
{
}
virtual void onAfterOptimization() = 0;
Actually, we have added 4 methods at once, because they are logically interconnected. It's likely that EA will need to make some preparations before every optimization, this is why we introduced onBeforeOptimization. And we do already know what to place inside its concrete implementation:
{
p.setDeposit(::AccountBalance());
p.setSpread((int)MarketInfo(Symbol(), MODE_SPREAD));
return true;
}
As you remember, we have reserved setDeposit and setSpread in Optimystic. The method onBeforeOptimization returns boolean to let the library know if EA is ready to start optimization: by returning false EA can deny pending optimization.
{
MAPeriod = (int)p[0].getValue();
return true;
}
As you remember, operator[] is overloaded for Optimystic to return specific parameter, and it returns an object OptimysticParameter. In turn, it provides current parameter value via getValue.
{
double net = p.getOptimalPerformance(ESTIMATOR_NET_PROFIT);
double pf = p.getOptimalPerformance(ESTIMATOR_PROFIT_FACTOR);
double ddp = p.getOptimalPerformance(ESTIMATOR_DRAWDOWN_PERCENT);
int n = (int)p.getOptimalPerformance(ESTIMATOR_TRADES_COUNT);
Print("Performance: profit=", DoubleToString(net, 2), " PF=", (pf == DBL_MAX ? "n/a" : DoubleToString(pf, 2)), " DD%=", DoubleToString(ddp, 2), " N=", n);
}
Just as a brief reminder, the method getOptimalPerformance has been added to Optimystic a few paragraphs above.
{
if(p.getOptimalPerformance(ESTIMATOR_CUSTOM) > 0) // if have positive result
{
MAPeriod = (int)p[0].getBest();
}
}
So get back to the control flow and complement it:
What is the work behind the scenes that we mention all the time? This implies switching the trading context between real and virtual one. Most simple way to explain this is to look inside any wrapper function we have added into OptimysticCallback.
{
protected:
int OrdersTotal()
{
//...
}
What should we write instead the ellipsis?
{
public:
virtual datetime TimeCurrent() = 0;
virtual double AccountBalance() = 0;
//...
virtual int OrdersTotal() = 0;
virtual int OrderSend(string symbol, int cmd, double volume, double price, int deviation, double stoploss, double takeprofit, string comment = NULL, int magic = 0, datetime expiration = 0, color arrow = clrNONE) = 0;
//...
};
This is a contract for concrete implemention of our simulated trade context (I mean we will someday create a class, derived from the interface and populate all its method with complete meaning). We should somehow acquire a pointer to such implementation in OptimysticCallback and use it every time when optimization mode is on. For the simplicity, we can use the pointer itself as a flag: if it's empty - real trading is on, and if it's assigned to a virtual implementation - optimization mode is on.
{
private:
OptimysticTradeContext *context;
protected:
datetime TimeCurrent()
{
return context != NULL ? context.TimeCurrent() : ::TimeCurrent();
}
double AccountBalance()
{
return context != NULL ? context.AccountBalance() : ::AccountBalance();
}
// ...
public:
void setContext(OptimysticTradeContext *c)
{
context = c;
}
OptimysticTradeContext *getContext() const
{
return context;
}
// ...
};
The implementation of OptimysticTradeContext is hidden inside the library, and the library should call special setContext method to control current trade context. This is the work behind the scenes.
There is only one last touch that should be made upon the header. As you remember, we have the import directive which binds our factory method with ex4 library file. We can't leave it as is, because we're going to write implemetation for our classes, and the same header file should be included into the library source Optimystic.mq4 as well. But it can't import from itself. To solve the problem we add a macro definition:
#import "Optimystic.ex4"
Optimystic *createInstance(OptimysticCallback &callback);
#import
#endif
In EA source code OPTIMYSTIC_LIBRARY is undefined, so the library is imported. In our library surce code we'll define it as simple as:
#include <Optimystic.mqh>
and the header will be processed without an issue.
Implementation of the interfaces and example of EA with on the fly optimization will be studied in the part 2.