OnTester

Вызывается в экспертах при наступлении события Tester для выполнения необходимых действий по окончании тестирования.

double  OnTester(void);

Возвращаемое значение

Значение пользовательского критерия оптимизации для оценки результатов тестирования.  

Примечание

Функция OnTester() может быть использована только в экспертах при тестировании и предназначена в первую очередь для расчета некоторого значения, используемого в качестве критерия "Custom max" при оптимизации входных параметров.

При генетической оптимизации сортировка результатов в пределах одного поколения производится по убыванию. Это означает, что лучшими с точки зрения критерия оптимизации считаются результаты с наибольшим значением. Худшие значения при такой сортировке помещаются в конец и впоследствии отбрасываются и не принимают участия в формировании следующего поколения.

Таким образом, с помощью функции OnTester() можно не только создавать и сохранять собственные отчеты результатов тестирования, но и управлять ходом оптимизации для поиска наилучших параметров торговой стратегии.

Пример расчета пользовательского критерия оптимизации. Идея заключается в вычислении линейной регрессии графика баланса и описана в статье Оптимизируем стратегию по графику баланса и сравниваем результаты с критерием "Balance + max Sharpe Ratio"

//+------------------------------------------------------------------+
//|                                              OnTester_Sample.mq5 |
//|                        Copyright 2018, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2000-2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property description "Пример советника с обработчиком OnTester()"
#property description "В качестве пользовательского критерия оптимизации "
#property description "возвращается коэффициент линейной регрессии графика баланса,"
#property description "деленный на среднеквадратичную ошибку отклонения"
//--- подключим класс торговых операций
#include <Trade\Trade.mqh>
//--- входные параметры эксперта
input double Lots               = 0.1;     // Объем
input int    Slippage           = 10;      // Допустимое проскальзывание
input int    MovingPeriod       = 80;      // Период скользящей средней
input int    MovingShift        = 6;       // Сдвиг скользящей средней
//--- глобальные переменные
int    IndicatorHandle=0;  // хендл индикатора
bool   IsHedging=false;    // признак счета
CTrade trade;              // для проведения торговых операций
//--- 
#define EA_MAGIC 18052018
//+------------------------------------------------------------------+
//| Проверка условий на открытие позиции                             |
//+------------------------------------------------------------------+
void CheckForOpen(void)
  {
   MqlRates rt[2];
//--- торгуем только в начале нового бара
   if(CopyRates(_Symbol,_Period,0,2,rt)!=2)
     {
      Print("CopyRates of ",_Symbol," failed, no history");
      return;
     }
//--- тиковый объем 
   if(rt[1].tick_volume>1)
      return;
//--- получим значения скользящей средней
   double   ma[1];
   if(CopyBuffer(IndicatorHandle,0,1,1,ma)!=1)
     {
      Print("CopyBuffer from iMA failed, no data");
      return;
     }
//--- проверим наличие сигнала
   ENUM_ORDER_TYPE signal=WRONG_VALUE;
//--- свеча открылась выше, а закрылась ниже скользящей средней
   if(rt[0].open>ma[0] && rt[0].close<ma[0])
      signal=ORDER_TYPE_BUY;    // сигнал на покупку
   else // свеча открылась ниже, а закрылась выше скользящей средней
     {
      if(rt[0].open<ma[0] && rt[0].close>ma[0])
         signal=ORDER_TYPE_SELL;// сигнал на продажу
     }
//--- дополнительные проверки
   if(signal!=WRONG_VALUE)
     {
      if(TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) && Bars(_Symbol,_Period)>100)
        {
         double price=SymbolInfoDouble(_Symbol,signal==ORDER_TYPE_SELL ? SYMBOL_BID:SYMBOL_ASK);
         trade.PositionOpen(_Symbol,signal,Lots,price,0,0);
        }
     }
//---
  }
//+------------------------------------------------------------------+
//| Проверка условий на закрытие позиции                             |
//+------------------------------------------------------------------+
void CheckForClose(void)
  {
   MqlRates rt[2];
//--- торгуем только в начале нового бара
   if(CopyRates(_Symbol,_Period,0,2,rt)!=2)
     {
      Print("CopyRates of ",_Symbol," failed, no history");
      return;
     }
   if(rt[1].tick_volume>1)
      return;
//--- получим значения скользящей средней
   double   ma[1];
   if(CopyBuffer(IndicatorHandle,0,1,1,ma)!=1)
     {
      Print("CopyBuffer from iMA failed, no data");
      return;
     }
//--- позиция уже была выбрана ранее с помощью PositionSelect()
   bool signal=false;
   long type=PositionGetInteger(POSITION_TYPE);
//--- свеча открылась выше, а закрылась ниже скользящей средней - закрываем короткую позицию
   if(type==(long)POSITION_TYPE_SELL && rt[0].open>ma[0] && rt[0].close<ma[0])
      signal=true;
//--- свеча открылась ниже, а закрылась выше скользящей средней - закрываем длинную позицию
   if(type==(long)POSITION_TYPE_BUY && rt[0].open<ma[0] && rt[0].close>ma[0])
      signal=true;
//--- дополнительные проверки
   if(signal)
     {
      if(TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) && Bars(_Symbol,_Period)>100)
         trade.PositionClose(_Symbol,Slippage);
     }
//---
  }
//+------------------------------------------------------------------+
//| Выбираем позицию с учетом типа счета: Netting или Hedging        |
//+------------------------------------------------------------------+
bool SelectPosition()
  {
   bool res=false;
//--- выбор позиции для счета Hedging
   if(IsHedging)
     {
      uint total=PositionsTotal();
      for(uint i=0; i<total; i++)
        {
         string position_symbol=PositionGetSymbol(i);
         if(_Symbol==position_symbol && EA_MAGIC==PositionGetInteger(POSITION_MAGIC))
           {
            res=true;
            break;
           }
        }
     }
//--- выбор позиции для счета Netting
   else
     {
      if(!PositionSelect(_Symbol))
         return(false);
      else
         return(PositionGetInteger(POSITION_MAGIC)==EA_MAGIC); //---проверка Magic number
     }
//--- результат выполнения
   return(res);
  }
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(void)
  {
//--- установим тип торговли: Netting или Hedging
   IsHedging=((ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING);
//--- инициализируем объект для правильного контроля позиций
   trade.SetExpertMagicNumber(EA_MAGIC);
   trade.SetMarginMode();
   trade.SetTypeFillingBySymbol(Symbol());
   trade.SetDeviationInPoints(Slippage);
//--- создадим индикатор Moving Average 
   IndicatorHandle=iMA(_Symbol,_Period,MovingPeriod,MovingShift,MODE_SMA,PRICE_CLOSE);
   if(IndicatorHandle==INVALID_HANDLE)
     {
      printf("Ошибка при создании индикатора iMA");
      return(INIT_FAILED);
     }
//--- ok
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(void)
  {
//--- если позиция уже открыта, то проверим условие на закрытие
   if(SelectPosition())
      CheckForClose();
// проверим условие на открытие позиции
   CheckForOpen();
//---
  }
//+------------------------------------------------------------------+
//| Tester function                                                  |
//+------------------------------------------------------------------+
double OnTester()
  {
//--- значение пользовательского критерия оптимизации (чем больше, тем лучше)
   double ret=0.0;
//--- получим результаты трейдов в массив 
   double array[];
   double trades_volume;
   GetTradeResultsToArray(array,trades_volume);
   int trades=ArraySize(array);
//--- если трейдов меньше 10, то тестирование не дало положительных результатов
   if(trades<10)
      return (0);
//--- средний результат на трейд
   double average_pl=0;
   for(int i=0;i<ArraySize(array);i++)
      average_pl+=array[i];
   average_pl/=trades;
//--- выведем сообщение для режима одиночного тестирования
   if(MQLInfoInteger(MQL_TESTER) && !MQLInfoInteger(MQL_OPTIMIZATION))
      PrintFormat("%s: Трейдов=%d, Средняя прибыль=%.2f",__FUNCTION__,trades,average_pl);
//--- посчитаем коэффициенты линейной регрессии для графика прибыли
   double a,b,std_error;
   double chart[];
   if(!CalculateLinearRegression(array,chart,a,b))
      return (0);
//--- вычислим ошибку отклонения графика от линии регрессии
   if(!CalculateStdError(chart,a,b,std_error))
      return (0);
//--- вычислим отношение трендовой прибыли к среднеквадратичному отклонению
   ret=(std_error == 0.0) ? a*trades : a*trades/std_error;
//--- вернем значение пользовательского критерия оптимизации
   return(ret);
  }
//+------------------------------------------------------------------+
//| Получить массив прибылей/убытков из сделок                       |
//+------------------------------------------------------------------+
bool GetTradeResultsToArray(double &pl_results[],double &volume)
  {
//--- запросим полную торговую историю
   if(!HistorySelect(0,TimeCurrent()))
      return (false);
   uint total_deals=HistoryDealsTotal();
   volume=0;
//--- установим начальный размер массива с запасом - по количеству сделок в истории
   ArrayResize(pl_results,total_deals);
//--- счетчик сделок, фиксирующих торговый результат - прибыль или убыток
   int counter=0;
   ulong ticket_history_deal=0;
//--- пройдем по всем сделкам
   for(uint i=0;i<total_deals;i++)
     {
      //--- выберем сделку 
      if((ticket_history_deal=HistoryDealGetTicket(i))>0)
        {
         ENUM_DEAL_ENTRY deal_entry  =(ENUM_DEAL_ENTRY)HistoryDealGetInteger(ticket_history_deal,DEAL_ENTRY);
         long            deal_type   =HistoryDealGetInteger(ticket_history_deal,DEAL_TYPE);
         double          deal_profit =HistoryDealGetDouble(ticket_history_deal,DEAL_PROFIT);
         double          deal_volume =HistoryDealGetDouble(ticket_history_deal,DEAL_VOLUME);
         //--- нас интересуют только торговые операции        
         if((deal_type!=DEAL_TYPE_BUY) && (deal_type!=DEAL_TYPE_SELL))
            continue;
         //--- только сделки с фиксацией прибыли/убытка
         if(deal_entry!=DEAL_ENTRY_IN)
           {
            //--- запишем торговый результат в массив и увеличим счетчик трейдов
            pl_results[counter]=deal_profit;
            volume+=deal_volume;
            counter++;
           }
        }
     }
//--- установим окончательный размер массива
   ArrayResize(pl_results,counter);
   return (true);
  }
//+------------------------------------------------------------------+
//| Вычисляет линейную регрессию вида y=a*x+b                        |
//+------------------------------------------------------------------+
bool CalculateLinearRegression(double  &change[],double &chartline[],
                               double  &a_coef,double  &b_coef)
  {
//--- проверим достаточность данных
   if(ArraySize(change)<3)
      return (false);
//--- создадим массив графика с накоплением
   int N=ArraySize(change);
   ArrayResize(chartline,N);
   chartline[0]=change[0];
   for(int i=1;i<N;i++)
      chartline[i]=chartline[i-1]+change[i];
//--- теперь вычислим коэффициенты регрессии
   double x=0,y=0,x2=0,xy=0;
   for(int i=0;i<N;i++)
     {
      x=x+i;
      y=y+chartline[i];
      xy=xy+i*chartline[i];
      x2=x2+i*i;
     }
   a_coef=(N*xy-x*y)/(N*x2-x*x);
   b_coef=(y-a_coef*x)/N;
//---
   return (true);
  }
//+------------------------------------------------------------------+
//|  Вычисляет среднеквад. ошибку отклонения для заданных a и b      |
//+------------------------------------------------------------------+
bool  CalculateStdError(double  &data[],double  a_coef,double  b_coef,double &std_err)
  {
//--- сумма квадратов ошибки
   double error=0;
   int N=ArraySize(data);
   if(N<=2)
      return (false);
   for(int i=0;i<N;i++)
      error+=MathPow(a_coef*i+b_coef-data[i],2);
   std_err=MathSqrt(error/(N-2));
//--- 
   return (true);
  }

Смотри также

Тестирование торговых стратегий, TesterHideIndicators, Работа с результатами оптимизации, TesterStatistics, OnTesterInit, OnTesterDeinit, OnTesterPass, MQL_TESTER, MQL_OPTIMIZATION, FileOpen, FileWrite, FileLoad, FileSave