Получение финансовых показателей теста: TesterStatistics

Обычно мы оцениваем качество эксперта по торговому отчету, аналогом которого в тестере является отчет тестирования. В нем представлено большое количество показателей, характеризующих стиль торговли, стабильность и, разумеется, прибыльность. Все эти показатели, за некоторым исключением, доступны MQL-программе посредством специальной функции TesterStatistics. Таким образом, разработчик эксперта имеет возможность анализировать в коде отдельные показатели и конструировать из них собственные комбинированные критерии качества оптимизации.

double TesterStatistics(ENUM_STATISTICS statistic)

Функция TesterStatistics возвращает значение указанного статистического показателя, рассчитанного по результатам отдельного прогона эксперта в тестере. Функцию можно вызывать в обработчике OnDeinit или OnTester, о котором речь еще впереди.

Все доступные показатели сведены в перечисление ENUM_STATISTICS. Часть показателей представляет собой качественные характеристики, то есть вещественные числа (как правило, итоговые суммы прибыли, просадки, коэффициенты и так далее), а другая часть — количественные, то есть целые числа (например, количество сделок). Однако обе группы управляются одной и той же функцией, несмотря на её тип результат double.

В следующей таблице приведены показатели вещественные (денежные суммы и коэффициенты). Все денежные суммы выражаются в валюте депозита.

Идентификатор

Описание

STAT_INITIAL_DEPOSIT

Начальный депозит

STAT_WITHDRAWAL

Размер выведенных со счета средств

STAT_PROFIT

Чистая прибыль или убыток по окончании тестирования, сумма STAT_GROSS_PROFIT и STAT_GROSS_LOSS

STAT_GROSS_PROFIT

Общая прибыль, сумма всех прибыльных трейдов (больше или равно нулю)

STAT_GROSS_LOSS

Общий убыток, сумма всех убыточных трейдов (меньше или равно нулю)

STAT_MAX_PROFITTRADE

Максимальная прибыль — наибольшее значение среди всех прибыльных трейдов (больше или равно нулю)

STAT_MAX_LOSSTRADE

Максимальный убыток — наименьшее значение среди всех убыточных трейдов (меньше или равно нулю)

STAT_CONPROFITMAX

Общая максимальная прибыль в серии прибыльных трейдов (больше или равно нулю)

STAT_MAX_CONWINS

Общая прибыль в самой длинной серии прибыльных трейдов

STAT_CONLOSSMAX

Общий максимальный убыток в серии убыточных трейдов (меньше или равно нулю)

STAT_MAX_CONLOSSES

Общий убыток в самой длинной серии убыточных трейдов

STAT_BALANCEMIN

Минимальное значение баланса

STAT_BALANCE_DD

Максимальная просадка баланса в деньгах

STAT_BALANCEDD_PERCENT

Просадка баланса в процентах, которая была зафиксирована в момент максимальной просадки баланса в деньгах (STAT_BALANCE_DD)

STAT_BALANCE_DDREL_PERCENT

Максимальная просадка баланса в процентах

STAT_BALANCE_DD_RELATIVE

Просадка баланса в деньгах, которая была зафиксирована в момент максимальной просадки баланса в процентах (STAT_BALANCE_DDREL_PERCENT)

STAT_EQUITYMIN

Минимальное значение собственных средств

STAT_EQUITY_DD

Максимальная просадка средств в деньгах

STAT_EQUITYDD_PERCENT

Просадка средств в процентах, которая была зафиксирована в момент максимальной просадки средств в деньгах (STAT_EQUITY_DD)

STAT_EQUITY_DDREL_PERCENT

Максимальная просадка средств в процентах

STAT_EQUITY_DD_RELATIVE

Просадка средств в деньгах, которая была зафиксирована в момент максимальной просадки средств в процентах (STAT_EQUITY_DDREL_PERCENT)

STAT_EXPECTED_PAYOFF

Математическое ожидание выигрыша (среднее арифметические общей прибыли и количества сделок)

STAT_PROFIT_FACTOR

Прибыльность — отношение STAT_GROSS_PROFIT/STAT_GROSS_LOSS (если STAT_GROSS_LOSS = 0, прибыльность принимает значение DBL_MAX)

STAT_RECOVERY_FACTOR

Фактор восстановления — отношение STAT_PROFIT/STAT_BALANCE_DD

STAT_SHARPE_RATIO

Коэффициент Шарпа

STAT_MIN_MARGINLEVEL

Минимальное достигнутое значение уровня маржи

STAT_CUSTOM_ONTESTER

Значение пользовательского критерия оптимизации, возвращенного функцией OnTester

В следующей таблице приведены показатели целочисленные (количества).

Идентификатор

Описание

STAT_DEALS

Общее количество совершенных сделок

STAT_TRADES

Количество трейдов (сделок выхода из рынка)

STAT_PROFIT_TRADES

Прибыльные трейды

STAT_LOSS_TRADES

Убыточные трейды

STAT_SHORT_TRADES

Короткие трейды

STAT_LONG_TRADES

Длинные трейды

STAT_PROFIT_SHORTTRADES

Короткие прибыльные трейды

STAT_PROFIT_LONGTRADES

Длинные прибыльные трейды

STAT_PROFITTRADES_AVGCON

Средняя длина прибыльной серии трейдов

STAT_LOSSTRADES_AVGCON

Средняя длина убыточной серии трейдов

STAT_CONPROFITMAX_TRADES

Количество трейдов, сформировавших STAT_CONPROFITMAX (максимальная прибыль в последовательности прибыльных трейдов)

STAT_MAX_CONPROFIT_TRADES

Количество трейдов в самой длинной серии прибыльных трейдов STAT_MAX_CONWINS

STAT_CONLOSSMAX_TRADES

Количество трейдов, сформировавших STAT_CONLOSSMAX (максимальный убыток в последовательности убыточных трейдов)

STAT_MAX_CONLOSS_TRADES

Количество трейдов в самой длинной серии убыточных трейдов STAT_MAX_CONLOSSES

Попробуем воспользоваться представленными показателями, чтобы создать свой собственный комплексный критерий качества эксперта. Для этого нам нужен некий "подопытный" пример MQL-программы. Возьмем за отправную точку эксперт MultiMartingale.mq5, но упростим его: уберем мультивалютность, встроенную обработку ошибок и работу по расписанию. Более того, выберем для него сигнальную торговую стратегию с однократным расчетом на баре, то есть по ценам открытия. Это позволит ускорить оптимизацию и расширить поле для экспериментов.

Стратегия будет основываться на состояниях перекупленности и перепроданности, определяемых индикатором OsMA. Динамически находить границы избыточной волатильности, означающей торговые сигналы, поможет индикатор Bollinger Bands, наложенный на OsMA.

Когда OsMA будет возвращаться внутрь коридора, пересекая нижнюю границу снизу вверх, будем открывать покупку. Когда OsMA будет аналогичным образом пересекать сверху вниз верхнюю границу, будем продавать. Для выхода из позиций используем еще один индикатор — скользящую среднюю, также примененную к OsMA. Если OsMA продемонстрирует обратное движение (вниз для длинной позиции или вверх для короткой) и коснется MA, позиция будет закрыта. Данная стратегия иллюстрируется следующим скриншотом.

Торговая стратегия на индикаторах OsMA, BBands и MA

Торговая стратегия на индикаторах OsMA, BBands и MA

Синяя вертикальная линия соответствует бару, на котором открыта покупка, так как на двух предыдущих барах произошло пересечение нижней линии Боллинджера гистограммой OsMA снизу вверх (в подокне это место помечено полой синей стрелкой). Красная вертикальная линия — это место возникновения обратного сигнала, поэтому покупка была закрыта и открыта продажа. В подокне в этом месте (а точнее, на двух предыдущих барах, где стоит полая красная стрелка) гистограмма OsMA пересекает верхнюю линию Боллинджера сверху вниз. Наконец зеленая линия обозначает закрытие продажи, из-за того, что гистограмма стала подниматься выше красной MA.

Дадим эксперту говорящее имя BandOsMA.mq5. В общие настройки войдут магическое число, фиксированный лот и дистанция стоплосса в пунктах. Для стоплосса оставим сопровождение с помощью TrailingStop из прошлого примера. Тейкпрофит здесь не используется.

input group "C O M M O N   S E T T I N G S"
sinput ulong Magic = 1234567890;
input double Lots = 0.01;
input int StopLoss = 1000;

Три группы настроек предназначены для индикаторов.

input group "O S M A   S E T T I N G S"
input int FastOsMA = 12;
input int SlowOsMA = 26;
input int SignalOsMA = 9;
input ENUM_APPLIED_PRICE PriceOsMA = PRICE_TYPICAL;
   
input group "B B A N D S   S E T T I N G S"
input int BandsMA = 26;
input int BandsShift = 0;
input double BandsDeviation = 2.0;
   
input group "M A   S E T T I N G S"
input int PeriodMA = 10;
input int ShiftMA = 0;
input ENUM_MA_METHOD MethodMA = MODE_SMA;

В эксперте MultiMartingale.mq5 у нас фактически не было торговых сигналов: в какую сторону открываться, задавал пользователь. Здесь у нас появились торговые сигналы, и имеет смысл оформить их отдельным классом. Для начала опишем абстрактный интерфейс TradingSignal.

interface TradingSignal
{
   virtual int signal(void);
};

Он такой же простой, как и другой наш интерфейс TradingStrategy. И это хорошо. Чем проще интерфейсы и объекты, тем вероятнее, что они отвечают за один единственный род деятельности, что является хорошим стилем программирования, поскольку минимизирует ошибки и делает более понятными крупные программные проекты. Благодаря абстракции в любой программе, использующий TradingSignal, можно будет заменить один сигнал на другой. Впрочем, и как одну стратегию — на другую. У нас сейчас стратегии отвечают за подготовку и отправку приказов, а сигналы их инициируют за счет анализа рынка.

В нашем случае конкретную реализацию TradingSignal упакуем в класс BandOsMaSignal. Разумеется, нам потребуются переменные для хранения дескрипторов 3-х индикаторов. Создание экземпляров индикаторов и их удаление производится, соответственно, в конструкторе и деструкторе. Все параметры будут передаваться из входных переменных. Обратите внимание, что iBands и iMA строятся на дескрипторе hOsMA.

class BandOsMaSignalpublic TradingSignal
{
   int hOsMAhBandshMA;
   int direction;
public:
   BandOsMaSignal(const int fastconst int slowconst int signal,
      const ENUM_APPLIED_PRICE price,
      const int bandsconst int shiftconst double deviation,
      const int periodconst int xENUM_MA_METHOD method)
   {
      hOsMA = iOsMA(_Symbol_Periodfastslowsignalprice);
      hBands = iBands(_Symbol_PeriodbandsshiftdeviationhOsMA);
      hMA = iMA(_Symbol_PeriodperiodxmethodhOsMA);
      direction = 0;
   }
   
   ~BandOsMaSignal()
   {
      IndicatorRelease(hMA);
      IndicatorRelease(hBands);
      IndicatorRelease(hOsMA);
   }
   ...

Направление текущего торгового сигнала находится в переменной direction: 0 — нет сигналов (неопределенная ситуация), +1 — покупка, -1 — продажа. Заполнение этой переменной произведем в методе signal. Его код повторяет на MQL5 приведенное выше словесное описание сигналов.

   virtual int signal(voidoverride
   {
      double osma[2], upper[2], lower[2], ma[2];
      // получаем два значения каждого индикатора на барах 1 и 2
      if(CopyBuffer(hOsMA012osma) != 2return 0;
      if(CopyBuffer(hBandsUPPER_BAND12upper) != 2return 0;
      if(CopyBuffer(hBandsLOWER_BAND12lower) != 2return 0;
      if(CopyBuffer(hMA012ma) != 2return 0;
      
      // если уже был сигнал, проверяем не закончился ли он
      if(direction != 0)
      {
         if(direction > 0)
         {
            if(osma[0] >= ma[0] && osma[1] < ma[1])
            {
               direction = 0;
            }
         }
         else
         {
            if(osma[0] <= ma[0] && osma[1] > ma[1])
            {
               direction = 0;
            }
         }
      }
      
      // в любом случае проверяем, нет ли нового сигнала      
      if(osma[0] <= lower[0] && osma[1] > lower[1])
      {
         direction = +1;
      }
      else if(osma[0] >= upper[0] && osma[1] < upper[1])
      {
         direction = -1;
      }
      
      return direction;
   }
};

Как легко заметить, значения индикаторов считываются для баров 1 и 2, поскольку мы будем работать по открытию бара, и 0-й бар не то что незавершенный, а он только что открылся, когда мы вызовем метод signal.

Новый класс, реализующий интерфейс TradingStrategy, назовем SimpleStrategy.

В нем мы найдем кое-что новое, но и кое-что старое. В частности, в нем остались автоуказатели для PositionState и TrailingStop, зато добавился автоуказатель на сигнал TradingSignal. Также, поскольку мы собираемся торговать только по открытию баров, потребовалась переменная lastBar, в которой будет храниться время последнего обработанного бара.

class SimpleStrategypublic TradingStrategy
{
protected:
   AutoPtr<PositionStateposition;
   AutoPtr<TrailingStoptrailing;
   AutoPtr<TradingSignalcommand;
   
   const int stopLoss;
   const ulong magic;
   const double lots;
   
   datetime lastBar;
   ...

В конструктор SimpleStrategy передаются глобальные параметры, а также указатель на объект TradingSignal — очевидно, что это в данном случае будет BandOsMaSignal, и его должен будет создать вызывающий код. Далее конструктор пытается найти среди существующих позиций те, что имеют нужные magic-число и символ, и в случае успеха подключает к ней сопровождение. Это пригодится в случае, если в работе эксперт возник по тем или иным причинам перерыв, а позиция уже была открыта.

public:
   SimpleStrategy(TradingSignal *signalconst ulong mconst int slconst double v):
      command(signal), magic(m), stopLoss(sl), lots(v), lastBar(0)
   {
      // подбираем "свою" позицию среди существующих (если есть подходящая)
      PositionFilter positions;
      ulong tickets[];
      positions.let(POSITION_MAGICmagic).let(POSITION_SYMBOL_Symbol).select(tickets);
      const int n = ArraySize(tickets);
      if(n > 1)
      {
         Alert(StringFormat("Too many positions: %d"n));
         // TODO: закрыть лишние позиции - это не допускается стратегией
      }
      else if(n > 0)
      {
         position = new PositionState(tickets[0]);
         if(stopLoss)
         {
           trailing = new TrailingStop(tickets[0], stopLossstopLoss / 50);
         }
      }
   }

Реализация метода trade во многом похожа на пример с мартингейлом, но здесь отсутствуют умножения лотов и добавился вызов метода signal.

   virtual bool trade() override
   {
      // работаем только один раз при появлении нового бара
      if(lastBar == iTime(_Symbol_Period0)) return false;
      
      int s = command[].signal(); // получаем сигнал
      
      ulong ticket = 0;
      
      if(position[] != NULL)
      {
         if(position[].refresh()) // позиция существует
         {
            // сигнал изменился на противоположный или пропал
            if((position[].get(POSITION_TYPE) == POSITION_TYPE_BUY && s != +1)
            || (position[].get(POSITION_TYPE) == POSITION_TYPE_SELL && s != -1))
            {
               PrintFormat("Signal lost: %d for position %d %lld",
                  sposition[].get(POSITION_TYPE), position[].get(POSITION_TICKET));
               if(close(position[].get(POSITION_TICKET)))
               {
                  position = NULL;
               }
               else
               {
                  // актуализируем внутренний флаг 'ready'
                  // согласно тому, было или нет закрытие
                  position[].refresh();
               }
            }
            else
            {
               position[].update();
               if(trailing[]) trailing[].trail();
            }
         }
         else // позиция закрыта
         {
            position = NULL;
         }
      }
      
      if(position[] == NULL && s != 0)
      {
         ticket = (s == +1) ? openBuy() : openSell();
      }
      
      if(ticket > 0// новая позиция только что открыта
      {
         position = new PositionState(ticket);
         if(stopLoss)
         {
            trailing = new TrailingStop(ticketstopLossstopLoss / 50);
         }
      }
      // запоминаем текущий бар
      lastBar = iTime(_Symbol_Period0);
      
      return true;
   }

Вспомогательные методы openBuy, openSell и другие претерпели минимальные изменения, поэтому мы не станем их приводить (полный исходный код прилагается).

Поскольку в данном эксперте у нас всегда только одна стратегия, в отличие от мультивалютного мартингейла, где каждый символ требовал собственных настроек, исключим пул стратегий и будем управлять объектом стратегии напрямую.

AutoPtr<TradingStrategystrategy;
   
int OnInit()
{
   if(FastOsMA >= SlowOsMAreturn INIT_PARAMETERS_INCORRECT;
   strategy = new SimpleStrategy(
      new BandOsMaSignal(FastOsMASlowOsMASignalOsMAPriceOsMA,
         BandsMABandsShiftBandsDeviation,
         PeriodMAShiftMAMethodMA),
         MagicStopLossLots);
   return INIT_SUCCEEDED;
}
   
void OnTick()
{
   if(strategy[] != NULL)
   {
      strategy[].trade();
   }
}

Теперь, вооружившись экспертом как инструментом, перейдем вплотную к исследованию тестера. Для начала создадим вспомогательную структуру для запроса и хранения всех статистических данных TesterRecord.

struct TesterRecord
{
   string feature;
   double value;
   
   static void fill(TesterRecord &stats[])
   {
      ResetLastError();
      for(int i = 0; ; ++i)
      {
         const double v = TesterStatistics((ENUM_STATISTICS)i);
         if(_LastErrorreturn;
         TesterRecord t = {EnumToString((ENUM_STATISTICS)i), v};
         PUSH(statst);
      }
   }
};

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

С помощью структуры проверим в обработчике OnDeinit, что MQL5 API возвращает нам те же значения, что и отчет тестера.

void OnDeinit(const int)
{
   TesterRecord stats[];
   TesterRecord::fill(stats);
   ArrayPrint(stats2);
}

Например, при запуске на EURUSD,H1 с депозитом 10000 и без всяких оптимизаций (с настройками по умолчанию) получим за 2021 год примерно такие величины (фрагмент):

                        [feature]  [value]
[ 0] "STAT_INITIAL_DEPOSIT"       10000.00
[ 1] "STAT_WITHDRAWAL"                0.00
[ 2] "STAT_PROFIT"                    6.01
[ 3] "STAT_GROSS_PROFIT"            303.63
[ 4] "STAT_GROSS_LOSS"             -297.62
[ 5] "STAT_MAX_PROFITTRADE"          15.15
[ 6] "STAT_MAX_LOSSTRADE"           -10.00
...
[27] "STAT_DEALS"                   476.00
[28] "STAT_TRADES"                  238.00
...
[37] "STAT_CONLOSSMAX_TRADES"         8.00
[38] "STAT_MAX_CONLOSS_TRADES"        8.00
[39] "STAT_PROFITTRADES_AVGCON"       2.00
[40] "STAT_LOSSTRADES_AVGCON"         2.00

Зная все эти величины, мы можем изобретать собственную формулу комбинированного показателя качества эксперта и заодно целевую функцию оптимизации. Но значение этого показателя в любом случае нужно будет сообщать тестеру. И именно для этого служит функция OnTester.