English 中文 Español Deutsch 日本語 Português
preview
Непрерывная скользящая оптимизация (Часть 2): Механизм создания отчета оптимизации для любого робота

Непрерывная скользящая оптимизация (Часть 2): Механизм создания отчета оптимизации для любого робота

MetaTrader 5Примеры | 31 декабря 2019, 12:51
3 217 2
Andrey Azatskiy
Andrey Azatskiy

Введение

Продолжаем цикл статей про создание автоматического оптимизатора для проведения скользящей оптимизации торговых стратегий. Если прошлая статья повествовала о создании DLL-библиотеки, которая будет использоваться в нашем автооптимизаторе и в роботе, то данная статья будет целиком посвящена языку MQL5. Мы рассмотрим методы формирования отчетов оптимизации и способы использования данного функционала в ваших алгоритмах. 

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

Внедрение пользовательской комиссии и проскальзывания

Тестер платформы MetaTrader 5 имеет множество прекрасных возможностей, однако порой некоторые брокеры не добавляют в историю комиссию за сделку, либо же в выгрузку для дополнительного тестирования стратегии хочется добавить дополнительную комиссию — для этих целей был добавлен класс, сохраняющий комиссии для каждого отдельного символа. Позже при вызове соответствующего метода он выдает комиссию и заложенное проскальзывание. Сам класс озаглавлен следующим образом:

class CCCM
  {
private:
   struct Keeper
     {
      string            symbol;
      double            comission;
      double            shift;
     };

   Keeper            comission_data[];
public:

   void              add(string symbol,double comission,double shift);

   double            get(string symbol,double price,double volume);
   void              remove(string symbol);
  };

Для данного класса создана структура Keeper, которая хранит в себе комиссию и проскальзывание для задаваемого актива, а также создан массив данных структур, в котором хранятся все переданные комиссии и проскальзывания. Также объявлены 3 метода, которые добавляют, получают и удаляют данные. Метод добавления актива реализован следующим образом: 

void CCCM::add(string symbol,double comission,double shift)
{
 int s=ArraySize(comission_data);

 for(int i=0;i<s;i++)
   {
    if(comission_data[i].symbol==symbol)
        return;
   }

 ArrayResize(comission_data,s+1,s+1);

 Keeper keeper;
 keeper.symbol=symbol;
 keeper.comission=MathAbs(comission);
 keeper.shift=MathAbs(shift);

 comission_data[s]=keeper;
}

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

  • Комиссия — в зависимости от типа актива добавляется в валюте, в которой оценивается PL или же в процентах от наторгованного объема,
  • Проскальзывание — добавляется всегда в пунктах. 

Также стоит заметить, что данные величины добавляются не на круг (открытие + закрытие), а на сделку, т.е. на круг будет n*comission + n*shift, где n — количество сделок открывающих и закрывающих позицию вместе взятых.

Метод remove удаляет выбранный актив, причем в качестве ключа используется имя символа.

void CCCM::remove(string symbol)
{
 int total=ArraySize(comission_data);
 int ind=-1;
 for(int i=0;i<total;i++)
   {
    if(comission_data[i].symbol==symbol)
      {
       ind=i;
       break;
      }
   }
 if(ind!=-1)
    ArrayRemove(comission_data,ind,1);
}

Если не найден соответствующий символ, то метод завершается без удаления какого-либо актива.

Для получения выбранного сдвига и комиссии используется метод get, а его реализация различается для разных видов активов. 

double CCCM::get(string symbol,double price,double volume)
{

 int total=ArraySize(comission_data);
 for(int i=0;i<total;i++)
   {
    if(comission_data[i].symbol==symbol)
      {
       ENUM_SYMBOL_CALC_MODE mode=(ENUM_SYMBOL_CALC_MODE)SymbolInfoInteger(symbol,SYMBOL_TRADE_CALC_MODE);

       double shift=comission_data[i].shift*SymbolInfoDouble(symbol,SYMBOL_TRADE_TICK_VALUE);

       double ans;
       switch(mode)
         {
          case SYMBOL_CALC_MODE_FOREX :
             ans=(comission_data[i].comission+shift)*volume;
             break;
          case SYMBOL_CALC_MODE_FOREX_NO_LEVERAGE :
             ans=(comission_data[i].comission+shift)*volume;
             break;
          case SYMBOL_CALC_MODE_FUTURES :
             ans=(comission_data[i].comission+shift)*volume;
             break;
          case SYMBOL_CALC_MODE_CFD :
             ans=(comission_data[i].comission+shift)*volume;
             break;
          case SYMBOL_CALC_MODE_CFDINDEX :
             ans=(comission_data[i].comission+shift)*volume;
             break;
          case SYMBOL_CALC_MODE_CFDLEVERAGE :
             ans=(comission_data[i].comission+shift)*volume;
             break;
          case SYMBOL_CALC_MODE_EXCH_STOCKS :
            {
             double trading_volume=price*volume*SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE);
             ans=trading_volume*comission_data[i].comission/100+shift*volume;
            }
          break;
          case SYMBOL_CALC_MODE_EXCH_FUTURES :
             ans=(comission_data[i].comission+shift)*volume;
             break;
          case SYMBOL_CALC_MODE_EXCH_FUTURES_FORTS :
             ans=(comission_data[i].comission+shift)*volume;
             break;
          case SYMBOL_CALC_MODE_EXCH_BONDS :
            {
             double trading_volume=price*volume*SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE);
             ans=trading_volume*comission_data[i].comission/100+shift*volume;
            }
          break;
          case SYMBOL_CALC_MODE_EXCH_STOCKS_MOEX :
            {
             double trading_volume=price*volume*SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE);
             ans=trading_volume*comission_data[i].comission/100+shift*volume;
            }
          break;
          case SYMBOL_CALC_MODE_EXCH_BONDS_MOEX :
            {
             double trading_volume=price*volume*SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE);
             ans=trading_volume*comission_data[i].comission/100+shift*volume;
            }
          break;
          case SYMBOL_CALC_MODE_SERV_COLLATERAL :
             ans=(comission_data[i].comission+shift)*volume;
             break;
          default: ans=0; break;
         }

       if(ans!=0)
          return -ans;

      }
   }

 return 0;
}

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

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

Нововведение в класс CDealHistoryGetter

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

Первым из обещанных классов стоит рассмотреть класс CDealHistoryGetter, который верой и правдой с некоторыми модификациями служит мне с момента написания первой статьи. Эта статья по большей мере и описывала данный класс. В приложенных файлах находится самая свежая версия данного класса с выявленными некоторыми недочетами, а также добавлениями в виде описанного выше. За более подробным рассмотрением механизма работы выгрузки отчета торгов в удобочитаемом виде читайте указанную статью. В этой же статье рассмотрим менее подробно его функционал и то, каким образом добавляется пользовательская комиссия и проскальзывание к выгружаемому отчету. Согласно принципу ООП, где один объект должен выполнять одну конкретно поставленную перед ним цель, данный объект создан для получения всех видов результатов отчета торгов, что могут нас заинтересовать, и содержит следующие публичные методы, где каждый из них выполняет свою роль:

  • getHistory — данный метод помогает выгружать историю торгов сгруппированную по позициям. Если выгрузить в цикле историю торгов без какой-либо фильтрации стандартными методами, то мы получим описание сделок, представленных структурой DealData: 

struct DealData
  {
   long              ticket;        // Тикет сделки
   long              order;         // Номер ордера, открывшего сделку
   datetime          DT;            // Дата открытия позиции
   long              DT_msc;        // Дата открытия позиции в миллисекундах
   ENUM_DEAL_TYPE    type;          // Тип открытой позиции
   ENUM_DEAL_ENTRY   entry;         // Тип входа в позицию
   long              magic;         // Уникальный номер позиции
   ENUM_DEAL_REASON  reason;        // Откуда был выставлен ордер
   long              ID;            // ID позиции
   double            volume;        // Объем позиции (лоты)
   double            price;         // Цена входа в позицию
   double            comission;     // Комиссия уплаченная
   double            swap;          // Своп
   double            profit;        // Прибыль / убыток
   string            symbol;        // Символ
   string            comment;       // Комментарий, указанный во время открытия
   string            ID_external;   // Внешний ID
  };

Причем полученные данные будут сортированы по времени открытия позиций, но никак не сгруппированы. В указанной статье на примерах показывается, как тяжело читать подобную выгрузку, ведь при ведении торгов несколькими алгоритмами одновременно, всегда возникает ситуация путаницы сделок между собой. Особенно, если используются системы донабора позиций, которые докупают / допродают актив в соответствии с собственными алгоритмами.  Как итог мы получаем ряд сделок на вход и на выход, которые представлены в несгруппированном виде, только лишь мешаются друг другу и не предоставляют полной картины.

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

struct DealKeeper
  {
   DealData          deals[]; /* Список всех сделок для данной позиции
                              (или же нескольких позиций в случае если имел место быть переворот сделки)*/
   string            symbol;  // Символ
   long              ID;      // ID данной позиции (позиций)
   datetime          DT_min;  // дата открытия (или же дата самой первой позиции)
   datetime          DT_max;  // дата закрытия
  };

Стоит сразу сказать, что данный класс не учитывает в своих группировках Magic номера, так как фактически, как бы ни использовались два или же более алгоритма в торгах на одном символе, они не смогут полноценно вести позиции. Как минимум на "Московской бирже", для коей я в основной массе пишу алгоритмы — это технически невозможно. К тому же к доводам принятого решения стоит отнести тот факт, что рассматриваемый объект предполагается использовать либо для выгрузки отчетов торгов, либо для выгрузки результатов тестов/оптимизации. Для первой задачи достаточно иметь статистику по выбранному символу, которая предоставляется в полной мере, для двух других же Magic номер несущественен, так как по логике данного функционала один алгоритм должен иметь лишь один Magic номер, а тестер за раз прогоняет лишь один алгоритм.

Реализация данного метода осталась неизменной с момента его первого написания, за тем небольшим дополнением, что был добавлен метод внедрения пользовательской комиссии. Для поставленной задачи в конструктор класса передается по ссылке рассмотренный выше класс CCCM и сохраняется в соответствующем поле. Затем в момент заполнения структуры DealData, а именно — в момент заполнения комиссии — происходит добавление пользовательской комиссии, хранящейся в переданном класса CCCM. 

#ifndef ONLY_CUSTOM_COMISSION
               if(data.comission==0 && comission_manager != NULL)
                 {
                  data.comission=comission_manager.get(data.symbol,data.price,data.volume);
                 }
#else
               data.comission=comission_manager.get(data.symbol,data.price,data.volume);
#endif

Причем добавление комиссии происходит как директивно,  так и условно. Если мы в роботе перед подключением условленного файла с данным классом определим  ONLY_CUSTOM_COMISSION параметр, то тогда поле комиссии будет всегда содержать переданную нами комиссию, а не ту, что предоставляет брокер. Если же данный параметр не будет определен, то переданная нами комиссия будет добавляться условно, а именно — лишь в тот момент, когда брокер не поставляет ее с котировками, в иных случаях пользовательская комиссия будет игнорироваться.

  • getIDArr — возвращает массив ID позиций, которые были открыты по всем символам за запрашиваемый временной интервал. Именно с помощью ID позиций становится возможным объединить все сделки в позиции в прошлом рассмотренном методе. По сути это уникальный список поля DealData.ID. 
  • getDealsDetales — метод по своей сути схожий с методом getHistory, однако уступающий ему в детализации. Суть данного метода — предоставление таблицы позиций в удобочитаемом виде, где каждая строка соответствует одной конкретной сделке. Каждая из позиций  описывается следующей структурой: 
    struct DealDetales
      {
       string            symbol;        // символ
       datetime          DT_open;       // Дата открытия
       ENUM_DAY_OF_WEEK  day_open;      // День открытия
       datetime          DT_close;      // Дата закрытия
       ENUM_DAY_OF_WEEK  day_close;     // День закрытия
       double            volume;        // Объем (лоты)
       bool              isLong;        // признак Лонг / шорт
       double            price_in;      // Цена входа в позицию
       double            price_out;     // Цена выхода из позиции
       double            pl_oneLot;     // прибыль / убыток если бы торговали одним лотом
       double            pl_forDeal;    // прибыль / убыток который реально был с учетом комиссии
       string            open_comment;  // Комментарий на момент открытия
       string            close_comment; // Комментарий на момент закрытия
      };
    В своей массе они представляют сортированную по датам закрытия позиций таблицу позиций. Именно массив из данных значений мы будем использовать для подсчета коэффициентов в следующем рассмотренном классе и, соответственно, на основании представленных данных мы будем получать итоговый отчет о произведенном тесте. Также, именно на основании подобных данных тестер по окончанию торгов строит синюю линию графика PL.

    Кстати раз мы затронули тестер, то стоит сказать о том факте, что в последующих расчетах фактор восстановления, рассчитанный в терминале, и значение, рассчитанное по полученной выгрузке, будут отличаться. Это происходит из-за того, что хоть выгрузка данных и верна, и формулы, по которым считается описанный коэффициент в терминале и в последующем классе идентичны, но тем не менее исходные данные разнятся. Тестер считает фактор восстановления по зеленой линии, т.е. по детализированной выгрузке, а мы будем вести подсчеты по синей, т.е. по данным, не учитывающим колебания цен в интервал времени с момента открытия позиции до ее закрытия.   
  • getBalance — данный метод создан для получения данных о балансе без учета торговых операций на указанную дату. 
    double CDealHistoryGetter::getBalance(datetime toDate)
      {
       if(HistorySelect(0,(toDate>0 ? toDate : TimeCurrent())))
         {
          int total=HistoryDealsTotal(); // Получаем общее количество позиций
          double balance=0;
          for(int i=0; i<total; i++)
            {
             long ticket=(long)HistoryDealGetTicket(i);
    
             ENUM_DEAL_TYPE dealType=(ENUM_DEAL_TYPE)HistoryDealGetInteger(ticket,DEAL_TYPE);
             if(dealType==DEAL_TYPE_BALANCE ||
                dealType == DEAL_TYPE_CORRECTION ||
                dealType == DEAL_TYPE_COMMISSION)
               {
                balance+=HistoryDealGetDouble(ticket,DEAL_PROFIT);
    
                if(toDate<=0)
                   break;
               }
            }
          return balance;
         }
       else
          return 0;
      }

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

  • getBalanceWithPL — данный метод аналогичен предыдущему, однако он в добавок к изменениям баланса, так же учитывает прибыль / убыток от совершенных операций к которым так же добавляется комиссия по описанному выше принципу.

Класс, создающий отчет оптимизации — структуры, участвующие в расчетах

Следующим из описываемых объектов, но уже упомянутых ранее в прошлых статьях, является класс CReportCreator — данный класс был кратко освящен в статье 100 лучших проходов оптимизаций в разделе "Расчетная часть", однако за избытком информации, которая была умещена в одной статье, не был описан подробно. Данное упущение следует устранить, так как именно он ведет расчет всех коэффициентов, на основе которых автооптимизатор будет принимать решение о том, соответствует ли данное сочетание параметров алгоритма запрашиваемым критериям или же нет. 

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

Порой при больших данных подобный подход может занять несколько секунд, что недопустимо долго. Именно для ускорения производимых расчетов был использован тот способ реализации данного класса, который будет рассмотрен далее, а также новый класс содержит куда больше данных, многие из которых не предоставляются даже в стандартной выгрузке отчетов оптимизаций. Если разобраться, то для подсчета многих коэффициентов требуются однообразные данные, такие как, к примеру, максимальная прибыль/убыток или же накопленная прибыль/убыток и им подобные.

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

Теперь, описав основную идею, которая была использована при его создании, рассмотрим, как именно происходят расчеты параметров. Начнем с рассмотрения объектов, хранящих данные, используемые для последующих расчетов искомых значений. Данные объекты создаются как вложенные объекты класса, объявленные в private области видимости. Это делается из двух побуждений — для невозможности их использования в других классах, которые будут пользоваться данным функционалом, ведь легко запутаться в том разнообразии объявленных структур и классов, какие из них требуются для внешних расчетов, а какие явяляются чисто техническими, или внутренними. Именно это и является вторым побуждением — подчеркнуть их чисто техническое предназначение. 

Структура PL_Keeper:

struct PL_keeper
{
 PLChart_item      PL_total[];
 PLChart_item      PL_oneLot[];
 PLChart_item      PL_Indicative[];
};

Данная структура создана для хранения всех возможных графиков прибыли и убытка, они были довольно подробно описаны в моей первой статье, ссылка на которую уже приводилась. Ниже под объявлением данной структуры создаются её экземпляры:

PL_keeper         PL,PL_hist,BH,BH_hist;

Каждый из экземпляров хранит в себе 4 представленные вида графиков, но только для разных исходных данных. Данные с префиксом PL рассчитываются по той самой, уже упомянутой, синей линии графика PL из терминала, а данные с префиксом BH рассчитываются по данным графика прибыли и убытка, полученного от стратегии Buy and Hold. Данные с постфиксом hist  рассчитываются по гистограмме прибыли и убытка.

Структура DailyPL_keeper:

// Структура типов графика дневной Прибыли/Убытка
struct DailyPL_keeper
{
 DailyPL           avarage_open,avarage_close,absolute_open,absolute_close;
};

Данная структура хранит в себе все 4 возможных типа графиков дневной прибыли/убытка, иначе говоря, той самой гистограммы в отчете торгов, где по дням расписаны прибыли/убытки от торгов. Экземпляры структуры DailyPL, помеченные префиксом average, рассчитываются по усредненным данным прибыли/убытка, те же, что помечены префиксом absolute, рассчитываются по абсолютным, суммарным значениям прибыли и убытка. Соответственно, различия между ними очевидны — в первом случае отображается средняя прибыль за день за все время торгов, а во втором — суммарная. Данные с префиксом open сортированы по дням, исходя из дат открытия, а данные с постфиксом сlose исходя из дат закрытия. Экземпляр данной структуры так же, как и экземпляры других описываемых структур, объявлен ниже в коде, но его объявление тривиально.

Структура RationTable_keeper:

// Структура таблиц крайних точек
struct RatioTable_keeper
  {
   ProfitDrawdown    Total_max,Total_absolute,Total_percent;
   ProfitDrawdown    OneLot_max,OneLot_absolute,OneLot_percent;
  };

Данная структура состоит из экземпляров структуры ProfitDrawdown

struct ProfitDrawdown
  {
   double            Profit; // В некоторых случаях - Прибыль, иногда - прибыль / убыток
   double            Drawdown; // Просадка
  };

И хранит в себе соотношение прибыли и убытка по определенным критериям. Данные с префиксом Total рассчитываются по графику прибыли/убытка, построенному с учетом перемены лотности во время торговли от позиции к позиции. Данные с префиксом OneLot рассчитываются как-будто всю торговлю все время мы вели одним контрактом. Подробнее про идею данного нестандартного учета лотности можно почитать в вышеупомянутой первой статье. Вкратце же могу сказать, что это было создано для оценки результатов деятельности торговой системы, чтобы можно было оценить, откуда производится больше результата — от своевременного управления лотностью или же от самой логики системы. Постфикс max свидетельствует о том, что в данном экземпляре занесены данные о максимальном значении прибыли и просадки за историю торгов. Постфикс absolute свидетельствует о суммарных данных прибыли за историю торгов и просадки за историю торгов. Постфикс percent свидетельствует о том, что данные, занесенные по прибыли и просадке, рассчитаны как процентное отношение к максимальному значению на кривой PL, которое было достигнуто за исследуемый интервал. Объявление данной структуры также тривиально и посему не прилагается. 

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

// Структуры для подсчета кол-ва прибылей и убытка подряд
   struct S_dealsCounter
     {
      int               Profit,DD;
     };
   struct S_dealsInARow : public S_dealsCounter
     {
      S_dealsCounter    Counter;
     };
   // Структуры для расчета вспомогательных данных
   struct CalculationData_item
     {
      S_dealsInARow     dealsCounter;
      int               R_arr[];
      double            DD_percent;
      double            Accomulated_DD,Accomulated_Profit;
      double            PL;
      double            Max_DD_forDeal,Max_Profit_forDeal;
      double            Max_DD_byPL,Max_Profit_byPL;
      datetime          DT_Max_DD_byPL,DT_Max_Profit_byPL;
      datetime          DT_Max_DD_forDeal,DT_Max_Profit_forDeal;
      int               Total_DD_numDeals,Total_Profit_numDeals;
     };
   struct CalculationData
     {
      CalculationData_item total,oneLot;
      int               num_deals;
      bool              isNot_firstDeal;
     };


Структуры S_dealsCounter и S_dealsInARow по сути являются единым целым. Столь странное сочетание ассоциации и одновременно наследования в данных структурах связано с особенностью исчисления ее параметров. Для начала следует сказать, что структура S_dealsInARow создана для хранения и подсчета количества сделок (на самом деле позиций, т.е. имеется в виду круг от открытия до закрытия позиции), произошедших подряд — как положительных так и отрицательных. Вложенный экземпляр структуры S_dealsCounter объявлен для сохранения промежуточных результатов вычислений, а унаследованные поля сохраняют итоговые значения. Мы еще вернемся к данной операции исчисления прибыльных / убыточных сделок в нашем повествовании, но позже.     

Структура CalculationData_item содержит поля, необходимые для подсчетов требуемых коэффициентов. 

  • R_arr — массив последовательностей прибыльных / убыточных серий сделок, отображаемых 1 / 0 соответственно. Данный массив используется для подсчета Z-счета;
  • DD_percent — процентное значение просадки;
  • Accomulated_DD, Accomulated_Profit  — хранят суммарное значение убытков и прибылей;
  • PL — прибыль / убыток;
  • Max_DD_forDeal, Max_Profit_forDeal — согласно их наименованиям, хранят максимальную просадку и прибыль среди сделок;
  • Max_DD_byPL, Mаx_Profit_byPL — согласно названию, хранят максимальную просадку и прибыль, подсчитанную по графику PL;
  • DT_Max_DD_byPL, DT_Max_Profit_byPL — хранят даты максимальных просадок по PL;
  • DT_Max_DD_forDeal, DT_Max_Profit_forDeal — соответственно, даты просадок и прибылей для соответствующих сделок;
  • Total_DD_numDeals, TotalProfit_numDeals — суммарное количество прибыльных и убыточных сделок. 

Исходя из этих данных ведутся дальнейшие расчеты.

Структура CalculationData является аккумулирующей структурой, где сочетаются все описанные структуры, и именно она хранит в себе все требуемые данные. В ней также содержится поле num_deals, которое по сути является суммой полей CalculationData_item::Total_DD_numDeals и CalculationData_item::TotalProfit_numDeals, а поле sNot_firstDeal является техническим флагом, сигнализирующем, что подсчет на текущей итерации цикла ведется не для самой первой из сделок.

Структура CoefChart_keeper:

struct CoefChart_keeper
     {
      CoefChart_item    OneLot_ShartRatio_chart[],Total_ShartRatio_chart[];
      CoefChart_item    OneLot_WinCoef_chart[],Total_WinCoef_chart[];
      CoefChart_item    OneLot_RecoveryFactor_chart[],Total_RecoveryFactor_chart[];
      CoefChart_item    OneLot_ProfitFactor_chart[],Total_ProfitFactor_chart[];
      CoefChart_item    OneLot_AltmanZScore_chart[],Total_AltmanZScore_chart[];
     };

Создана для хранения графиков коэффициентов. Так как данный класс строит не только графики прибыли и убытка, но и графики некоторых из коэффициентов, то по аналогу со структурой, хранящей графики прибыли и убытка, была создана данная структур для описываемых типов данных. Префикс OneLot указывает на то, что в данном экземпляре объекта будут храниться данные, полученные путем анализа графика прибыли/убытка от торгов, если торговать одним лотом без учета управления лотностью. Префикс Total указывает на то, что оценивался график торгов с тем учетом управления лотностью, который был использован. Если же не использовалась никакая система управления лотностью, то оба графика будут идентичны.

Класс СHistoryComparer:

Так же определяется класс, который участвует в сортировке данных. Как можно прочесть в статье "100 лучших проходов оптимизации", мною был создан класс CGenericSorter, умеющий сортировать по убыванию и возрастанию данные любого типа, однако для его работы требуется описать класс, который смог бы сравнивать передаваемые типы. Именно подобным классом является класс СHisoryComparer.

class CHistoryComparer : public ICustomComparer<DealDetales>
     {
   public:
      int               Compare(DealDetales &x,DealDetales &y);
     };

Реализация его метода довольно прозаична, он сравнивает даты закрытия, так как сортировка производится именно по ним:

int CReportCreator::CHistoryComparer::Compare(DealDetales &x,DealDetales &y)
  {
   return(x.DT_close == y.DT_close ? 0 : (x.DT_close > y.DT_close ? 1 : -1));
  }

Также существует класс, сортирующий графики коэффициентов, он имеет аналогичную структуру. Оба эти класса и класс сортировщика инстанцированы в качестве глобального поля, описываемого класса CReportCreator. Также помимо описанных объектов имеются еще два поля, типы которых описаны уже как отдельные, а невложенные объекты:

PL_detales        PL_detales_data;
DistributionChart OneLot_PDF_chart,Total_PDF_chart;

Структура PL_detales содержит краткие сведения о торгах для убыточных и прибыльных позиций:

//+------------------------------------------------------------------+
struct PL_detales_PLDD
  {
   int               orders; // кол-во сделок
   double            orders_in_Percent; // кол-во ордеров в % к общему кол-ву ордеров
   int               dealsInARow; // Сделок подряд
   double            totalResult; // Суммарный результат в деньгах
   double            averageResult; // Средний результат в деньгах
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
struct PL_detales_item
  {
   PL_detales_PLDD   profit; // Информация по прибыльным сделкам
   PL_detales_PLDD   drawdown; // Информация по убыточным сделкам
  };
//+-------------------------------------------------------------------+
//| Краткая сводка по графику PL разбитая на 2 основопологающих блока |
//+-------------------------------------------------------------------+
struct PL_detales
  {
   PL_detales_item   total,oneLot;
  };

А вторая структура, DistributionChart, содержит в себе ряд показателей VaR, а также график распределения, по которому эти коэффициенты рассчитывались. Распределение считается как нормальное распределение.

//+------------------------------------------------------------------+
//| Структура - используется для сохранения графиков распределений   |
//+------------------------------------------------------------------+
struct Chart_item
  {
   double            y; // ось y
   double            x; // ось x
  };
//+------------------------------------------------------------------+
//| Структура - содержит в себе значение VaR                         |
//+------------------------------------------------------------------+
struct VAR
  {
   double            VAR_90,VAR_95,VAR_99;
   double            Mx,Std;
  };
//+------------------------------------------------------------------+
//| Структура - Используется для хранения графиков распределения,    |
//| а так же значений VaR                                            |
//+------------------------------------------------------------------+
struct Distribution_item
  {
   Chart_item        distribution[]; // График распределения
   VAR               VaR; // VaR
  };
//+------------------------------------------------------------------+
//| Структура - Хранит данные о распределении. Разбита на 2 блока    |
//+------------------------------------------------------------------+
struct DistributionChart
  {
   Distribution_item absolute,growth;
  };

Сами коэффициенты VaR рассчитываются по формуле самого простого — Исторического VaR, что, возможно, недостаточно точно, однако для текущей реализации, думаю, вполне сгодится. 

Методы для расчета коэффициентов, описывающих результаты торгов

Теперь, разобравшись со структурами, хранящими данные, уже можно представить тот объем статистики, который рассчитывает данный класс. Рассмотрим конкретные методы, ведущие расчет описанных показателей по очереди — как они озаглавлены в классе CReportCreator.

Метод CalcPL создан для подсчета графика PL. Его реализация следующая:

void CReportCreator::CalcPL(const DealDetales &deal,CalculationData &data,PLChart_item &pl_out[],CalcType type)
  {
   PLChart_item item;
   ZeroMemory(item);
   item.DT=deal.DT_close; // Сохранение даты

   if(type!=_Indicative)
     {
      item.Profit=(type==_Total ? data.total.PL : data.oneLot.PL); // Сохранение прибыли
      item.Drawdown=(type==_Total ? data.total.DD_percent : data.oneLot.DD_percent); // Сохранение просадки
     }
   else // Расчет индикативного графика
     {
      if(data.isNot_firstDeal)
        {
         if(data.total.PL!=0)
           {
            if(data.total.PL > 0 && data.total.Max_DD_forDeal < 0)
               item.Profit=data.total.PL/MathAbs(data.total.Max_DD_forDeal);
            else
               if(data.total.PL<0 && data.total.Max_Profit_forDeal>0)
                  item.Profit=data.total.PL/data.total.Max_Profit_forDeal;
           }
        }
     }
// Добавление данных в массив
   int s=ArraySize(pl_out);
   ArrayResize(pl_out,s+1,s+1);
   pl_out[s]=item;
  }

Как видно из его реализации, все его расчеты основаны на данных из уже описанных структур, которые передаются в качестве входного параметра.

Если требуется подсчитать график PL неиндикативного типа, то мы просто копируем уже известные данные. В противном случае подсчет производится лишь при соблюдении двух условий, а именно — в цикле была пройдена не первая итерация и график PL не равен нулю. Сам подсчет производится по следующей логике:

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

Следующий метод CalcPLHist основан на подобном механизме, но использует в расчетах другие поля структуры, а именно  — data.oneLot.Accomulated_DD, data.total.Accomulated_DD и data.oneLot.Accomulated_Profit, data.total.Accomulated_Profit. Так как алгоритм его действий мы уже рассмотрели, то не будем задерживаться на данном методе и перейдем к более важным двум методам.

Методы CalcData и CalcData_item:

Именно в данных методах происходит расчет всех вспомогательных и основных коэффициентов. Начнем наше рассмотрение с метода CalcData_item, задача которого — произвести расчет вспомогательных коэффициентов, кои были описаны выше и на основании которых вычисляются основные.  

//+------------------------------------------------------------------+
//| Подсчет вспомогательных данных                                   |
//+------------------------------------------------------------------+
void CReportCreator::CalcData_item(const DealDetales &deal,CalculationData_item &out,
                                   bool isOneLot)
  {
   double pl=(isOneLot ? deal.pl_oneLot : deal.pl_forDeal); //PL
   int n=0;
// Кол-прибылей и убытков
   if(pl>=0)
     {
      out.Total_Profit_numDeals++;
      n=1;
      out.dealsCounter.Counter.DD=0;
      out.dealsCounter.Counter.Profit++;
     }
   else
     {
      out.Total_DD_numDeals++;
      out.dealsCounter.Counter.DD++;
      out.dealsCounter.Counter.Profit=0;
     }
   out.dealsCounter.DD=MathMax(out.dealsCounter.DD,out.dealsCounter.Counter.DD);
   out.dealsCounter.Profit=MathMax(out.dealsCounter.Profit,out.dealsCounter.Counter.Profit);

// Серии из прибылей и убытков
   int s=ArraySize(out.R_arr);
   if(!(s>0 && out.R_arr[s-1]==n))
     {
      ArrayResize(out.R_arr,s+1,s+1);
      out.R_arr[s]=n;
     }

   out.PL+=pl; //PL общий
// Макс Profit / DD
   if(out.Max_DD_forDeal>pl)
     {
      out.Max_DD_forDeal=pl;
      out.DT_Max_DD_forDeal=deal.DT_close;
     }
   if(out.Max_Profit_forDeal<pl)
     {
      out.Max_Profit_forDeal=pl;
      out.DT_Max_Profit_forDeal=deal.DT_close;
     }
// Накопленная Profit / DD
   out.Accomulated_DD+=(pl>0 ? 0 : pl);
   out.Accomulated_Profit+=(pl>0 ? pl : 0);
// Крайние точки по прибыли
   double maxPL=MathMax(out.Max_Profit_byPL,out.PL);
   if(compareDouble(maxPL,out.Max_Profit_byPL)==1/* || !isNot_firstDeal*/)// для сохранения даты нужна еще одна проверка
     {
      out.DT_Max_Profit_byPL=deal.DT_close;
      out.Max_Profit_byPL=maxPL;
     }
   double maxDD=out.Max_DD_byPL;
   double DD=0;
   if(out.PL>0)
      DD=out.PL-maxPL;
   else
      DD=-(MathAbs(out.PL)+maxPL);
   maxDD=MathMin(maxDD,DD);
   if(compareDouble(maxDD,out.Max_DD_byPL)==-1/* || !isNot_firstDeal*/)// для сохранения даты нужна еще одна проверка
     {
      out.Max_DD_byPL=maxDD;
      out.DT_Max_DD_byPL=deal.DT_close;
     }
   out.DD_percent=(balance>0 ?(MathAbs(DD)/(maxPL>0 ? maxPL : balance)) :(maxPL>0 ?(MathAbs(DD)/maxPL) : 0));
  }

Первым делом производится расчет PL на i-той итерации. Далее, если была прибыль на данной итерации, мы производим увеличение счетчиков прибыльных сделок, а также зануляем счетчик убыточных сделок подряд. В добавок мы присваиваем переменной n значение 1, показывающее, что текущая сделка была прибыльной. Если же PL был ниже нуля, мы производим увеличение счетчиков убытка и зануляем счетчик прибыльных сделок подряд. После этого мы присваиваем максимальное значение количества прибыльных и убыточных серий подряд.

Следующим пунктом происходит подсчет серий прибыльных и убыточных сделок. Под серией понимаем подряд идущие положительные или же отрицательные сделки. В данном массиве всегда за нулем будет идти единица, а за единицей только нуль — данная смена показывает смену положительных  и отрицательных сделок, тогда как на том месте, где стоит, к примеру единица, по факту может произойти не одна, а сколько угодно положительных сделок, аналогичная картина с нулем. Данный массив будет использоваться при подсчете Z-счета, показывающего то, насколько случайно происходит торговля. Следующим этапом происходит присвоение значений максимальной прибыли / просадки и подсчет накопленной прибыли / убытков. В заключении данного метода происходит подсчет крайних точек, т.е. заполняются структуры со значениями максимальных прибылей и убытков.

Метод CalcData уже использует полученные промежуточные данные в расчете требуемых коэффициентов и обновляет расчеты на каждой итерации. Его реализация следующая:

void CReportCreator::CalcData(const DealDetales &deal,CalculationData &out,bool isBH)
  {
   out.num_deals++; // Подсчет общего кол-ва сделок
   CalcData_item(deal,out.oneLot,true);
   CalcData_item(deal,out.total,false);

   if(!isBH)
     {
      // Заполняем графики PL
      CalcPL(deal,out,PL.PL_total,_Total);
      CalcPL(deal,out,PL.PL_oneLot,_OneLot);
      CalcPL(deal,out,PL.PL_Indicative,_Indicative);

      // Заполняем графики Гистограмм PL
      CalcPLHist(deal,out,PL_hist.PL_total,_Total);
      CalcPLHist(deal,out,PL_hist.PL_oneLot,_OneLot);
      CalcPLHist(deal,out,PL_hist.PL_Indicative,_Indicative);

      // Заполняем графики PL по дням
      CalcDailyPL(DailyPL_data.absolute_close,CALC_FOR_CLOSE,deal);
      CalcDailyPL(DailyPL_data.absolute_open,CALC_FOR_OPEN,deal);
      CalcDailyPL(DailyPL_data.avarage_close,CALC_FOR_CLOSE,deal);
      CalcDailyPL(DailyPL_data.avarage_open,CALC_FOR_OPEN,deal);

      // Заполняем графики Профит фактора
      ProfitFactor_chart_calc(CoefChart_data.OneLot_ProfitFactor_chart,out,deal,true);
      ProfitFactor_chart_calc(CoefChart_data.Total_ProfitFactor_chart,out,deal,false);

      // Заполняем графики Фактора востановления
      RecoveryFactor_chart_calc(CoefChart_data.OneLot_RecoveryFactor_chart,out,deal,true);
      RecoveryFactor_chart_calc(CoefChart_data.Total_RecoveryFactor_chart,out,deal,false);

      // Заполняем графики коэфицента выигрыша
      WinCoef_chart_calc(CoefChart_data.OneLot_WinCoef_chart,out,deal,true);
      WinCoef_chart_calc(CoefChart_data.Total_WinCoef_chart,out,deal,false);

      // Заполняем графики Коэфицента шарпа
      ShartRatio_chart_calc(CoefChart_data.OneLot_ShartRatio_chart,PL.PL_oneLot,deal/*,out.isNot_firstDeal*/);
      ShartRatio_chart_calc(CoefChart_data.Total_ShartRatio_chart,PL.PL_total,deal/*,out.isNot_firstDeal*/);

      // Заполняем графики Z-счета
      AltmanZScore_chart_calc(CoefChart_data.OneLot_AltmanZScore_chart,(double)out.num_deals,
                              (double)ArraySize(out.oneLot.R_arr),(double)out.oneLot.Total_Profit_numDeals,
                              (double)out.oneLot.Total_DD_numDeals/*,out.isNot_firstDeal*/,deal);
      AltmanZScore_chart_calc(CoefChart_data.Total_AltmanZScore_chart,(double)out.num_deals,
                              (double)ArraySize(out.total.R_arr),(double)out.total.Total_Profit_numDeals,
                              (double)out.total.Total_DD_numDeals/*,out.isNot_firstDeal*/,deal);
     }
   else // Заполняем графики PL Buy and Hold
     {
      CalcPL(deal,out,BH.PL_total,_Total);
      CalcPL(deal,out,BH.PL_oneLot,_OneLot);
      CalcPL(deal,out,BH.PL_Indicative,_Indicative);

      CalcPLHist(deal,out,BH_hist.PL_total,_Total);
      CalcPLHist(deal,out,BH_hist.PL_oneLot,_OneLot);
      CalcPLHist(deal,out,BH_hist.PL_Indicative,_Indicative);
     }

   if(!out.isNot_firstDeal)
      out.isNot_firstDeal=true; // Флаг "Это НЕ первая сделка"
  }

Первым делом производится подсчет промежуточных коэффициентов для торговли одним лотом и для торгов с системами управления лотностью путем вызова уже описанного метода для обоих типов данных. Затем производится разбиение на расчет коэффициентов для BH и для противоположного типа данных. Внутри каждого из блоков производятся расчеты интерпретируемых коэффициентов. Для стратегии Buy and Hold ведется расчет только лишь графиков, поэтому мы не вызываем методы, рассчитывающие коэффициенты.  

Следующая группа методов подсчитывает прибыль / убыток с разбивкой по дням:

//+------------------------------------------------------------------+
//| Создание структуры торговли в течении дня                        |
//+------------------------------------------------------------------+
void CReportCreator::CalcDailyPL(DailyPL &out,DailyPL_calcBy calcBy,const DealDetales &deal)
  {
   cmpDay(deal,MONDAY,out.Mn,calcBy);
   cmpDay(deal,TUESDAY,out.Tu,calcBy);
   cmpDay(deal,WEDNESDAY,out.We,calcBy);
   cmpDay(deal,THURSDAY,out.Th,calcBy);
   cmpDay(deal,FRIDAY,out.Fr,calcBy);
  }
//+------------------------------------------------------------------+
//| Сохранение PL/DD, наторгованных за день                          |
//+------------------------------------------------------------------+
void CReportCreator::cmpDay(const DealDetales &deal,ENUM_DAY_OF_WEEK etalone,PLDrawdown &ans,DailyPL_calcBy calcBy)
  {
   ENUM_DAY_OF_WEEK day=(calcBy==CALC_FOR_CLOSE ? deal.day_close : deal.day_open);
   if(day==etalone)
     {
      if(deal.pl_forDeal>0)
        {
         ans.Profit+=deal.pl_forDeal;
         ans.numTrades_profit++;
        }
      else
         if(deal.pl_forDeal<0)
           {
            ans.Drawdown+=MathAbs(deal.pl_forDeal);
            ans.numTrades_drawdown++;
           }
     }
  }
//+------------------------------------------------------------------+
//| Усреднение PL/DD, наторгованных за день                          |
//+------------------------------------------------------------------+
void CReportCreator::avarageDay(PLDrawdown &day)
  {
   if(day.numTrades_profit>0)
      day.Profit/=day.numTrades_profit;
   if(day.numTrades_drawdown > 0)
      day.Drawdown/=day.numTrades_drawdown;
  }

Как видно из представленной реализации, основная работа по разбивке прибыли / убытков по дням происходит в методе cmpDay, который сперва проверяет — соответствует ли день запрашиваемому или же нет — затем просто добавляет значения прибыли и убытков, однако убытки суммируются по модулю. Метод CalcDailyPL является агрегирующим, где происходит попытка добавить текущую переданную PL в один их пяти рабочих дней. Завершающий метод avarageDay вызывается для усредненных прибылей / убытков в основном методе Create. Данный метод не делает ничего особенного, он всего лишь переводит абсолютные значения прибыли / убытков, рассчитанные ранее в средние. 

Метод, рассчитывающий Профит фактор

//+------------------------------------------------------------------+
//| Подсчет Профит фактора                                           |
//+------------------------------------------------------------------+
void CReportCreator::ProfitFactor_chart_calc(CoefChart_item &out[],CalculationData &data,const DealDetales &deal,bool isOneLot)
  {
   CoefChart_item item;
   item.DT=deal.DT_close;
   double profit=(isOneLot ? data.oneLot.Accomulated_Profit : data.total.Accomulated_Profit);
   double dd=MathAbs(isOneLot ? data.oneLot.Accomulated_DD : data.total.Accomulated_DD);
   if(dd==0)
      item.coef=0;
   else
      item.coef=profit/dd;
   int s=ArraySize(out);
   ArrayResize(out,s+1,s+1);
   out[s]=item;
  }

По сути, данный метод рассчитывает график того, как менялся профит фактор на протяжении торгов, а самый последний как раз и будет тем самым коэффициентом, что отображается в отчете тестирования. Формула проста = накопленная прибыль / накопленную просадку. Если просадка нулевая, то коэффициент будет равняться нулю, так как в классической арифметике без использования пределов нельзя делить на нуль, и это же правило принято в используемом языке, то и мы на протяжении всех арифметических операций будем делать соответствующие проверки делителя.

Фактор восстановления также рассчитывается аналогичным образом:

//+------------------------------------------------------------------+
//| Подсчет Фактора востановления                                    |
//+------------------------------------------------------------------+
void CReportCreator::RecoveryFactor_chart_calc(CoefChart_item &out[],CalculationData &data,const DealDetales &deal,bool isOneLot)
  {
   CoefChart_item item;
   item.DT=deal.DT_close;
   double pl=(isOneLot ? data.oneLot.PL : data.total.PL);
   double dd=MathAbs(isOneLot ? data.oneLot.Max_DD_byPL : data.total.Max_DD_byPL);
   if(dd==0)
      item.coef=0;//по-хорошему - это плюс бесконечность
   else
      item.coef=pl/dd;
   int s=ArraySize(out);
   ArrayResize(out,s+1,s+1);
   out[s]=item;
  }

Формула расчета данного коэффициента прибыль на i-тую итерацию / просадка на i-тую итерацию. Стоит заметить, так как прибыль на момент подсчета данного коэффициента может быть нулевой или же отрицательной, то и сам коэффициент может быть нулевым или же отрицательным.

Коэффициент выигрыша

//+------------------------------------------------------------------+
//| Подсчет Коэфицента выигрыша                                      |
//+------------------------------------------------------------------+
void CReportCreator::WinCoef_chart_calc(CoefChart_item &out[],CalculationData &data,const DealDetales &deal,bool isOneLot)
  {
   CoefChart_item item;
   item.DT=deal.DT_close;
   double profit=(isOneLot ? data.oneLot.Accomulated_Profit : data.total.Accomulated_Profit);
   double dd=MathAbs(isOneLot ? data.oneLot.Accomulated_DD : data.total.Accomulated_DD);
   int n_profit=(isOneLot ? data.oneLot.Total_Profit_numDeals : data.total.Total_Profit_numDeals);
   int n_dd=(isOneLot ? data.oneLot.Total_DD_numDeals : data.total.Total_DD_numDeals);
   if(n_dd == 0 || n_profit == 0)
      item.coef = 0;
   else
      item.coef=(profit/n_profit)/(dd/n_dd);
   int s=ArraySize(out);
   ArrayResize(out,s+1,s+1);
   out[s]=item;
  }

Формула расчета коэффициента выигрыша = (прибыль / кол-во прибыльных сделок) / (просадка / кол-во убыточных сделок).  Данный коэффициент также может быть отрицательным, если прибыль на момент подсчета коэффициента будет отсутствовать. 

Коэффициент Шарпа рассчитывается более мудрено чем предыдущие описанные:

//+------------------------------------------------------------------+
//| Подсчет Коэфицента Шарпа                                         |
//+------------------------------------------------------------------+
double CReportCreator::ShartRatio_calc(PLChart_item &data[])
  {
   int total=ArraySize(data);
   double ans=0;
   if(total>=2)
     {
      double pl_r=0;
      int n=0;
      for(int i=1; i<total; i++)
        {
         if(data[i-1].Profit!=0)
           {
            pl_r+=(data[i].Profit-data[i-1].Profit)/data[i-1].Profit;
            n++;
           }
        }
      if(n>=2)
         pl_r/=(double)n;
      double std=0;
      n=0;
      for(int i=1; i<total; i++)
        {
         if(data[i-1].Profit!=0)
           {
            std+=MathPow((data[i].Profit-data[i-1].Profit)/data[i-1].Profit-pl_r,2);
            n++;
           }
        }
      if(n>=2)
         std=MathSqrt(std/(double)(n-1));

      ans=(std!=0 ?(pl_r-r)/std : 0);
     }
   return ans;
  }

Сначала в первом цикле производится расчет усредненной доходности по графику PL, где каждая i-тая доходность рассчитывается как отношение прироста над предыдущим значением PL к предыдущему значению PL. За образец был взят способ нормализации ряда цен, используемый для оценки временных рядов. 

Затем в последующем цикле производится расчет волатильности, причем волатильность рассчитывается по тому же нормализованному ряду доходностей

В завершении производится расчет самого коэффициента по формуле (средняя прибыль - без рисковая ставка доходности) / волатильность (СКО  доходностей).

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

Расчет VaR и графика нормального распределения. Данная часть расчетов состоит из трех методов, как и всегда, два из них являются рассчитывающими, а один агрегирующим все вычисления. Рассмотрим их по порядку.

//+------------------------------------------------------------------+
//| Подсчет Распределения                                            |
//+------------------------------------------------------------------+
void CReportCreator::NormalPDF_chart_calc(DistributionChart &out,PLChart_item &data[])
  {
   double Mx_absolute=0,Mx_growth=0,Std_absolute=0,Std_growth=0;
   int total=ArraySize(data);
   ZeroMemory(out.absolute);
   ZeroMemory(out.growth);
   ZeroMemory(out.absolute.VaR);
   ZeroMemory(out.growth.VaR);
   ArrayFree(out.absolute.distribution);
   ArrayFree(out.growth.distribution);

// Расчет параметров распределения
   if(total>=2)
     {
      int n=0;
      for(int i=0; i<total; i++)
        {
         Mx_absolute+=data[i].Profit;
         if(i>0 && data[i-1].Profit!=0)
           {
            Mx_growth+=(data[i].Profit-data[i-1].Profit)/data[i-1].Profit;
            n++;
           }
        }
      Mx_absolute/=(double)total;
      if(n>=2)
         Mx_growth/=(double)n;

      n=0;
      for(int i=0; i<total; i++)
        {
         Std_absolute+=MathPow(data[i].Profit-Mx_absolute,2);
         if(i>0 && data[i-1].Profit!=0)
           {
            Std_growth+=MathPow((data[i].Profit-data[i-1].Profit)/data[i-1].Profit-Mx_growth,2);
            n++;
           }
        }
      Std_absolute=MathSqrt(Std_absolute/(double)(total-1));
      if(n>=2)
         Std_growth=MathSqrt(Std_growth/(double)(n-1));

      // Подсчет VaR
      out.absolute.VaR.Mx=Mx_absolute;
      out.absolute.VaR.Std=Std_absolute;
      out.absolute.VaR.VAR_90=VaR(Q_90,Mx_absolute,Std_absolute);
      out.absolute.VaR.VAR_95=VaR(Q_95,Mx_absolute,Std_absolute);
      out.absolute.VaR.VAR_99=VaR(Q_99,Mx_absolute,Std_absolute);
      out.growth.VaR.Mx=Mx_growth;
      out.growth.VaR.Std=Std_growth;
      out.growth.VaR.VAR_90=VaR(Q_90,Mx_growth,Std_growth);
      out.growth.VaR.VAR_95=VaR(Q_95,Mx_growth,Std_growth);
      out.growth.VaR.VAR_99=VaR(Q_99,Mx_growth,Std_growth);

      // Расчет распределения
      for(int i=0; i<total; i++)
        {
         Chart_item  item_a,item_g;
         ZeroMemory(item_a);
         ZeroMemory(item_g);
         item_a.x=data[i].Profit;
         item_a.y=PDF_calc(Mx_absolute,Std_absolute,data[i].Profit);
         if(i>0)
           {
            item_g.x=(data[i-1].Profit != 0 ?(data[i].Profit-data[i-1].Profit)/data[i-1].Profit : 0);
            item_g.y=PDF_calc(Mx_growth,Std_growth,item_g.x);
           }
         int s=ArraySize(out.absolute.distribution);
         ArrayResize(out.absolute.distribution,s+1,s+1);
         out.absolute.distribution[s]=item_a;
         s=ArraySize(out.growth.distribution);
         ArrayResize(out.growth.distribution,s+1,s+1);
         out.growth.distribution[s]=item_g;
        }
      // Acending
      sorter.Sort<Chart_item>(out.absolute.distribution,&chartComparer);
      sorter.Sort<Chart_item>(out.growth.distribution,&chartComparer);
     }
  }
//+------------------------------------------------------------------+
//| Подсчет VaR                                                      |
//+------------------------------------------------------------------+
double CReportCreator::VaR(double quantile,double Mx,double Std)
  {
   return Mx-quantile*Std;
  }
//+------------------------------------------------------------------+
//| Подсчет Распределения                                            |
//+------------------------------------------------------------------+
double CReportCreator::PDF_calc(double Mx,double Std,double x)
  {
   if(Std!=0)
      return MathExp(-0.5*MathPow((x-Mx)/Std,2))/(MathSqrt(2*M_PI)*Std);
   else
      return 0;
  }

Метод расчета VaR является самым простым — он использует в своих расчетах модель исторического VaR, как уже было упомянуто выше в данной статье.

Метод расчета нормализованного распределения, для пущей точности, был взят в полной мере из пакета статистического анализа Matlab

Метод расчета нормализованного распределения и построения его графика является агрегирующим, где применяются все вышеописанные методы. В первом цикле производится расчет среднего значения прибыли, а во втором цикле производится расчет стандартно-квадратического отклонения доходностей. Причем сами доходности для графика и VaR, рассчитанного по приростам, опять же рассчитываются как нормализованный временной ряд. Далее, после заполнения показателей VaR производится расчет графика нормального распределения при помощи упомянутого метода. В качестве значения оси x используются уже упомянутые доходности для графика, рассчитанного по приростам, и абсолютные значения прибыли для графика, рассчитанного по прибылям.

Расчет Z-счета также довольно тривиален, а формула была взята в одной из статей, ранее написанных на данном сайте его администрацией. Посему за тривиальностью данного метода считаю возможным пропустить код его реализации, но вы всегда можете его посмотреть в приложенных файлах. 

В завершение описания данного класса стоит указать на то, что все расчеты начинаются с метода Calculate со следующей сигнатурой вызова

void CReportCreator::Create(DealDetales &history[],DealDetales &BH_history[],const double _balance,const string &Symb[],double _r);

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

Заключение

Рассмотрев в прошлой статье процесс написания библиотеки на языке C#, мы перешли к следующему этапу — созданию отчета торгов, который как раз и будет выгружаться при содействии разработанной DLL-библиотеки и механизмов, рассмотренный в следующей статье. Сам механизм формирования отчетов, как уже сказано выше, был взят из прошлых наработок, но с тех пор как они были созданы, они перетерпели ряд изменений и доработок. В данной статье представлены последние версии данных наработок и вся стыковка уже оттестирована не одним месяцем оптимизаций и выгрузкой отчетов.

 

В приложенном архиве находятся две папки, обе должны быть разархивированы в директорию MQL/Include. 

В архиве содержатся следующие файлы:

  1. CustomGeneric
    • GenericSorter.mqh
    • ICustomComparer.mqh
  2. History manager
    • CustomComissionManager.mqh
    • DealHistoryGetter.mqh
    • ReportCreator.mqh
Прикрепленные файлы |
Include.zip (24.84 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (2)
Aliaksei Karalkou
Aliaksei Karalkou | 12 янв. 2020 в 11:48
Статья супер, но я даже не начинал изучать mql5. Когда то я пытался то же проделать на mql4, но затея провалилась, хотя до конца не отказался. Поэтому вопрос: можно ли что то там реализовать похожее ?
Andrey Azatskiy
Andrey Azatskiy | 12 янв. 2020 в 15:24
Aliaksei Karalkou:
Статья супер, но я даже не начинал изучать mql5. Когда то я пытался то же проделать на mql4, но затея провалилась, хотя до конца не отказался. Поэтому вопрос: можно ли что то там реализовать похожее ?

Думаю что можно, однако я на MQL4 стараюсь не писать, по моему лучше на последней версии продукта работать, коей является MQL5 на текущий момент времени.

Библиотека для простого и быстрого создания программ для MetaTrader (Часть XXX): Отложенные торговые запросы - управление объектами-запросами Библиотека для простого и быстрого создания программ для MetaTrader (Часть XXX): Отложенные торговые запросы - управление объектами-запросами
В прошлой статье создали классы объектов отложенных запросов, соответствующие общей концепции объектов библиотеки. Сегодня займёмся классом, позволяющем управлять объектами отложенных запросов.
Библиотека для простого и быстрого создания программ для MetaTrader (Часть XXIX): Отложенные торговые запросы - классы объектов-запросов Библиотека для простого и быстрого создания программ для MetaTrader (Часть XXIX): Отложенные торговые запросы - классы объектов-запросов
В прошлых статьях проверили концепцию отложенных торговых запросов. Отложенный запрос — это по сути обычный торговый приказ, но исполняемый по некоему условию. Сегодня создадим полноценные классы объектов-отложенных запросов — базовый объект-запрос и его наследников.
Непрерывная скользящая оптимизация (Часть 3): Способ адаптации робота к автооптимизатору Непрерывная скользящая оптимизация (Часть 3): Способ адаптации робота к автооптимизатору
Третья статья служит неким мостом между двумя предыдущими, в ней освещается механизм взаимодействия с DLL, написанной в первой статье, и объектами для выгрузки из второй статьи. Показывается процесс создания обертки для класса, который импортируется из DLL и формирует XML-файл с историей торгов, а также способ взаимодействии с данной оберткой.
Библиотека для простого и быстрого создания программ для MetaTrader (Часть XXVIII): Отложенные торговые запросы - закрытие, удаление, модификации Библиотека для простого и быстрого создания программ для MetaTrader (Часть XXVIII): Отложенные торговые запросы - закрытие, удаление, модификации
Это третья статья о концепции отложенных запросов. В ней мы завершим тестирование работы с отложенными торговыми запросами - создадим методы для закрытия позиций, удаления отложенных ордеров и модификацию параметров позиций и отложенных ордеров.