### Introduction

From time to time we hear opinions about the necessity of extending the set of optimization
criteria in MT4 tester. Still you can guess than no matter what criteria developers
add, there will always be users and situations requiring other criteria. Can this
problem be solved inside MQL4 and MetaTrader? Yes, it can. This article shows the
implementation of using a custom optimization criterion on the example of the standard
Expert Advisor Moving Average. The criterion here is the relation profit/drawdown.

### Expert Advisor

Let us start with the optimization criterion. For its calculation we need to trace
during testing the maximal balance and drawdown. In order not to depend on the
logic of the Expert Advisor operation, we will add the existing code strings in
the very beginning of the function start().

if (AccountEquity() > MaxEqu) MaxEqu = AccountEquity();
if (MaxEqu-AccountEquity() > MaxDD) MaxDD = MaxEqu-AccountEquity();

For processing the last tick they should be replicated in deinit(). After that we
can calculate the value of the optimization criterion.

Criterion = (AccountBalance()-StartBalance)/MaxDD;

Now we can start the main part - maintenance of the optimization process. We have
one problem - MQL4 does not have the built-in means of determining the end of the
optimization. The only way out, known to me, is the so called "optimization
on a counter". The meaning is the following: the only changeable parameter
of the Expert Advisor is the special external variable-counter. Still here is a
serious consequence - we lose the possibility to change real parameters of the
Expert Advisor in a standard way and should provide this ourselves. Another disadvantage
is that the optimization cache from our friend turns into our enemy. But the end
justifies the means, so let us continue.

Let us add external variables:

extern int Counter = 1;
extern int TestsNumber = 200;
extern int MovingPeriodStepsNumber = 20;
extern int MovingShiftStepsNumber = 10;
extern double MovingPeriodLow = 150;
extern double MovingShiftLow = 1;
extern double MovingPeriodStep = 1;
extern double MovingShiftStep = 1;

First comes the counter. The next variable is the check one (and information). Then
we assign number of steps, lower limit and optimization step for the two built-in
variables of the Moving Average, intended for optimization. You can see some excessiveness:
if we are going take the complete examination (and we are going to do it) the product
of MovingPeriodStepsNumber and MovingShiftStepsNumber should be equal to TestsNumber.

After each test run the Expert Advisor fully terminates its work and the next run
can be considered its reincarnation. We have two means of organizing "the
genetical storage": global variables and a separate file. We will use both.

Let us modify the function init():

int init() {
if (IsTesting() && TestsNumber > 0) {
if (GlobalVariableCheck("FilePtr")==false || Counter == 1) {
FilePtr = 0;
GlobalVariableSet("FilePtr",0);
} else {
FilePtr = GlobalVariableGet("FilePtr");
}
MovingPeriod = MovingPeriodLow+((Counter-1)/MovingShiftStepsNumber)*MovingPeriodStep;
MovingShift = MovingShiftLow+((Counter-1)%MovingShiftStepsNumber)*MovingShiftStep; StartBalance = AccountBalance();
MaxEqu = 0;
MaxDD = 0;
}
return(0);
}

Our addition is located inside the operation conditions only in the tester and at
non-zero TestsNumber. Thus the task TestsNumber=0 will turn the Expert Advisor
into a standard Moving Average. While we are discussing the process of optimization,
we must use any possibility for the process speed-up. That is why the code starts
with the provision of managing a through (through tester runs) file pointer using
a global variable. Then we calculate values of changeable parameters and initialize
variables used for optimization criterion calculation.

The main work should be done in the function deinit(). Upon the testing results
we will save in a text file values of the optimization criterion, values of the
optimized parameters and number of the test run. After the end of the optimization
the results will be sorted according to the optimization criterion and saved in
the same file. So, we need to process three situations: the first start, the last
start and all others. Let us use the tester runs counter for their separation. Processing the first start:

if (Counter == 1) {
h=FileOpen("test.txt",FILE_CSV|FILE_WRITE,';');
FileWrite(h,Criterion,MovingPeriod,MovingShift,Counter);
FilePtr = FileTell(h);
GlobalVariableSet("FilePtr",FilePtr);
FileClose(h);

The peculiarity of processing other starts is that the new data is added into the
file:

} else {
h=FileOpen("test.txt",FILE_CSV|FILE_READ|FILE_WRITE,';');
FilePtr = GlobalVariableGet("FilePtr");
FileSeek(h,FilePtr, SEEK_SET);
FileWrite(h,Criterion,MovingPeriod,MovingShift,Counter);
FilePtr = FileTell(h);
GlobalVariableSet("FilePtr",FilePtr);

Now let us process the last start:

if (Counter == TestsNumber) {
ArrayResize(Data,TestsNumber);
FileSeek(h,0,SEEK_SET);
int i = 0;
while (i<TestsNumber && FileIsEnding(h)== false) {
for (int j=0;j<4;j++) {
Data[i][j]=FileReadNumber(h);
}
i++;
}
ArraySort(Data,WHOLE_ARRAY,0,MODE_DESCEND);
FileClose(h);
h=FileOpen("test.txt",FILE_CSV|FILE_WRITE,' ');
FileWrite(h," Criterion"," MovingPeriod"," MovingShift"," Counter");
for (i=0;i<TestsNumber;i++) {
FileWrite(h,DoubleToStr(Data[i][0],10)," ",Data[i][1]," ",Data[i][2]," ",Data[i][3]);
}

The array was preliminarily initialized as double Data[][4]. That is all. Let us
clean up:

GlobalVariableDel("FilePtr");
}
FileClose(h);
}
}

Compile, open the tester and select the Expert Advisor. Then open the property sheet
of the Expert Advisor and check four positions:

- The product of MovingPeriodStepsNumber by MovingShiftStepsNumber MUST be equal
to TestsNumber.

- Optimisation must be conducted ONLY for the Counter,

- The optimization range MUST be from 1 to TestsNumber with the step 1.

- Genetical algorithm must be disabled.

Start the optimization. After the end move to the folder [Meta Trader]\tester\files
and view the result in the file test.txt. The author did it for EURUSD_H1 from
the middle of 2004 on the opening prices and got the following results:

And now let us get back to the statement that the cache is our enemy. The fact is,
when we take the testing results from the cache, the functions init() and deinit()
are not started. As a result, at the restart of the optimization all or part of
variants are unaccounted. Moreover, while the actual number of runs will be less
than TestsNumber, the array Data will contain some zeros. The author suggests two
ways of eliminating the "cache effect": recompilation of an Expert Advisor
or closing/pausing/opening the tester window.

The cache interference can be detected by an independent counting of test runs.
There are three commented insertions for the organization of such a counting using
a special global variable, suggested in the attached EA code:

if (GlobalVariableCheck("TestsCnt")==false || Counter == 1) {
TestsCnt = 0;
GlobalVariableSet("TestsCnt",0);
} else {
TestsCnt = GlobalVariableGet("TestsCnt");
}

TestsCnt++;
GlobalVariableSet("TestsCnt",TestsCnt);

GlobalVariableDel("TestsCnt");

And the last thing. An attentive reader must have noticed the fact that we can do
without the variable FilePtr (and the accompanying global variable) - the data
is written at the end of the file and read from the beginning. Then what is it
for? The answer is the following: this Expert Advisor is intended for the demonstration
of the optimization maintenance method. The method allows managing the operation
on-the-fly with the results of previous testings. And here the trough file pointer
can be rather useful, as well as the independent counter. As an example of tasks
requiring working with previous results on-the-fly we can name the management of
the out-of-sample testing and the implementation of one's own genetic algorithm.

### Conclusion

The causes for such an interest to this problem were topics from the forum http://forum.mql4.com.