Записываем историю сделок в файл и строим графики балансов для каждого символа в Excel

Записываем историю сделок в файл и строим графики балансов для каждого символа в Excel
Общаясь на многих форумах, я довольно часто приводил в пример результаты тестов на скриншотах с графиков в Microsoft Excel. И многие просили меня объяснить, как же строить эти замечательные графики. Возможности построения диаграмм в Excel очень обширны и написаны множество книг по этой теме. В книгах так сложно бывает что-то найти, что приходиться читать её всю. Но наконец у меня появилось немного времени, чтобы написать статью об этом.

В предыдущих двух статьях ("Мультивалютный советник на MQL5. Пример простой, точной и быстрой схемы." и "Разработка мультивалютного эксперта с неограниченным количеством параметров") мы рассматривали разработку мультивалютных экспертов на языке MQL5. Но результат теста в MetaTrader 5 отображается, как общая кривая баланса и средств. То есть, если нужно посмотреть результаты по каждому отдельному символу, то нужно снова и снова заходить во внешние параметры эксперта, чтобы внести изменения, отключив все символы, кроме того, результаты которого нужно увидеть, и снова проводить тест. Это неудобно. Поэтому сегодня я покажу простой способ, как можно в пару кликов получать графики всех балансов символов и совокупный результат мультивалютного эксперта на одной диаграмме в Excel одновременно.

Для воспроизведения примера возьмём мультивалютного эксперта из предыдущей статьи. Добавим в него функцию, которая будет по окончании теста записывать в файл в формате CSV историю сделок и кривые балансов всех символов по отдельности. Кроме этого, добавим в отчёт ещё один столбец, в котором будут содержаться просадки депозита от всех локальных максимумов. Ещё создадим книгу Excel, которую настроим таким образом, чтобы можно было подключить к ней файл с данными. Книга может быть всё время загружена. То есть, её не нужно закрывать перед тем, как провести очередной тест. После проведения теста нужно будет всего лишь обновить данные одним нажатием кнопки, чтобы увидеть изменения в отчёте и на диаграмме.

Особых изменений в эксперте не будет. Всего лишь добавим несколько функций. Начнём с того, что в файл ARRAYS.mqh добавим структуру и массив для балансов символов.

//+------------------------------------------------------------------+
//| МАССИВЫ ДЛЯ ПОСТРОЕНИЯ ГРАФИКОВ                                  |
//+------------------------------------------------------------------+
struct BalS { double b[]; };
//---
BalS blcS[]; // Объявим объект массивов для балансов символов
//---

Далее для функций, которые будут готовить отчёты тестов, создадим отдельный подключаемый файл TESTING_REPORTS.mqh и подключим его к основному файлу эксперта (выделенная строка в коде ниже):

//+------------------------------------------------------------------+
//| СВОИ БИБЛИОТЕКИ                                                  |
//+------------------------------------------------------------------+
#include "Include/ENUMS.mqh"
#include "Include/ARRAYS.mqh"
#include "Include/INIT_ARRAYS.mqh"
#include "Include/ERRORS.mqh"
#include "Include/FILE_OPERATIONS.mqh"
#include "Include/TRADE_SIGNALS.mqh"
#include "Include/TRADE_FUNCTIONS.mqh"
#include "Include/GET_STRING.mqh"
#include "Include/TESTING_REPORTS.mqh"
#include "Include/ADD_FUNCTIONS.mqh"
//---

В первую очередь создадим структуру свойств сделок подобно тем, которые сейчас уже есть в проекте для свойств позиции и свойств символов. Для этого в файл ENUMS.mqh добавим перечисление идентификаторов свойств:

//+------------------------------------------------------------------+
//| ПЕРЕЧИСЛЕНИЕ СВОЙСТВ СДЕЛКИ                                      |
//+------------------------------------------------------------------+
enum ENUM_PROP_HISDEAL
  {
   HDL_SYMBOL     = 0, // Символ
   HDL_COMMENT    = 1, // Комментарий
   HDL_TYPE       = 2, // Тип
   HDL_ENTRY      = 3, // Направление
   HDL_PRICE      = 4, // Цена
   HDL_PROFIT     = 5, // Прибыль/убыток
   HDL_VOLUME     = 6, // Объём
   HDL_SWAP       = 7, // Своп
   HDL_COMMISSION = 8, // Комиссия
   HDL_TIME       = 9, // Время
   HDL_ALL        = 10 // Все вышеперечисленные свойства сделки
  };
//---

Затем в файле TESTING_REPORTS.mqh создадим структуру свойств сделки и функцию GetPropHistoryDeal(), которая возвращает свойство сделки. Функция принимает два параметра: тикет сделки и идентификатор.

Ниже можно посмотреть код структуры и функции GetPropHistoryDeal():

//+------------------------------------------------------------------+
//| СТРУКТУРА СВОЙСТВ СДЕЛКИ                                         |
//+------------------------------------------------------------------+
struct propHistoryDeal
  {
   string
   symbol,     // Символ
   comment;    // Комментарий
   //---
   uint
   type,       // Тип
   entry;      // Направление
   //---
   double
   price,      // Цена
   profit,     // Прибыль/убыток
   volume,     // Объём
   swap,       // Своп
   commission; // Комиссия
   //---
   datetime
   time;       // Время
  };
//---
propHistoryDeal hdl; // Создадим переменную hdl типа структуры propHistoryDeal
//---
//+------------------------------------------------------------------+
//| ПОЛУЧАЕТ СВОЙСТВА СДЕЛКИ                                         |
//+------------------------------------------------------------------+
void GetPropHistoryDeal(ulong t,int deal_prop)
  {
// Получим свойство/(а) сделки/(ок)
   switch(deal_prop)
     {
      case HDL_SYMBOL     : hdl.symbol=HistoryDealGetString(t,DEAL_SYMBOL);         break;
      case HDL_COMMENT    : hdl.comment=HistoryDealGetString(t,DEAL_COMMENT);       break;
      case HDL_TYPE       : hdl.type=(int)HistoryDealGetInteger(t,DEAL_TYPE);       break;
      case HDL_ENTRY      : hdl.entry=(int)HistoryDealGetInteger(t,DEAL_ENTRY);     break;
      case HDL_PRICE      : hdl.price=HistoryDealGetDouble(t,DEAL_PRICE);           break;
      case HDL_PROFIT     : hdl.profit=HistoryDealGetDouble(t,DEAL_PROFIT);         break;
      case HDL_VOLUME     : hdl.volume=HistoryDealGetDouble(t,DEAL_VOLUME);         break;
      case HDL_SWAP       : hdl.swap=HistoryDealGetDouble(t,DEAL_SWAP);             break;
      case HDL_COMMISSION : hdl.commission=HistoryDealGetDouble(t,DEAL_COMMISSION); break;
      case HDL_TIME       : hdl.time=(datetime)HistoryDealGetInteger(t,DEAL_TIME);  break;
      //---
      case HDL_ALL :
         hdl.symbol=HistoryDealGetString(t,DEAL_SYMBOL);
         hdl.comment=HistoryDealGetString(t,DEAL_COMMENT);
         hdl.type=(int)HistoryDealGetInteger(t,DEAL_TYPE);
         hdl.entry=(int)HistoryDealGetInteger(t,DEAL_ENTRY);
         hdl.price=HistoryDealGetDouble(t,DEAL_PRICE);
         hdl.profit=HistoryDealGetDouble(t,DEAL_PROFIT);
         hdl.volume=HistoryDealGetDouble(t,DEAL_VOLUME);
         hdl.swap=HistoryDealGetDouble(t,DEAL_SWAP);
         hdl.commission=HistoryDealGetDouble(t,DEAL_COMMISSION);
         hdl.time=(datetime)HistoryDealGetInteger(t,DEAL_TIME);
         //---
         break;
         //---
      default: Print("Переданное свойство сделки не учтено в перечислении!"); return;
     }
  }
//---

Ещё нам понадобятся функции, которые преобразуют некоторые свойства сделок в строковые значения. Напишем их в файле GET_STRING.mqh. Ниже представлен их код:

//+------------------------------------------------------------------+
//| ПРЕОБРАЗУЕТ ЦЕНУ В СТРОКУ                                        |
//+------------------------------------------------------------------+
string DealSymbolCorrectStr(string lsymbol)
  {
   string str="";
//---
   if(lsymbol=="") { str="-"; } else { str=lsymbol; }
//---
   return(str);
  }
//+------------------------------------------------------------------+
//| ПРЕОБРАЗУЕТ ТИП СДЕЛКИ В СТРОКУ                                  |
//+------------------------------------------------------------------+
string DealTypeToString(int ldeal_type)
  {
   string str="";
//---
   switch(ldeal_type)
     {
      case DEAL_TYPE_BUY                      : str="buy";                      break;
      case DEAL_TYPE_SELL                     : str="sell";                     break;
      case DEAL_TYPE_BALANCE                  : str="balance";                  break;
      case DEAL_TYPE_CREDIT                   : str="credit";                   break;
      case DEAL_TYPE_CHARGE                   : str="charge";                   break;
      case DEAL_TYPE_CORRECTION               : str="correction";               break;
      case DEAL_TYPE_BONUS                    : str="bonus";                    break;
      case DEAL_TYPE_COMMISSION               : str="commission";               break;
      case DEAL_TYPE_COMMISSION_DAILY         : str="commission daily";         break;
      case DEAL_TYPE_COMMISSION_MONTHLY       : str="commission monthly";       break;
      case DEAL_TYPE_COMMISSION_AGENT_DAILY   : str="commission agent daily";   break;
      case DEAL_TYPE_COMMISSION_AGENT_MONTHLY : str="commission agent monthly"; break;
      case DEAL_TYPE_INTEREST                 : str="interest";                 break;
      case DEAL_TYPE_BUY_CANCELED             : str="buy canceled";             break;
      case DEAL_TYPE_SELL_CANCELED            : str="sell canceled";            break;
      //---
      default : str="unknown";
     }
//---
   return(str);
  }
//+------------------------------------------------------------------+
//| ПРЕОБРАЗУЕТ НАПРАВЛЕНИЕ СДЕЛКИ В СТРОКУ                          |
//+------------------------------------------------------------------+
string DealEntryToString(int ldeal_entry)
  {
   string str="";
//---
   switch(ldeal_entry)
     {
      case DEAL_ENTRY_IN   : str="in";            break;
      case DEAL_ENTRY_OUT  : str="out";           break;
      case DEAL_ENTRY_INOUT: str="in/out";        break;
      case DEAL_ENTRY_STATE: str="status record"; break;
      //---
      default : str="unknown";
     }
//---
   return(str);
  }
//+------------------------------------------------------------------+
//| ПРЕОБРАЗУЕТ ОБЪЁМ В СТРОКУ                                       |
//+------------------------------------------------------------------+
string DealLotToString(double ldeal_lot)
  {
   string str="";
//---
   if(ldeal_lot<=0) { str="-"; } else { str=DS(ldeal_lot,2); }
//---
   return(str);
  }
//+------------------------------------------------------------------+
//| ПРЕОБРАЗУЕТ ЦЕНУ В СТРОКУ                                        |
//+------------------------------------------------------------------+
string DealPriceToString(double ldeal_price,int digit)
  {
   string str="";
//---
   if(ldeal_price<=0) { str="-"; } else { str=DS(ldeal_price,digit); }
//---
   return(str);
  }
//+------------------------------------------------------------------+
//| ПРЕОБРАЗУЕТ ПРОФИТ В СТРОКУ                                      |
//+------------------------------------------------------------------+
string DealProfitToString(string ldeal_symbol, double ldeal_profit)
  {
   string str="";
//---
   if(ldeal_profit==0 || hdl.symbol=="") { str="-"; } else { str=DS(ldeal_profit,2); }
//---
   return(str);
  }
//+------------------------------------------------------------------+
//| ПРЕОБРАЗУЕТ СВОП В СТРОКУ                                        |
//+------------------------------------------------------------------+
string DealSwapToString(double ldeal_swap)
  {
   string str="";
//---
   if(ldeal_swap==0) { str="-"; } else { str=DS(ldeal_swap,2); }
//---
   return(str);
  }
//---

Теперь всё готово для того, чтобы создать функцию CreateReportBalancesSymbols(), в которой будут подготавливаться данные для отчёта и затем записываться в файл. Ниже представлен код этой функции:

//+------------------------------------------------------------------+
//| СОЗДАЁТ ОТЧЁТ ТЕСТИРОВАНИЯ ПО СДЕЛКАМ В ФОРМАТЕ CSV              |
//+------------------------------------------------------------------+
void CreateReportBalancesSymbols()
  {
   int hdlFl=-1; // Хэндл файла
   string path=""; // Путь к файлу
//---
// Если ошибка при создании/получении директории, выходим
   if((path=CheckCreateGetTestPath())=="") { return; }
//---
// Создадим файл для записи данных в общей папке терминала
   hdlFl=FileOpen(path+"\\LastTest.csv",FILE_CSV|FILE_WRITE|FILE_ANSI|FILE_COMMON);
//---
// Если хэндл валиден (файл создался/открылся)
   if(hdlFl>0)
     {
      int dgt=0; // Количество знаков в цене после запятой
      int s=0,i=0,n=0; // Индексы в циклах
      int deals_total=0; // Количество сделок выбранной истории
      //---
      ulong ticket=0; // Тикет сделки
      double
      maxdds=0.0,// Просадка
      balance=0.0; // Баланс
      //---
      string
      dlm=",",// Разделитель
      writeString=""; // Для формирования строки для записи
      //---
      // Заголовки
      string Headers="TIME,SYMBOL,TYPE DEAL,TYPE ENTRY,LOT,PRICE,SWAP($),PROFIT($),DRAWDOWNS(%),BALANCE";
      //---
      // Если участвует больше одного символа, то...
      if(CNTS>1) // ...дополним строку заголовков
        {
         for(s=0; s<CNTS; s++)
           { StringAdd(Headers,","+aSymbol[s]); }
        }
      //---
      FileWrite(hdlFl,Headers); // Запишем заголовки отчёта
      //---
      HistorySelect(0,TimeCurrent()); // Получим всю историю
      deals_total=HistoryDealsTotal(); // Узнаем количество сделок
      //---
      // Установим размер массива балансов по кол-ву символов
      ArrayResize(blcS,CNTS);
      //---
      // Установим размер массивов сделок для каждого символа
      for(s=0; s<CNTS; s++)
        { ArrayResize(blcS[s].b,deals_total); }
      //---
      // Пройдёмся в цикле и запишем данные
      for(i=0; i<deals_total; i++)
        {
         ticket=HistoryDealGetTicket(i); // Получим тикет сделки
         //---
         GetPropHistoryDeal(ticket,HDL_ALL); // Получим все свойства сделки
         dgt=(int)SymbolInfoInteger(hdl.symbol,SYMBOL_DIGITS); // Узнаем кол-во знаков в цене
         //---
         balance+=hdl.profit+hdl.swap+hdl.commission; // Посчитаем общий баланс
         //---
         // Сформируем строку для записи путём конкатенации
         StringConcatenate(writeString,
                           hdl.time,dlm,
                           DealSymbolCorrectStr(hdl.symbol),dlm,
                           DealTypeToString(hdl.type),dlm,
                           DealEntryToString(hdl.entry),dlm,
                           DealLotToString(hdl.volume),dlm,
                           DealPriceToString(hdl.price,dgt),dlm,
                           DealSwapToString(hdl.swap),dlm,
                           DealProfitToString(hdl.symbol,hdl.profit),dlm,
                           StrTesterMaxDD(i,balance,maxdds),dlm,
                           DS(balance,2));
         //---
         // Если участвует больше одного символа, то...
         if(CNTS>1) // ...запишем их значения баланса
           {
            // Пройдёмся по всем символам
            for(s=0; s<CNTS; s++)
              {
               // Если символы равны и результат сделки ненулевой
               if(hdl.symbol==aSymbol[s] && hdl.profit!=0)
                 {
                  // ...отразим сделку в балансе с этим символом
                  // Посчитаем
                  blcS[s].b[i]=blcS[s].b[i-1]+
                               hdl.profit+
                               hdl.swap+
                               hdl.commission;
                  //---
                  // Добавим к строке
                  StringAdd(writeString,","+DS(blcS[s].b[i],2));
                 }
               else // Иначе запишем предыдущее значение
                 {
                  // Если тип сделки "Начисление баланса" (первая сделка), то...
                  if(hdl.type==DEAL_TYPE_BALANCE)
                    {
                     // ...для всех символов баланс одинаковый
                     blcS[s].b[i]=balance;
                     StringAdd(writeString,","+DS(blcS[s].b[i],2));
                    }
                  else
                    {
                     // Запишем предыдущее значение в текущий индекс
                     blcS[s].b[i]=blcS[s].b[i-1];
                     StringAdd(writeString,","+DS(blcS[s].b[i],2));
                    }
                 }
              }
           }
         //---
         FileWrite(hdlFl,writeString); // Запишем сформированную строку
         writeString=""; // Обязательное обнуление переменной для следующей строки
        }
      //---
      FileClose(hdlFl); // Закроем файл
     }
   else // Если файл не создался/открылся, выведем сообщение
     { Print("Ошибка при создании файла, error: "+IS(GetLastError())+""); }
  }
//---

Обратите внимание на выделенную строку (136) в коде выше. Функция StrTesterMaxDD() рассчитывает все просадки от локальных максимумов и возвращает результат, когда появился новый локальный максимум. Во всех остальных случаях функция возвращает строку "-" (прочерк).

Код функции StrTesterMaxDD():

//+------------------------------------------------------------------+
//| ВОЗВРАЩАЕТ МАКСИМАЛЬНУЮ ПРОСАДКУ ОТ ЛОКАЛЬНОГО МАКСИМУМА         |
//+------------------------------------------------------------------+
string StrTesterMaxDD(int i,double balance,double &maxdds)
  {
   string str=""; // Строка для отображения в отчёте
   static double max=0.0,min=0.0; // Для расчёта локального максимума и просадки
//---
   if(i==0) // Если первая сделка
     {
      maxdds=0.0; // Просадки нет
      //---
      // Зададим начальную точку, как локальный максимум
      max=balance; 
      min=balance;
     }
   else
     {
      // Если текущий баланс больше, чем в памяти,...
      if(balance>max)
        {
         // ...посчитаем просадку по предыдущим значениям и...
         maxdds=100-((min/max)*100);
         //---
         // ...обновим локальный максимум
         max=balance;
         min=balance;
        }
      else
        {
         maxdds=0.0; // Возвратим нулевое значение просадки
         min=fmin(min,balance); // Обновим минимум
        }
     }
//---
// Определим строку для отчёта
   if(maxdds==0) { str="-"; } // Прочерк или
   else { str=DS(maxdds,2); } // значение просадки
//---
   return(str); // Вернём строку
  }
//---

Все функции для формирования отчёта готовы. Осталось только разобраться, как это нужно использовать. А для этого понадобится функция OnTester(), которая вызывается по окончании тестирования. Обязательно посмотрите в Справке подробное описание этой функции.

В теле функции OnTester() нужно написать всего лишь пару строчек кода, чтобы указать программе, в каком случае следует формировать отчёт. Ниже показан этот фрагмент кода:

//+------------------------------------------------------------------+
//| РЕЗУЛЬТАТЫ ТЕСТИРОВАНИЯ                                          |
//+------------------------------------------------------------------+
double OnTester()
  {
   // Отчёт записываем только после теста
   if(Testing() && !Optimization() && !VisualMode())
     {
      CreateReportBalancesSymbols(); // Формирование отчёта и запись в файл
     }
//---
   return(0.0);
  }
//---

Теперь, если запустить эксперта в тестере, по окончании теста будет создана папка эксперта в общей папке терминала в директории C:\ProgramData\MetaQuotes\Terminal\Common\Files. А в папке эксперта будет создан файл отчёта LastTest.csv. Если открыть файл в Блокноте, то можно увидеть примерно такие записи:

Файл отчёта в формате CSV

Этот файл можно открыть в Excel и каждый тип данных будет расположен в отдельном столбце, что намного удобнее для просмотра (см. рисунок ниже). Можно построить графики уже сейчас и сохранить файл, как книгу Excel в формате *.xlsx, но если провести потом тест и открыть книгу снова, то мы увидим старые данные. Если же попытаться обновить данные, когда в Excel загружен файл LastTest.csv, то файл также не обновится, так как эксперт не сможет открыть его для записи, когда он занят другим приложением.

Файл отчёта в формате CSV в Excel 2010

Для нашего случая есть способ. Сначала создайте книгу Excel в формате *.xlsx в любом удобном для Вас месте. После этого откройте её и перейдите на вкладку Данные (см. рисунок ниже).

Вкладка Данные и опция Из текста в Excel 2010
 
На ленте этой вкладки нужно выбрать опцию Из текста. Она выделена на рисунке выше красным прямоугольником. Нажав на неё откроется окно Импорт текстового файла, в котором нужно указать файл LastTest.csv.

Выделив файл и нажав кнопку Импорт, откроется окно Мастер текстов (шаг 1 из 3):

Окно Мастер текстов в Excel 2010 - шаг 1 из 3

Установите настройки так, как показано выше и нажмите кнопку Далее >. На следующем шаге (2 из 3) нужно указать, какой разделитель используется в файле данных (см. рисунок ниже). В нашем файле это "," (запятая).

Окно Мастер текстов в Excel 2010 - шаг 2 из 3

Перейдите к следующему шагу (3 из 3) нажав кнопку Далее >. В этом диалоговом окне оставьте для всех столбцов формат Общий. Формат можно будет изменить потом позже.

Окно Мастер текстов в Excel 2010 - шаг 3 из 3

После нажатия кнопки Готово откроется диалоговое окно Импорт данных, в котором будет предложено выбрать лист и ячейку для импорта данных (см. рисунок ниже).

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

Свойства внешнего диапазона при импорте данных их текста в Excel 2010

Установите всё также, как показано на рисунке выше и нажмите OK в этом и следующем окне.

В итоге Вы увидите данные, также как, если бы Вы просто загрузили файл в формате CSV. Но теперь Вы можете многократно проводить тест в MetaTrader 5 не закрывая книгу Excel. Всё, что нужно сделать после теста, так это просто обновить данные нажав горячие клавиши Ctrl+Alt+F5 или кнопку Обновить все на ленте вкладки Данные.

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

Условное форматирование данных в Excel 2010

Теперь данные нужно отобразить на диаграммах Excel. На одной диаграмме мы отобразим графики всех балансов, а на второй все просадки от локальных максимумов в виде гистограммы.

Сначала создадим диаграмму для графиков балансов. Выделите заголовки всех балансов и удерживая клавишу Shift нажмите на клавиатуре поочерёдно (нажимая и отпуская) сначала клавишу End, а затем Стрелка вниз. Весь массив данных будет выделен вниз до конца. Теперь на вкладке Вставка нужно выбрать тип диаграммы для отображения данных. На рисунке ниже показан, какой тип нужно выбрать:

Выбор типа диаграммы в Excel 2010

Будет создана диаграмма, которую для удобства лучше перенести на другой лист. Создайте новый лист, а затем перенесите на него диаграмму. Для этого просто выделите её, нажмите Ctrl+X (вырезать). Затем перейдите на только что созданный лист, выделите ячейку A1 и нажмите Ctrl+V (вставить).

На рисунке ниже показан внешний вид диаграммы с настройками по умолчанию:

Внешний вид диаграммы с настройками по умолчанию

Каждый элемент диаграммы можно настроить: изменить размер, цвет, стиль и т.д.

На рисунке выше видно, что горизонтальная ось показывает количество сделок. Сделаем так, чтобы там отображались даты. Для этого нажмите на диаграмме правой кнопкой мыши и в контекстном меню нажмите опцию Выбрать данные.  Откроется диалоговое окно Выбор источника данных. Нажмите кнопку Изменить (выделено красным прямоугольником на рисунке ниже) и затем выделите диапазон данных в столбце TIME и нажмите OK.

Диалоговое окно Выбор источника данных

Диаграмму для просадок попробуйте создать самостоятельно и затем, разместите её под первой диаграммой.

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

Настроенный внешний вид диаграмм в Excel 2010


Выглядит супер. Если понравилось, жмите кнопку +1 под статьёй. :) На этом закончим. В одной из будущих статей я покажу, как сделать ещё более информативные отчёты в Excel.

Успехов!


Комментариев нет :

Отправить комментарий