### 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; // Counter of the tester's running extern int TestsNumber = 200; // he check digit - total number of runs extern int MovingPeriodStepsNumber = 20; // Number of optimization steps for MovingPeriod extern int MovingShiftStepsNumber = 10; // Number of optimization steps for MovingShift extern double MovingPeriodLow = 150; // Lower limit of the optimization range for MovingPeriod extern double MovingShiftLow = 1; // Lower limit of the optimization range for MovingShift extern double MovingPeriodStep = 1; // Optimization step for MovingPeriod extern double MovingShiftStep = 1; // Optimization step for MovingShift

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) { // First run, create/initialize a datafile. h=FileOpen("test.txt",FILE_CSV|FILE_WRITE,';'); FileWrite(h,Criterion,MovingPeriod,MovingShift,Counter); // Remember the position of the file pointer after writing in the global variable 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 { // After the first start is processed, the data are added into the file h=FileOpen("test.txt",FILE_CSV|FILE_READ|FILE_WRITE,';'); // It is time to use the file pointer written in the global variable FilePtr = GlobalVariableGet("FilePtr"); FileSeek(h,FilePtr, SEEK_SET); FileWrite(h,Criterion,MovingPeriod,MovingShift,Counter); // Remember the file pointer position once again FilePtr = FileTell(h); GlobalVariableSet("FilePtr",FilePtr);

Now let us process the last start:

if (Counter == TestsNumber) { ArrayResize(Data,TestsNumber); // Returns the file pointer to the beginning FileSeek(h,0,SEEK_SET); // Read from the file the results of all testings int i = 0; while (i<TestsNumber && FileIsEnding(h)== false) { for (int j=0;j<4;j++) { Data[i][j]=FileReadNumber(h); } i++; } // Sort the array according to our optimization criterion ArraySort(Data,WHOLE_ARRAY,0,MODE_DESCEND); // Now let us arrange the results. Reopen the file 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:

// Code of the independent counter if (GlobalVariableCheck("TestsCnt")==false || Counter == 1) { TestsCnt = 0; GlobalVariableSet("TestsCnt",0); } else { TestsCnt = GlobalVariableGet("TestsCnt"); }

// Code of the independent counter TestsCnt++; GlobalVariableSet("TestsCnt",TestsCnt);

// Code of the independent counter 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.

Translated from Russian by MetaQuotes Software Corp.

Original article: https://www.mql5.com/ru/articles/1498

**Attached files**|