Мультисимвольный график баланса в MetaTrader 5

Anatoli Kazharski | 13 марта, 2018

Содержание

Введение

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

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

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


Разработка графического интерфейса

В статье Визуализируем оптимизацию торговой стратегии в MetaTrader 5 было подробно показано, как подключить и использовать библиотеку EasyAndFast, и как создать с ее помощью графический интерфейс для своего MQL-приложения. Поэтому здесь сразу начнём с графического интерфейса по рассматриваемой теме. 

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

В листинге кода ниже представлены объявления методов для создания этих элементов. Имплементация методов вынесена в отдельный подключаемый файл.

//+------------------------------------------------------------------+
//| Класс для создания приложения                                    |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  {
private:
   //--- Окно
   CWindow           m_window1;
   //--- Статусная строка
   CStatusBar        m_status_bar;
   //--- Графики
   CGraph            m_graph1;
   CGraph            m_graph2;
   //--- Кнопки
   CButton           m_update_graph;
   //---
public:
   //--- Создаёт графический интерфейс
   bool              CreateGUI(void);
   //---
private:
   //--- Форма
   bool              CreateWindow(const string text);
   //--- Статусная строка
   bool              CreateStatusBar(const int x_gap,const int y_gap);
   //--- Графики
   bool              CreateGraph1(const int x_gap,const int y_gap);
   bool              CreateGraph2(const int x_gap,const int y_gap);
   //--- Кнопки
   bool              CreateUpdateGraph(const int x_gap,const int y_gap,const string text);
  };
//+------------------------------------------------------------------+
//| Методы для создания элементов управления                         |
//+------------------------------------------------------------------+
#include "CreateGUI.mqh"
//+------------------------------------------------------------------+

Главный метод создания графического интерфейса тогда будет выглядеть так:

//+------------------------------------------------------------------+
//| Создаёт графический интерфейс                                    |
//+------------------------------------------------------------------+
bool CProgram::CreateGUI(void)
  {
//--- Создание формы для элементов управления
   if(!CreateWindow("Expert panel"))
      return(false);
//--- Создание элементов управления
   if(!CreateStatusBar(1,23))
      return(false);
   if(!CreateGraph1(1,50))
      return(false);
   if(!CreateGraph2(1,159))
      return(false);
   if(!CreateUpdateGraph(7,25,"Update data"))
      return(false);
//--- Завершение создания GUI
   CWndEvents::CompletedGUI();
   return(true);
  }

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

 Рис. 1 – Графический интерфейс эксперта.

Рис. 1. Графический интерфейс эксперта.

Далее рассмотрим запись данных в файл после теста.


Мультисимвольный эксперт для тестов

Для тестов возьмём эксперта MACD Sample из стандартной поставки, но сделаем его мультисимвольным. Используемая мультисимвольная схема в этой версии неточная. На одних и тех же параметрах результат будет отличаться в зависимости от того, на каком символе будет проходить тест (выбирается в настройках тестера). Поэтому этот эксперт предназначен только для тестов и демонстрации полученных результатов в рамках представленной темы.

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

Во внешних параметрах добавим ещё один строковой параметр для указания символов, на которых будет проходить тест:

//--- Внешние параметры
sinput string Symbols           ="EURUSD,USDJPY,GBPUSD,EURCHF"; // Symbols
input  double InpLots           =0.1;                           // Lots
input  int    InpTakeProfit     =167;                           // Take Profit (in pips)
input  int    InpTrailingStop   =97;                            // Trailing Stop Level (in pips)
input  int    InpMACDOpenLevel  =16;                            // MACD open level (in pips)
input  int    InpMACDCloseLevel =19;                            // MACD close level (in pips)
input  int    InpMATrendPeriod  =14;                            // MA trend period

Символы нужно указывать через запятую. В классе программы (CProgram) реализованы методы для чтения этого параметра, а также для проверки символов и установки в обзор рынка тех из них, которые есть в списке на сервере. Как вариант, можно указывать символы для торговли и через заранее подготовленный список в файле, как это было показано в статье Рецепты MQL5 - Разработка мультивалютного эксперта с неограниченным количеством параметров. Более того, можно в файле делать несколько списков на выбор пользователя, и такой пример можно посмотреть в статье Рецепты MQL5 - Уменьшаем эффект подгонки и решаем проблему недостаточного количества котировок. Можно придумать ещё множество различных способов для выбора символов и их списков с помощью графического интерфейса. В одной из следующих статей я продемонстрирую такой вариант. 

Перед тем, как проверить символы в общем списке, их нужно сохранить в массив. Затем передадим этот массив (source_array[]) в метод CProgram::CheckTradeSymbols(). Здесь в первом цикле проходим по указанным во внешнем параметре символам и затем во втором цикле проверяем, есть ли этот символ в списке на сервере брокера. Если есть, то добавляем его в окно "Обзор рынка" и в массив проверенных символов. 

В самом конце метода, если символов не обнаружено, то будет использоваться только текущий символ, на котором установлен эксперт.

class CProgram : public CWndEvents
  {
private:
   //--- Проверяет символы для торговли в переданном массиве и возвращает массив доступных
   void              CheckTradeSymbols(string &source_array[],string &checked_array[]);
  };
//+------------------------------------------------------------------+
//| Проверяет символы для торговли в переданном массиве и            |
//| возвращает массив доступных                                      |
//+------------------------------------------------------------------+
void CProgram::CheckTradeSymbols(string &source_array[],string &checked_array[])
  {
   int symbols_total     =::SymbolsTotal(false);
   int size_source_array =::ArraySize(source_array);
//--- Ищем указанные символы в общем списке
   for(int i=0; i<size_source_array; i++)
     {
      for(int s=0; s<symbols_total; s++)
        {
         //--- Получим имя текущего символа в общем списке
         string symbol_name=::SymbolName(s,false);
         //--- Если совпадение
         if(symbol_name==source_array[i])
           {
            //--- Установим символ в обзоре рынка
            ::SymbolSelect(symbol_name,true);
            //--- Добавим в массив подтверждённых символов
            int size_array=::ArraySize(checked_array);
            ::ArrayResize(checked_array,size_array+1);
            checked_array[size_array]=symbol_name;
            break;
           }
        }
     }
//--- Если символов не обнаружено, то используем только текущий символ
   if(::ArraySize(checked_array)<1)
     {
      ::ArrayResize(checked_array,1);
      checked_array[0]=_Symbol;
     }
  }

Для чтения строкового внешнего параметра, в котором указываются символы, используется метод CProgram::CheckSymbols(). Здесь строка расщепляется в массив по разделителю ','. В полученных строках обрезаются пробелы по обеим сторонам. После этого массив отправляется на проверку в метод CProgram::CheckTradeSymbols(), который мы рассматривали выше.

class CProgram : public CWndEvents
  {
private:
   //--- Проверяет и отбирает в массив символы для торговли из строки
   int               CheckSymbols(const string symbols_enum);
  };
//+------------------------------------------------------------------+
//| Проверяет и отбирает в массив символы для торговли из строки     |
//+------------------------------------------------------------------+
int CProgram::CheckSymbols(const string symbols_enum)
  {
   if(symbols_enum!="")
      ::Print(__FUNCTION__," > input trade symbols: ",symbols_enum);
//--- Получим символы из строки
   string symbols[];
   ushort u_sep=::StringGetCharacter(",",0);
   ::StringSplit(symbols_enum,u_sep,symbols);
//--- Обрежем пробелы с двух сторон
   int elements_total=::ArraySize(symbols);
   for(int e=0; e<elements_total; e++)
     {
      ::StringTrimLeft(symbols[e]);
      ::StringTrimRight(symbols[e]);
     }
//--- Проверим символы
   ::ArrayFree(m_symbols);
   CheckTradeSymbols(symbols,m_symbols);
//--- Вернём количество символов для торговли
   return(::ArraySize(m_symbols));
  }

Файл с классом торговой стратегии подключается к файлу с классом приложения, и создаётся динамический массив типа CStrategy

#include "Strategy.mqh"
//+------------------------------------------------------------------+
//| Класс для создания приложения                                    |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  {
private:
   //--- Массив стратегий
   CStrategy         m_strategy[];
  };

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

class CProgram : public CWndEvents
  {
private:
   //--- Всего символов
   int               m_symbols_total;
  };
//+------------------------------------------------------------------+
//| Инициализация                                                    |
//+------------------------------------------------------------------+
bool CProgram::OnInitEvent(void)
  {
//--- Получим символы для торговли
   m_symbols_total=CheckSymbols(Symbols);
//--- Размер массива ТС
   ::ArrayResize(m_strategy,m_symbols_total);
//--- Инициализация
   for(int i=0; i<m_symbols_total; i++)
     {
      if(!m_strategy[i].OnInitEvent(m_symbols[i]))
         return(false);
     }
//--- Инициализация прошла успешно
   return(true);
  }

Далее рассмотрим запись данных последнего теста в файл.


Запись данных в файл

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

class CProgram : public CWndEvents
  {
private:
   //--- Путь к файлу с результатами последнего теста
   string            m_last_test_report_path;
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CProgram::CProgram(void) : m_symbols_total(0)
  {
//--- Путь к файлу с результатами последнего теста
   m_last_test_report_path=::MQLInfoString(MQL_PROGRAM_NAME)+"\\LastTest.csv";
  }

Рассмотрим метод CProgram::CreateSymbolBalanceReport(), с помощью которого будет проходить запись в файл. Для работы в этом методе (а также и в другом, который мы будем рассматривать позже) нам понадобятся массивы балансов символов.

//--- Массивы для балансов всех символов
struct CReportBalance { double m_data[]; };
//+------------------------------------------------------------------+
//| Класс для создания приложения                                    |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  {
private:
   //--- Массив балансов всех символов
   CReportBalance    m_symbol_balance[];
   //---
private:
   //--- Создает отчет тестирования по сделкам в формате CSV
   void              CreateSymbolBalanceReport(void);
  };
//+------------------------------------------------------------------+
//| Создает отчет тестирования по сделкам в формате CSV              |
//+------------------------------------------------------------------+
void CProgram::CreateSymbolBalanceReport(void)
  {
   ...
  }

В начале метода открываем файл для работы в общей папке терминалов (FILE_COMMON):

...
//--- Создадим файл для записи данных в общей папке терминала
   int file_handle=::FileOpen(m_last_test_report_path,FILE_CSV|FILE_WRITE|FILE_ANSI|FILE_COMMON);
//--- Если хэндл валиден (файл создался/открылся)
   if(file_handle==INVALID_HANDLE)
     {
      ::Print(__FUNCTION__," > Error creating file: ",::GetLastError());
      return;
     }
...

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

Здесь же формируем первую строку с заголовками этих данных:

...
   double max_drawdown    =0.0; // Максимальная просадка
   double balance         =0.0; // Баланс
   string delimeter       =","; // Разделитель
   string string_to_write ="";  // Для формирования строки для записи
//--- Сформируем строку заголовков
   string headers="TIME,SYMBOL,DEAL TYPE,ENTRY TYPE,VOLUME,PRICE,SWAP($),PROFIT($),DRAWDOWN(%),BALANCE";
...

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

...
//--- Если участвует больше одного символа, то дополним строку заголовков
   int symbols_total=::ArraySize(m_symbols);
   if(symbols_total>1)
     {
      for(int s=0; s<symbols_total; s++)
         ::StringAdd(headers,delimeter+m_symbols[s]);
     }
//--- Запишем заголовки отчета
   ::FileWrite(file_handle,headers);
...

Далее получаем всю историю сделок и их количество, а затем устанавливаем размеры массивам:

...
//--- Получим всю историю
   ::HistorySelect(0,LONG_MAX);
//--- Узнаем количество сделок
   int deals_total=::HistoryDealsTotal();
//--- Установим размер массива балансов по кол-ву символов
   ::ArrayResize(m_symbol_balance,symbols_total);
//--- Установим размер массивов сделок для каждого символа
   for(int s=0; s<symbols_total; s++)
      ::ArrayResize(m_symbol_balance[s].m_data,deals_total);
...

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

Построчно записываем данные в файл. В конце метода файл закрывается.
...
//--- Пройдемся в цикле и запишем данные
   for(int i=0; i<deals_total; i++)
     {
      //--- Получим тикет сделки
      if(!m_deal_info.SelectByIndex(i))
         continue;
      //--- Узнаем кол-во знаков в цене
      int digits=(int)::SymbolInfoInteger(m_deal_info.Symbol(),SYMBOL_DIGITS);
      //--- Посчитаем общий баланс
      balance+=m_deal_info.Profit()+m_deal_info.Swap()+m_deal_info.Commission();
      //--- Сформируем строку для записи путем конкатенации
      ::StringConcatenate(string_to_write,
                          ::TimeToString(m_deal_info.Time(),TIME_DATE|TIME_MINUTES),delimeter,
                          m_deal_info.Symbol(),delimeter,
                          m_deal_info.TypeDescription(),delimeter,
                          m_deal_info.EntryDescription(),delimeter,
                          ::DoubleToString(m_deal_info.Volume(),2),delimeter,
                          ::DoubleToString(m_deal_info.Price(),digits),delimeter,
                          ::DoubleToString(m_deal_info.Swap(),2),delimeter,
                          ::DoubleToString(m_deal_info.Profit(),2),delimeter,
                          MaxDrawdownToString(i,balance,max_drawdown),delimeter,
                          ::DoubleToString(balance,2));
      //--- Если участвует больше одного символа, то запишем значения их баланса
      if(symbols_total>1)
        {
         //--- Пройдемся по всем символам
         for(int s=0; s<symbols_total; s++)
           {
            //--- Если символы совпадают и результат сделки ненулевой
            if(m_deal_info.Symbol()==m_symbols[s] && m_deal_info.Profit()!=0)
               //--- Отразим сделку в балансе с этим символом. Учтём своп и комиссию
               m_symbol_balance[s].m_data[i]=m_symbol_balance[s].m_data[i-1]+m_deal_info.Profit()+m_deal_info.Swap()+m_deal_info.Commission();
            //--- Иначе запишем предыдущее значение
            else
              {
               //--- Если тип сделки "Начисление баланса" (первая сделка), то для всех символов баланс одинаковый
               if(m_deal_info.DealType()==DEAL_TYPE_BALANCE)
                  m_symbol_balance[s].m_data[i]=balance;
               //--- Иначе запишем предыдущее значение в текущий индекс
               else
                  m_symbol_balance[s].m_data[i]=m_symbol_balance[s].m_data[i-1];
              }
            //--- Добавим к строке баланс символа
            ::StringAdd(string_to_write,delimeter+::DoubleToString(m_symbol_balance[s].m_data[i],2));
           }
        }
      //--- Запишем сформированную строку
      ::FileWrite(file_handle,string_to_write);
      //--- Обязательное обнуление переменной для следующей строки
      string_to_write="";
     }
//--- Закроем файл
   ::FileClose(file_handle);
...

В процессе формирования строк (см. код выше) для записи в файл для расчёта общей просадки по балансу используется метод CProgram::MaxDrawdownToString(). При первом его вызове просадка равна нулю, и в качестве локального максимума/минимума запоминаем текущее значение баланса. В следующих вызовах метода, в случае, когда баланс больше, чем в памяти, считаем просадку по предыдущим значениям и обновляем локальный максимум. В противном случае обновляем локальный минимум и возвращаем нулевое значение (пустая строка).

class CProgram : public CWndEvents
  {
private:
   //--- Возвращает максимальную просадку от локального максимума
   string            MaxDrawdownToString(const int deal_number,const double balance,double &max_drawdown);
  };
//+------------------------------------------------------------------+
//| Возвращает максимальную просадку от локального максимума         |
//+------------------------------------------------------------------+
string CProgram::MaxDrawdownToString(const int deal_number,const double balance,double &max_drawdown)
  {
//--- Строка для отображения в отчете
   string str="";
//--- Для расчета локального максимума и просадки
   static double max=0.0;
   static double min=0.0;
//--- Если первая сделка
   if(deal_number==0)
     {
      //--- Просадки еще нет
      max_drawdown=0.0;
      //--- Зададим начальную точку, как локальный максимум
      max=balance;
      min=balance;
     }
   else
     {
      //--- Если текущий баланс больше, чем в памяти
      if(balance>max)
        {
         //--- Посчитаем просадку по предыдущим значениям
         max_drawdown=100-((min/max)*100);
         //--- Обновим локальный максимум
         max=balance;
         min=balance;
        }
      else
        {
         //--- Возвратим нулевое значение просадки и обновим минимум
         max_drawdown=0.0;
         min=fmin(min,balance);
        }
     }
//--- Определим строку для отчета
   str=(max_drawdown==0)? "" : ::DoubleToString(max_drawdown,2);
   return(str);
  }

Структура файла позволяет открыть его в программе Excel. Как это выглядит, показано на скриншоте ниже:

 Рис. 2 – Структура файла отчёта.

Рис. 2. Структура файла отчёта в программе Excel.

В итоге вызов метода CProgram::CreateSymbolBalanceReport() для записи отчёта после теста нужно делать в конце теста:

//+------------------------------------------------------------------+
//| Событие окончания теста                                          |
//+------------------------------------------------------------------+
double CProgram::OnTesterEvent(void)
  {
//--- Отчет записываем только после теста
   if(::MQLInfoInteger(MQL_TESTER) && !::MQLInfoInteger(MQL_OPTIMIZATION) && 
      !::MQLInfoInteger(MQL_VISUAL_MODE) && !::MQLInfoInteger(MQL_FRAME_MODE))
     {
      //--- Формирование отчёта и запись в файлы
      CreateSymbolBalanceReport();
     }
//---
   return(0.0);
  }

Далее рассмотрим чтение данных отчёта.


Извлечение данных из файла

После всего того, что реализовано выше, теперь каждая проверка эксперта в Тестере стратегий будет заканчиваться записью отчёта в файл. Далее рассмотрим методы, с помощью которых будут считываться данные из этого отчёта. В первую очередь нужно прочитать файл и поместить его содержимое в массив, чтобы с ним было удобно работать. Для этого используется метод CProgram::ReadFileToArray(). Здесь открываем файл, в который в конце теста эксперта была записана история сделок. В цикле читаем файл до последней строки и заполняем массив исходными данными. 

class CProgram : public CWndEvents
  {
private:
   //--- Массив для данных из файла
   string            m_source_data[];
   //--- 
private:
   //--- Чтение файла в переданный массив
   bool              ReadFileToArray(const int file_handle);
  };
//+------------------------------------------------------------------+
//| Чтение файла в переданный массив                                 |
//+------------------------------------------------------------------+
bool CProgram::ReadFileToArray(const int file_handle)
  {
//--- Открываем файл
   int file_handle=::FileOpen(m_last_test_report_path,FILE_READ|FILE_ANSI|FILE_COMMON);
//--- Выйти, если файл не открылся
   if(file_handle==INVALID_HANDLE)
      return(false);
//--- Освободим массив
   ::ArrayFree(m_source_data);
//--- Читаем файл в массив
   while(!::FileIsEnding(file_handle))
     {
      int size=::ArraySize(m_source_data);
      ::ArrayResize(m_source_data,size+1,RESERVE);
      m_source_data[size]=::FileReadString(file_handle);
     }
//--- Закрыть файл
   ::FileClose(file_handle);
   return(true);
  }

Понадобится вспомогательный метод CProgram::GetStartIndex() для определения индекса столбца с названием BALANCE. В качестве аргументов в него нужно передать строку заголовков, в которой будет осуществляться поиск названия столбца и динамический массив для элементов строки расщеплённой по разделителю ','.  

class CProgram : public CWndEvents
  {
private:
   //--- Начальный индекс балансов в отчёте
   bool              GetBalanceIndex(const string headers);
  };
//+------------------------------------------------------------------+
//| Определим индекс, с которого нужно начать копирование данных     |
//+------------------------------------------------------------------+
bool CProgram::GetBalanceIndex(const string headers)
  {
//--- Получим элементы строки по разделителю
   string str_elements[];
   ushort u_sep=::StringGetCharacter(",",0);
   ::StringSplit(headers,u_sep,str_elements);
//--- Ищем столбец с названием 'BALANCE'
   int elements_total=::ArraySize(str_elements);
   for(int e=elements_total-1; e>=0; e--)
     {
      string str=str_elements[e];
      ::StringToUpper(str);
      //--- Если нашли столбец с нужным заголовком
      if(str=="BALANCE")
        {
         m_balance_index=e;
         break;
        }
     }
//--- Вывести сообщение, если не найден столбец с названием 'BALANCE'
   if(m_balance_index==WRONG_VALUE)
     {
      ::Print(__FUNCTION__," > In the report file there is no heading \'BALANCE\' ! ");
      return(false);
     }
//--- Получилось
   return(true);
  }

По оси X у обоих графиков будут отображаться номера сделок. Диапазон дат будем показывать как дополнительную информацию в нижнем колонтитуле графика балансов. Для определения начальной и конечной даты истории сделок реализован метод CProgram::GetDateRange(). В него передаются две строковые переменные по ссылке для начальной и конечной даты истории сделок.

class CProgram : public CWndEvents
  {
private:
   //--- Диапазон дат
   void              GetDateRange(string &from_date,string &to_date);
  };
//+------------------------------------------------------------------+
//| Получим начальную и конечную даты тестового диапазона            |
//+------------------------------------------------------------------+
void CProgram::GetDateRange(string &from_date,string &to_date)
  {
//--- Выйти, если строк меньше 3
   int strings_total=::ArraySize(m_source_data);
   if(strings_total<3)
      return;
//--- Получим начальную и конечную даты отчёта
   string str_elements[];
   ushort u_sep=::StringGetCharacter(",",0);
//---
   ::StringSplit(m_source_data[1],u_sep,str_elements);
   from_date=str_elements[0];
   ::StringSplit(m_source_data[strings_total-1],u_sep,str_elements);
   to_date=str_elements[0];
  }

Для получения данных балансов и просадок используется методы CProgram::GetReportDataToArray() и CProgram::AddDrawDown(). Второй вызывается в первом, и его код очень краток (см. листинг ниже). Здесь передаётся индекс сделки и значение просадки, которые помещаются в соответствующие массивы, значения которых потом будут отображаться на графике. В массив m_dd_y[] сохраняем значение просадки, а в массив m_dd_x[] — индекс, на котором нужно отобразить это значение. Таким образом, на тех индексах, где значений нет, на графиках ничего не будет отображаться (пустые значения).

class CProgram : public CWndEvents
  {
private:
   //--- Просадки по общему балансу
   double            m_dd_x[];
   double            m_dd_y[];
   //--- 
private:
   //--- Добавляет просадку в массивы
   void              AddDrawDown(const int index,const double drawdown);
  };
//+------------------------------------------------------------------+
//| Добавляет просадку в массивы                                     |
//+------------------------------------------------------------------+
void CProgram::AddDrawDown(const int index,const double drawdown)
  {
   int size=::ArraySize(m_dd_y);
   ::ArrayResize(m_dd_y,size+1,RESERVE);
   ::ArrayResize(m_dd_x,size+1,RESERVE);
   m_dd_y[size] =drawdown;
   m_dd_x[size] =(double)index;
  }

В методе CProgram::GetReportDataToArray() сначала определяются размеры массивов и количество серий для графика балансов. Далее инициализируем массив заголовков. Затем в цикле построчно извлекаются элементы строки по разделителю, и данные помещаются в массивы просадок и балансов.  

class CProgram : public CWndEvents
  {
private:
   //--- Получает данные символов из отчёта
   int               GetReportDataToArray(string &headers[]);
  };
//+------------------------------------------------------------------+
//| Получим данные символов из отчёта                                |
//+------------------------------------------------------------------+
int CProgram::GetReportDataToArray(string &headers[])
  {
//--- Получим элементы строки заголовков
   string str_elements[];
   ushort u_sep=::StringGetCharacter(",",0);
   ::StringSplit(m_source_data[0],u_sep,str_elements);
//--- Размеры массивов
   int strings_total  =::ArraySize(m_source_data);
   int elements_total =::ArraySize(str_elements);
//--- Освободим массивы
   ::ArrayFree(m_dd_y);
   ::ArrayFree(m_dd_x);
//--- Получим количество серий
   int curves_total=elements_total-m_balance_index;
   curves_total=(curves_total<3)? 1 : curves_total;
//--- Установить размер массивам по количеству серий
   ::ArrayResize(headers,curves_total);
   ::ArrayResize(m_symbol_balance,curves_total);
//--- Установить размер сериям
   for(int i=0; i<curves_total; i++)
      ::ArrayResize(m_symbol_balance[i].m_data,strings_total,RESERVE);
//--- Если символов несколько (получим заголовки)
   if(curves_total>2)
     {
      for(int i=0,e=m_balance_index; e<elements_total; e++,i++)
         headers[i]=str_elements[e];
     }
   else
      headers[0]=str_elements[m_balance_index];
//--- Получим данные
   for(int i=1; i<strings_total; i++)
     {
      ::StringSplit(m_source_data[i],u_sep,str_elements);
      //--- Собираем данные в массивы
      if(str_elements[m_balance_index-1]!="")
         AddDrawDown(i,double(str_elements[m_balance_index-1]));
      //--- Если символов несколько
      if(curves_total>2)
         for(int b=0,e=m_balance_index; e<elements_total; e++,b++)
            m_symbol_balance[b].m_data[i]=double(str_elements[e]);
      else
         m_symbol_balance[0].m_data[i]=double(str_elements[m_balance_index]);
     }
//--- Первое значение серий
   for(int i=0; i<curves_total; i++)
      m_symbol_balance[i].m_data[0]=(strings_total<2)? 0 : m_symbol_balance[i].m_data[1];
//--- Вернуть количество серий
   return(curves_total);
  }

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


Отображение данных на графиках

Вызов вспомогательных методов, которые мы рассмотрели в предыдущем разделе, будет проходить в начале метода для обновления графика балансов — CProgram::UpdateBalanceGraph(). Далее текущие серии удаляются с графика, так как количество символов, участвующих в последнем тесте, могло измениться. Затем в цикле по текущему количеству символов, которое определили в методе CProgram::GetReportDataToArray(), добавляем новые серии данных балансов и заодно определяем минимальное и максимальное значения по оси Y. 

Здесь же запоминаем в полях класса размер серий и  шаг делений по оси X. Эти значения понадобятся также и для форматирования графика просадок. Для оси Y рассчитываются отступы для экстремумов графика, равные 5%. В итоге все эти значения применяются к графику балансов, а график обновляется для отображения последних изменений. 

class CProgram : public CWndEvents
  {
private:
   //--- Всего данных в серии
   double            m_data_total;
   //--- Шаг делений на шкале X
   double            m_default_step;
   //--- 
private:
   //--- Обновляет данные на графике балансов
   void              UpdateBalanceGraph(void);
  };
//+------------------------------------------------------------------+
//| Обновить график балансов                                         |
//+------------------------------------------------------------------+
void CProgram::UpdateBalanceGraph(void)
  {
//--- Получим даты тестового диапазона
   string from_date=NULL,to_date=NULL;
   GetDateRange(from_date,to_date);
//--- Определим индекс, с которого нужно начать копирование данных
   if(!GetBalanceIndex(m_source_data[0]))
      return;
//--- Получим данные символов из отчёта
   string headers[];
   int curves_total=GetReportDataToArray(headers);

//--- Обновим все серии графика новыми данными
   CColorGenerator m_generator;
   CGraphic *graph=m_graph1.GetGraphicPointer();
//--- Очистка графика
   int total=graph.CurvesTotal();
   for(int i=total-1; i>=0; i--)
      graph.CurveRemoveByIndex(i);
//--- Максимум и минимум графика
   double y_max=0.0,y_min=m_symbol_balance[0].m_data[0];
//--- Добавляем данные
   for(int i=0; i<curves_total; i++)
     {
      //--- Определим максимум/минимум по оси Y
      y_max=::fmax(y_max,m_symbol_balance[i].m_data[::ArrayMaximum(m_symbol_balance[i].m_data)]);
      y_min=::fmin(y_min,m_symbol_balance[i].m_data[::ArrayMinimum(m_symbol_balance[i].m_data)]);
      //--- Добавить серию на график
      CCurve *curve=graph.CurveAdd(m_symbol_balance[i].m_data,m_generator.Next(),CURVE_LINES,headers[i]);
     }
//--- Количество значений и шаг сетки оси X
   m_data_total   =::ArraySize(m_symbol_balance[0].m_data)-1;
   m_default_step =(m_data_total<10)? 1 : ::MathFloor(m_data_total/5.0);
//--- Диапазон и отступы
   double range  =::fabs(y_max-y_min);
   double offset =range*0.05;
//--- Цвет для первой серии
   graph.CurveGetByIndex(0).Color(::ColorToARGB(clrCornflowerBlue));
//--- Свойства горизонтальной оси
   CAxis *x_axis=graph.XAxis();
   x_axis.AutoScale(false);
   x_axis.Min(0);
   x_axis.Max(m_data_total);
   x_axis.MaxGrace(0);
   x_axis.MinGrace(0);
   x_axis.DefaultStep(m_default_step);
   x_axis.Name(from_date+" - "+to_date);
//--- Свойства вертикальной оси
   CAxis *y_axis=graph.YAxis();
   y_axis.AutoScale(false);
   y_axis.Min(y_min-offset);
   y_axis.Max(y_max+offset);
   y_axis.MaxGrace(0);
   y_axis.MinGrace(0);
   y_axis.DefaultStep(range/10.0);
//--- Обновить график
   graph.CurvePlotAll();
   graph.Update();
  }

Для обновления графика просадок используется метод CProgram::UpdateDrawdownGraph(). Так как данные уже рассчитаны в методе CProgram::UpdateBalanceGraph(), то здесь их нужно просто применить к графику и обновить его.

class CProgram : public CWndEvents
  {
private:
   //--- Обновляет данные на графике просадок
   void              UpdateDrawdownGraph(void);
  };
//+------------------------------------------------------------------+
//| Обновить график просадок                                         |
//+------------------------------------------------------------------+
void CProgram::UpdateDrawdownGraph(void)
  {
//--- Обновим график просадок
   CGraphic *graph=m_graph2.GetGraphicPointer();
   CCurve *curve=graph.CurveGetByIndex(0);
   curve.Update(m_dd_x,m_dd_y);
   curve.PointsFill(false);
   curve.PointsSize(6);
   curve.PointsType(POINT_CIRCLE);
//--- Свойства горизонтальной оси
   CAxis *x_axis=graph.XAxis();
   x_axis.AutoScale(false);
   x_axis.Min(0);
   x_axis.Max(m_data_total);
   x_axis.MaxGrace(0);
   x_axis.MinGrace(0);
   x_axis.DefaultStep(m_default_step);
//--- Обновить график
   graph.CalculateMaxMinValues();
   graph.CurvePlotAll();
   graph.Update();
  }

Вызов методов CProgram::UpdateBalanceGraph() и CProgram::UpdateDrawdownGraph() осуществляется в методе CProgram::UpdateGraphs(). Перед вызовом этих методов сначала вызывается метод CProgram::ReadFileToArray(), который получает данные из файла с результатами последнего теста эксперта. 

class CProgram : public CWndEvents
  {
private:
   //--- Обновляет данные на графиках результатов последнего теста
   void              UpdateGraphs(void);
  };
//+------------------------------------------------------------------+
//| Обновить графики                                                 |
//+------------------------------------------------------------------+
void CProgram::UpdateGraphs(void)
  {
//--- Заполняем массив данными из файла
   if(!ReadFileToArray())
     {
      ::Print(__FUNCTION__," > Could not open the test results file!");
      return;
     }
//--- Обновить график балансов и просадок
   UpdateBalanceGraph();
   UpdateDrawdownGraph();
  }

Демонстрация полученного результата

Чтобы отобразить результаты последнего теста на графиках интерфейса, нужно нажать всего лишь одну кнопку. Событие этого действия обрабатывается в методе CProgram::OnEvent():

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- События нажатия на кнопках
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
     {
      //--- Нажатие на кнопку 'Update data'
      if(lparam==m_update_graph.Id())
        {
         //--- Обновить графики
         UpdateGraphs();
         return;
        }
      //---
      return;
     }
  }

Если эксперт уже был протестирован до нажатия на кнопку, то мы увидим приблизительно вот что:

Рис. 3 – Результат последнего теста эксперта. 

Рис. 3. Результат последнего теста эксперта.

Таким образом, если эксперт загружен на график, то вы имеете возможность, просматривая результаты множества тестов после оптимизации параметров, сразу видеть изменения на мультисимвольном графике балансов. 

Мультисимвольный график баланса во время торговли и тестов

Теперь рассмотрим вторую версию эксперта, когда мультисимвольный график баланса отрисовывается и обновляется прямо в процессе торговли. 

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

Изменение истории будем проверять по приходу события в методе OnTrade(). Для проверки, того, что история пополнилась новой сделкой используется метод CProgram::IsLastDealTicket(). В нем получаем историю от времени, сохранённого в памяти после последнего вызова. Затем проверяем тикеты последней сделки и тикета, сохранённого в памяти. Если тикеты отличаются, то обновляем в памяти тикет и время последней сделки для следующей проверки, и возвращаем признак (true), что история изменилась.

class CProgram : public CWndEvents
  {
private:
   //--- Время и тикет последней проверенной сделки
   datetime          m_last_deal_time;
   ulong             m_last_deal_ticket;
   //--- 
private:
   //--- Проверка новой сделки
   bool              IsLastDealTicket(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CProgram::CProgram(void) : m_last_deal_time(NULL),
                           m_last_deal_ticket(WRONG_VALUE)
  {
  }
//+------------------------------------------------------------------+
//| Возвращает событие последней сделки на указанном символе         |
//+------------------------------------------------------------------+
bool CProgram::IsLastDealTicket(void)
  {
//--- Выйти, если история не получена
   if(!::HistorySelect(m_last_deal_time,LONG_MAX))
      return(false);
//--- Получим количество сделок в полученном списке
   int total_deals=::HistoryDealsTotal();
//--- Пройдемся по всем сделкам в полученном списке от последней сделки к первой
   for(int i=total_deals-1; i>=0; i--)
     {
      //--- Получим тикет сделки
      ulong deal_ticket=::HistoryDealGetTicket(i);
      //--- Если тикеты равны, выйдем
      if(deal_ticket==m_last_deal_ticket)
         return(false);
      //--- Если тикеты не равны, сообщим об этом
      else
        {
         datetime deal_time=(datetime)::HistoryDealGetInteger(deal_ticket,DEAL_TIME);
         //--- Запомним время и тикет последней сделки
         m_last_deal_time   =deal_time;
         m_last_deal_ticket =deal_ticket;
         return(true);
        }
     }
//--- Тикеты другого символа
   return(false);
  }

Перед тем, как пройтись по истории сделок и заполнить массивы данными, нужно определить, какие символы есть в истории и сколько их всего. Это нужно для установки размеров массивов. Для этого используется метод CProgram::GetHistorySymbols(). Перед его вызовом надо выбрать историю в нужном диапазоне. Далее прибавляем к строке символы, которые находим в истории. Чтобы символы в строке не повторялись, делаем проверку на наличие указанной подстроки. После этого добавляем найденные в истории символы в массив и возвращаем количество символов.

class CProgram : public CWndEvents
  {
private:
   //--- Массив символов из истории
   string            m_symbols_name[];
   //--- 
private:
   //--- Получим символы из истории счёта и вернём их количество
   int               GetHistorySymbols(void);
  };
//+------------------------------------------------------------------+
//| Получим символы из истории счёта и вернём их количество          |
//+------------------------------------------------------------------+
int CProgram::GetHistorySymbols(void)
  {
   string check_symbols="";
//--- Пройдемся первый раз в цикле и получим торгуемые символы
   int deals_total=::HistoryDealsTotal();
   for(int i=0; i<deals_total; i++)
     {
      //--- Получим тикет сделки
      if(!m_deal_info.SelectByIndex(i))
         continue;
      //--- Если есть название символа
      if(m_deal_info.Symbol()=="")
         continue;
      //--- Если такой строки ещё нет, добавим её
      if(::StringFind(check_symbols,m_deal_info.Symbol(),0)==-1)
         ::StringAdd(check_symbols,(check_symbols=="")? m_deal_info.Symbol() : ","+m_deal_info.Symbol());
     }
//--- Получим элементы строки по разделителю
   ushort u_sep=::StringGetCharacter(",",0);
   int symbols_total=::StringSplit(check_symbols,u_sep,m_symbols_name);
//--- Вернём количество символов
   return(symbols_total);
  }

Для получения мультисимвольного баланса нужно вызывать метод CProgram::GetHistorySymbolsBalance():

class CProgram : public CWndEvents
  {
private:
   //--- Получает общий баланс и балансы по всем символам отдельно
   void              GetHistorySymbolsBalance(void);
  };
//+------------------------------------------------------------------+
//| Получает общий баланс и балансы по всем символам отдельно        |
//+------------------------------------------------------------------+
void CProgram::GetHistorySymbolsBalance(void)
  {
   ...
  }

Здесь в самом начале нужно получить начальный баланс счёта. Получаем историю от самой первой сделки, она и будет этим начальным балансом. Предполагается, что есть возможность указать в календаре дату, от которой нужно показывать результат торговли. Поэтому выбираем историю ещё раз. Далее методом CProgram::GetHistorySymbols() получаем символы в выбранной истории и их количество, а после устанавливаем размеры массивам. Для отображения диапазона результата истории определяем начальную и конечную даты

...
//--- Начальный размер депозита
   ::HistorySelect(0,LONG_MAX);
   double balance=(m_deal_info.SelectByIndex(0))? m_deal_info.Profit() : 0;
//--- Получим историю от указанной даты
   ::HistorySelect(m_from_trade.SelectedDate(),LONG_MAX);
//--- Получим количество символов
   int symbols_total=GetHistorySymbols();
//--- Освободим массивы
   ::ArrayFree(m_dd_x);
   ::ArrayFree(m_dd_y);
//--- Установим размер массива балансов по количеству символов + 1 для общего баланса
   ::ArrayResize(m_symbols_balance,(symbols_total>1)? symbols_total+1 : 1);
//--- Установим размер массивов сделок для каждого символа
   int deals_total=::HistoryDealsTotal();
   for(int s=0; s<=symbols_total; s++)
     {
      if(symbols_total<2 && s>0)
         break;
      //---
      ::ArrayResize(m_symbols_balance[s].m_data,deals_total);
      ::ArrayInitialize(m_symbols_balance[s].m_data,0);
     }
//--- Количество кривых балансов
   int balances_total=::ArraySize(m_symbols_balance);
//--- Начало и конец истории
   m_begin_date =(m_deal_info.SelectByIndex(0))? m_deal_info.Time() : m_from_trade.SelectedDate();
   m_end_date   =(m_deal_info.SelectByIndex(deals_total-1))? m_deal_info.Time() : ::TimeCurrent();
...

В следующем цикле рассчитываются балансы символов и просадки. Полученные данные помещаются в массивы. Для расчёта просадки здесь тоже используются методы, описанные в предыдущих разделах.

...
//--- Максимальная просадка
   double max_drawdown=0.0;
//--- Запишем массивы балансов в переданный массив
   for(int i=0; i<deals_total; i++)
     {
      //--- Получим тикет сделки
      if(!m_deal_info.SelectByIndex(i))
         continue;
      //--- Инициализация на первой сделке
      if(i==0 && m_deal_info.DealType()==DEAL_TYPE_BALANCE)
         balance=0;
      //--- От указанной даты
      if(m_deal_info.Time()>=m_from_trade.SelectedDate())
        {
         //--- Посчитаем общий баланс
         balance+=m_deal_info.Profit()+m_deal_info.Swap()+m_deal_info.Commission();
         m_symbols_balance[0].m_data[i]=balance;
         //--- Рассчитаем просадку
         if(MaxDrawdownToString(i,balance,max_drawdown)!="")
            AddDrawDown(i,max_drawdown);
        }
      //--- Если участвует больше одного символа, то запишем значения их баланса
      if(symbols_total<2)
         continue;
      //--- Только от указанной даты
      if(m_deal_info.Time()<m_from_trade.SelectedDate())
         continue;
      //--- Пройдемся по всем символам
      for(int s=1; s<balances_total; s++)
        {
         int prev_i=i-1;
         //--- Если тип сделки "Начисление баланса" (первая сделка) ...
         if(prev_i<0 || m_deal_info.DealType()==DEAL_TYPE_BALANCE)
           {
            //--- ... для всех символов баланс одинаковый
            m_symbols_balance[s].m_data[i]=balance;
            continue;
           }
         //--- Если символы равны и результат сделки ненулевой
         if(m_deal_info.Symbol()==m_symbols_name[s-1] && m_deal_info.Profit()!=0)
           {
            //--- Отразим сделку в балансе с этим символом. Учтём своп и комиссию.
            m_symbols_balance[s].m_data[i]=m_symbols_balance[s].m_data[prev_i]+m_deal_info.Profit()+m_deal_info.Swap()+m_deal_info.Commission();
           }
         //--- Иначе запишем предыдущее значение
         else
            m_symbols_balance[s].m_data[i]=m_symbols_balance[s].m_data[prev_i];
        }
     }
...

Данные добавляются на графики и обновляются с помощью методов CProgram::UpdateBalanceGraph() и CProgram::UpdateDrawdownGraph(). Их код практически такой же, как и в первой версии эксперта, рассмотренного в предыдущих разделах, поэтому сразу перейдём к части, где они вызываются.

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

class CProgram : public CWndEvents
  {
private:
   //--- Инициализация графиков
   void              UpdateBalanceGraph(const bool update=false);
   void              UpdateDrawdownGraph(void);
  };
//+------------------------------------------------------------------+
//| Событие торговой операции                                        |
//+------------------------------------------------------------------+
void CProgram::OnTradeEvent(void)
  {
//--- Обновление графиков баланса и просадок
   UpdateBalanceGraph();
   UpdateDrawdownGraph();
  }

Кроме этого, пользователь в графическом интерфейсе может указать дату, от которой нужно построить графики баланса. Чтобы принудительно обновить график без проверки последнего тикета сделки, в метод CProgram::UpdateBalanceGraph() нужно передать значение true.

Событие изменения даты в календаре (ON_CHANGE_DATE) обрабатывается так:

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- События выбора даты в календаре
   if(id==CHARTEVENT_CUSTOM+ON_CHANGE_DATE)
     {
      if(lparam==m_from_trade.Id())
        {
         UpdateBalanceGraph(true);
         UpdateDrawdownGraph();
         m_from_trade.ChangeComboBoxCalendarState();
        }
      //---
      return;
     }
  }

Ниже показано, как это работает в тестере в режиме визуализации:

Рис. 4 – Демонстрация результата в тестере в режиме визуализации.

Рис. 4. Демонстрация результата в тестере в режиме визуализации.

Визуализируем отчёты из сервиса 'Сигналы'

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

Перейдите на страницу интересующего вас сигнала и выберите вкладку История сделок:

Рис. 5. История сделок сигнала.

Ссылку на скачивание CSV-файла с историей сделок можно найти внизу этого списка:

 Рис. 6 – Экспорт истории сделок в файл формата CSV.

Рис. 6. Экспорт истории сделок в файл формата CSV.

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

//+------------------------------------------------------------------+
//|                                                      Program.mqh |
//|                        Copyright 2018, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
//--- Внешние параметры
input string PathToFile=""; // Path to file
...

Рис. 7 – Внешний параметр для указания файла-отчёта.

Рис. 7.  Внешний параметр для указания файла-отчёта.

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

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

//--- Массивы для данных из файла
struct CReportTable
  {
   string            m_rows[];
  };
//+------------------------------------------------------------------+
//| Класс для создания приложения                                    |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  {
private:
   //--- Таблица для отчёта
   CReportTable      m_columns[];
   //--- Количество строк и столбцов
   uint              m_rows_total;
   uint              m_columns_total;
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CProgram::CProgram(void) : m_rows_total(0),
                           m_columns_total(0)
  {
...
  }

Для сортировки массива массивов понадобятся следующие методы:

class CProgram : public CWndEvents
  {
private:
   //--- Метод быстрой сортировки
   void              QuickSort(uint beg,uint end,uint column);
   //--- Проверка условия сортировки
   bool              CheckSortCondition(uint column_index,uint row_index,const string check_value,const bool direction);
   //--- Поменять значения в указанных ячейках местами
   void              Swap(uint r1,uint r2);
  };

Все эти методы подробно рассматривались в одной из предыдущих статей

Все основные операции осуществляются в методе CProgram::GetData(). Остановимся на нем подробнее. 

class CProgram : public CWndEvents
  {
private:
   //--- Получаем данные в массивы
   int               GetData(void);
  };
//+------------------------------------------------------------------+
//| Получим данные символов из отчёта                                |
//+------------------------------------------------------------------+
int CProgram::GetData(void)
  {
...
  }

Сначала определим количество строк и элементов строки по разделителю ';'. Затем получим в отдельный массив названия символов, которые есть в отчёте, и их количество. После готовим массивы и заполняем их данными из отчёта.

...
//--- Получим элементы строки заголовков
   string str_elements[];
   ushort u_sep=::StringGetCharacter(";",0);
   ::StringSplit(m_source_data[0],u_sep,str_elements);
//--- Количество строк и элементов строки
   int strings_total  =::ArraySize(m_source_data);
   int elements_total =::ArraySize(str_elements);
//--- Получим символы
   if((m_symbols_total=GetHistorySymbols())==WRONG_VALUE)
     return;
//--- Освободим массивы
   ::ArrayFree(m_dd_y);
   ::ArrayFree(m_dd_x);
//--- Размер рядов данных
   ::ArrayResize(m_columns,elements_total);
   for(int i=0; i<elements_total; i++)
      ::ArrayResize(m_columns[i].m_rows,strings_total-1);
//--- Заполним массивы данными из файла
   for(int r=0; r<strings_total-1; r++)
     {
      ::StringSplit(m_source_data[r+1],u_sep,str_elements);
      for(int c=0; c<elements_total; c++)
         m_columns[c].m_rows[r]=str_elements[c];
     }
...

Всё готово для сортировки данных. Здесь же нужно установить размер массивов балансов символов перед их заполнением:

...
//--- Количество рядов и столбцов
   m_rows_total    =strings_total-1;
   m_columns_total =elements_total;
//--- Отсортируем по времени в первом столбце
   QuickSort(0,m_rows_total-1,0);
//--- Размер серий
   ::ArrayResize(m_symbol_balance,m_symbols_total);
   for(int i=0; i<m_symbols_total; i++)
      ::ArrayResize(m_symbol_balance[i].m_data,m_rows_total);
...

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

...
//--- Баланс и максимальная просадка
   double balance      =0.0;
   double max_drawdown =0.0;
//--- Получим данные общего баланса
   for(uint i=0; i<m_rows_total; i++)
     {
      //--- Начальный баланс
      if(i==0)
        {
         balance+=(double)m_columns[elements_total-1].m_rows[i];
         m_symbol_balance[0].m_data[i]=balance;
        }
      else
        {
         //--- Пропускаем пополнения
         if(m_columns[1].m_rows[i]=="Balance")
            m_symbol_balance[0].m_data[i]=m_symbol_balance[0].m_data[i-1];
         else
           {
            balance+=(double)m_columns[elements_total-1].m_rows[i]+(double)m_columns[elements_total-2].m_rows[i]+(double)m_columns[elements_total-3].m_rows[i];
            m_symbol_balance[0].m_data[i]=balance;
           }
        }
      //--- Рассчитаем просадку
      if(MaxDrawdownToString(i,balance,max_drawdown)!="")
         AddDrawDown(i,max_drawdown);
     }
...

Затем заполняем массивы балансов для каждого отдельного символа. 

...
//--- Получим данные балансов символов
   for(int s=1; s<m_symbols_total; s++)
     {
      //--- Начальный баланс
      balance=m_symbol_balance[0].m_data[0];
      m_symbol_balance[s].m_data[0]=balance;
      //---
      for(uint r=0; r<m_rows_total; r++)
        {
         //--- Если символы не совпадают, то предыдущее значение
         if(m_symbols_name[s]!=m_columns[m_symbol_index].m_rows[r])
           {
            if(r>0)
               m_symbol_balance[s].m_data[r]=m_symbol_balance[s].m_data[r-1];
            //---
            continue;
           }
         //--- Если результат сделки ненулевой
         if((double)m_columns[elements_total-1].m_rows[r]!=0)
           {
            balance+=(double)m_columns[elements_total-1].m_rows[r]+(double)m_columns[elements_total-2].m_rows[r]+(double)m_columns[elements_total-3].m_rows[r];
            m_symbol_balance[s].m_data[r]=balance;
           }
         //--- Иначе запишем предыдущее значение
         else
            m_symbol_balance[s].m_data[r]=m_symbol_balance[s].m_data[r-1];
        }
     }
...

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

 Рис. 8 – Демонстрация результатов (пример 1).

Рис. 8.  Демонстрация результатов (пример 1).

 Рис. 9 – Демонстрация результатов (пример 2).

Рис. 9.  Демонстрация результатов (пример 2).

 Рис. 10 – Демонстрация результатов (пример 3).

Рис. 10. Демонстрация результатов (пример 3).

 Рис. 11 – Демонстрация результатов (пример 4).

Рис. 11.  Демонстрация результатов (пример 4).

Заключение

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

Ниже вы можете загрузить к себе на компьютер файлы для тестирования и подробного изучения представленного в статье кода. У каждой версии программы такая структура файлов: 

Наименование файла Комментарий
MacdSampleMultiSymbols.mq5 Модифицированный эксперт из стандартной поставки - MACD Sample
Program.mqh Файл с классом программы
CreateGUI.mqh Файл с реализацией методов из класса программы в файле Program.mqh
Strategy.mqh Файл с модифицированным классом стратегии MACD Sample (мультисимвольная версия)
FormatString.mqh Файл со вспомогательными функциями для форматирования строк