English 中文 Español Deutsch 日本語 Português 한국어 Français Italiano Türkçe
Создание информационных табло с использованием классов из Стандартной библиотеки и Google Chart API

Создание информационных табло с использованием классов из Стандартной библиотеки и Google Chart API

MetaTrader 5Примеры | 27 мая 2010, 13:10
6 304 20
Евгений
Евгений


Введение

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


1. Обзор компонентов Стандартной библиотеки

Итак, что же представляет собой эта библиотека? Раздел документации сайта гласит, что в ее состав входят:

Файлы с кодами всех классов лежат в папке MQL5/Include. Как можно убедиться, просматривая код библиотеки, она предоставляет для работы только  классы, но не функции. Следовательно, для ее использования необходимы некоторые познания в объектно-ориентированном программировании (ООП). 

Все классы библиотеки (кроме торговых) происходят от базового класса CObject. Для более наглядного представления, попытаемся построить Диаграмму классов , ведь у нас для этого все есть - базовый класс и его наследники. Т.к. язык  MQL5 является, по сути, подмножеством С++, то воспользуемся для автоматического построения диаграммы средством IBM Rational Rose, предоставляющим инструменты реверс-инженеринга С++ проектов.

 

Рисунок 1. Диаграмма классов Стандартной библиотеки

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

Как видим из диаграммы, каждый из компонентов библиотеки для работы со строками, файлами, графиком, графическими объектами и массивами, имеет свой собственный базовый класс (CString, CFile, CChart, CChartObject и CArray, соответственно), наследуемый от CObject.  Базовый класс для работы с индикаторам CIndicator и его вспомогательный класс CIndicators наследуются от CArrayObj, а класс доступа к индикаторному буферу CIndicatorBuffer от CArrayDouble.

Малиновым цветом на диаграмме выделены несуществующие в реальности классы Indicators, Arrays и ChartObjects  - они являются множествами, включающими классы для работы с индикаторами, массивами и графическими объектами. Так как их достаточно много и они наследуются от одного родителя, я позволил себе некоторое упрощение, чтобы не загромождать диаграмму. Например, Indicator включает в себя CiDEMA, CiStdDev и т.д.

Также стоит отметить, что диаграмму классов также можно построить с помощью системы автоматического создания документации Doxygen, там это сделать несколько проще, чем в Rational Rose. Подробнее о Doxygen можно прочитать в статье "Автоматическое создание документации к программам на MQL5".


2. Постановка задачи

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

Что будет отображать табло? Что-то вроде детализированного отчета MetaTrader 5, т.е.:

Рисунок 2. Вид детализированного отчета

Рисунок 2. Вид детализированного отчета

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

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

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


3. Проектирование интерфейса 

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

Реализуем наше Информационное табло в виде класса. Приступим:

//+------------------------------------------------------------------+
///Класс - табло
//+------------------------------------------------------------------+
class Board
  {
//защищенные данные
protected:
///номер подокна, куда разместим табло
   int               wnd;             
///массив с данными о сделках   
   CArrayObj        *Data;
///массив с данными о балансе   
   CArrayDouble      ChartData;       
///массив с элементами интерфейса   
   CChartObjectEdit  cells[10][6];    
///объект для работы с графиком   
   CChart            Chart;           
///объект для работы с графиком баланса   
   CChartObjectBmpLabel BalanceChart; 
///объект для работы с круговой диаграммой    
   CChartObjectBmpLabel PieChart;     
///данные для круговой диаграммы   
   PieData          *pie_data;
//скрытые данные и методы
private:
   double            net_profit;      //эти переменные будут хранить рассчитаные показатели
   double            gross_profit;
   double            gross_loss;
   double            profit_factor;
   double            expected_payoff;
   double            absolute_drawdown;
   double            maximal_drawdown;
   double            maximal_drawdown_pp;
   double            ralative_drawdown;
   double            ralative_drawdown_pp;
   int               total;
   int               short_positions;
   double            short_positions_won;
   int               long_positions;
   double            long_positions_won;
   int               profit_trades;
   double            profit_trades_pp;
   int               loss_trades;
   double            loss_trades_pp;
   double            largest_profit_trade;
   double            largest_loss_trade;
   double            average_profit_trade;
   double            average_loss_trade;
   int               maximum_consecutive_wins;
   double            maximum_consecutive_wins_usd;
   int               maximum_consecutive_losses;
   double            maximum_consecutive_losses_usd;
   int               maximum_consecutive_profit;
   double            maximum_consecutive_profit_usd;
   int               maximum_consecutive_loss;
   double            maximum_consecutive_loss_usd;
   int               average_consecutive_wins;
   int               average_consecutive_losses;
   ///метод получения данных о сделках и балансе
   void              GetData();
   ///метод расчета показателей
   void              Calculate();
   ///метод построения графика
   void              GetChart(int X_size,int Y_size,string request,string file_name);
   ///метод создания запроса для Google Charts
   string            CreateGoogleRequest(int X_size,int Y_size,bool type);
   ///метод получения оптимального размера шрифта
   int               GetFontSize(int x,int y);
   string            colors[12];  ///массив с текстовыми представлениями цветов
//открытые методы      
public:
///конструктор
   void              Board();    
///деструктор         
   void             ~Board();    
///метод для обновления табло
   void              Refresh();  
///метод для создания элементов интерфейса   
   void              CreateInterface(); 
  };

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

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

Сразу разберемся с  конструктором и деструктором класса, чтобы больше не возвращаться к ним:

//+------------------------------------------------------------------+
///Конструктор
//+------------------------------------------------------------------+
void Board::Board()
  {
   Chart.Attach();                               //привяжем текущий график к экземпляру класса
   wnd=ChartWindowFind(Chart.ChartId(),"IT");      //найдем окно индикатора
   Data = new CArrayObj;                         //создадим экземпляр класса "Список объектов"
   pie_data=new PieData;                         //создадим экземпляр класса "данные для диаграммы"
   //заполним массив с цветами
   colors[0]="003366"; colors[1]="00FF66"; colors[2]="990066";
   colors[3]="FFFF33"; colors[4]="FF0099"; colors[5]="CC00FF";
   colors[6]="990000"; colors[7]="3300CC"; colors[8]="000033";
   colors[9]="FFCCFF"; colors[10]="CC6633"; colors[11]="FF0000";
  }
//+------------------------------------------------------------------+
///Деструктор
//+------------------------------------------------------------------+
void Board::~Board()
  {
   if(CheckPointer(Data)!=POINTER_INVALID) delete Data;   //удалим данные о сделках
   if(CheckPointer(pie_data)!=POINTER_INVALID) delete pie_data;
   ChartData.Shutdown();    //и данные о балансе
   Chart.Detach();          //отвяжемся от графика
   for(int i=0;i<10;i++)     //удалим все элементы интерфейса
      for(int j=0;j<6;j++)
         cells[i][j].Delete();
   BalanceChart.Delete();   //удалим график балнса 
   PieChart.Delete();       //и круговую диаграмму
  }

В конструкторе привяжем объект типа CChart к текущему графику  с помощью его метода Attach(). Метод  Detach(), вызываемый в деструкторе, отвяжет график от объекта. Объект Data, являющийся указателем на объект типа CArrayObj, принимает адрес объекта, созданного динамически с помощью оператора new, и удаляется с помощью delete в деструкторе. Не забываем проверить наличие объекта перед удалением с помощью CheckPointer(), иначе произойдет ошибка при удалении.

Подробнее о классе CArrayObj будет сказано далее. Метод Shutdown() класса CArrayDouble, ровно как и любого другого класса, наследуемого от CArray(см. Диаграмму классов) очистит и высвободит занятую объектом память. Метод Delete() наследников CChartObject удаляет объект с графика.

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

Займемся, собственно, интерфейсом. Как было сказано выше, метод CreateInterface() создает интерфейс табло:

//+------------------------------------------------------------------+
///Функция-член создания интерфейса
//+------------------------------------------------------------------+
void Board::CreateInterface()
  {
   //извлечем ширину
   int x_size=Chart.WidthInPixels();
   //и высоту окна индикатора
   int y_size=Chart.GetInteger(CHART_HEIGHT_IN_PIXELS,wnd);
   
   //рассчитаем, сколько места займет график баланса
   double chart_border=y_size*(1.0-(Chart_ratio/100.0));

   if(Chart_ratio<100)//если график баланса занимает не все пространство на табло
     {
      for(int i=0;i<10;i++)//создадим столбцы
        {
         for(int j=0;j<6;j++)//и строки         
           {
            cells[i][j].Create(Chart.ChartId(),"InfTablo "+IntegerToString(i)+" "+IntegerToString(j),
                               wnd,j*(x_size/6.0),i*(chart_border/10.0),x_size/6.0,chart_border/10.0);
            //установим свойство "выделяемости" отключенным                   
            cells[i][j].Selectable(false);
            //текст в объектах только для чтения
            cells[i][j].ReadOnly(true);
            //устновим размер шрифта
            cells[i][j].FontSize(GetFontSize(x_size/6.0, chart_border/10.0));
            cells[i][j].Font("Arial");    //его имя  
            cells[i][j].Color(text_color);//и цвет           
           }
        }
     }

   if(Chart_ratio>0)//если график баланса необходим
     {
      //создадим график баланса
      BalanceChart.Create(Chart.ChartId(), "InfTablo chart", wnd, 0, chart_border);
      //установим свойство "выделяемости" отключенным
      BalanceChart.Selectable(false);
      //создадим круговую диаграмму
      PieChart.Create(Chart.ChartId(), "InfTablo pie_chart", wnd, x_size*0.75, chart_border);
      PieChart.Selectable(false);//установим свойство "выделяемости" отключенным   
     }

   Refresh();//обновим табло
  }

Для компактного расположения всех элементов сначала с помощью метода WidthInPixels() и GetInteger() класса CChart выясним длину и ширину подокна индикатора, где будет располагаться табло. Потом создадим ячейки, где будут размещаться значения показателей  с помощью метода Create() класса CChartObjectEdit (создает объект "поле ввода"), этот метод есть у всех наследников CChartObject.

Заметим, как удобно использовать Стандартную библиотеку для такого рода операций. Без нее нам пришлось бы каждый объект создавать функцией ObjectCreate, и устанавливать свойства объектов функциями типа ObjectSet, что привело бы к избыточности кода. А когда потом нам пришлось бы поменять свойства объектов, необходимо было контролировать имена объектов, чтобы избежать путаницы. Теперь мы можем просто создать массив графических объектов, и перебирать его вдоль и поперек при малейшем желании.

Кроме того, можно получать/устанавливать свойства объектов одной функцией, если она была перегружена создателями класса, как например метод Color наследников CChartObject, при вызове с параметром устанавливает, без параметра - получает цвет объекта. Рядом с графиком баланса разместим круговую диаграмму, она займет четверть размера всей ширины экрана.

Метод Refresh() обновляет табло. В чем заключается обновление? Нужно пересчитать показатели, занести их в графические объекты и перемасштабировать табло, если были изменены размеры окна, в котором оно находится. Табло должно занимать все свободное пространство окна.

//+------------------------------------------------------------------+
///Функция-член обновления табло
//+------------------------------------------------------------------+
void Board::Refresh()
  {
   //проверим наличие соединения с сервером
   if(!TerminalInfoInteger(TERMINAL_CONNECTED)) {Alert("Нет соеденения с торговым сервером!"); return;}
   //проверим разрешение на импорт функций из DLL
   if(!TerminalInfoInteger(TERMINAL_DLLS_ALLOWED)) {Alert("DLL запрещены!"); return;}
   //рассчитаем показатели
   Calculate();
   //извлечем ширину
   int x_size=Chart.WidthInPixels();
   //и высоту окна индикатора
   int y_size=Chart.GetInteger(CHART_HEIGHT_IN_PIXELS,wnd);
   //рассчитаем, сколько места займет график баланса
   double chart_border=y_size*(1.0-(Chart_ratio/100.0));

   string captions[10][6]= //массив с подписями элементов интерфейса
     {
        {"Total Net Profit:"," ","Gross Profit:"," ","Gross Loss:"," "},
        {"Profit Factor:"," ","Expected Payoff:"," ","",""},
        {"Absolute Drawdown:"," ","Maximal Drawdown:"," ","Relative Drawdown:"," "},
        {"Total Trades:"," ","Short Positions (won %):"," ","Long Positions (won %):"," "},
        {"","","Profit Trades (% of total):"," ","Loss trades (% of total):"," "},
        {"Largest","","profit trade:"," ","loss trade:"," "},
        {"Average","","profit trade:"," ","loss trade:"," "},
        {"Maximum","","consecutive wins ($):"," ","consecutive losses ($):"," "},
        {"Maximal","","consecutive profit (count):"," ","consecutive loss (count):"," "},
        {"Average","","consecutive wins:"," ","consecutive losses:"," "}
     };

   //занесем рассчитанные показатели в массив
   captions[0][1]=DoubleToString(net_profit, 2);
   captions[0][3]=DoubleToString(gross_profit, 2);
   captions[0][5]=DoubleToString(gross_loss, 2);

   captions[1][1]=DoubleToString(profit_factor, 2);
   captions[1][3]=DoubleToString(expected_payoff, 2);

   captions[2][1]=DoubleToString(absolute_drawdown, 2);
   captions[2][3]=DoubleToString(maximal_drawdown, 2)+"("+DoubleToString(maximal_drawdown_pp, 2)+"%)";
   captions[2][5]=DoubleToString(ralative_drawdown_pp, 2)+"%("+DoubleToString(ralative_drawdown, 2)+")";

   captions[3][1]=IntegerToString(total);
   captions[3][3]=IntegerToString(short_positions)+"("+DoubleToString(short_positions_won, 2)+"%)";
   captions[3][5]=IntegerToString(long_positions)+"("+DoubleToString(long_positions_won, 2)+"%)";

   captions[4][3]=IntegerToString(profit_trades)+"("+DoubleToString(profit_trades_pp, 2)+"%)";
   captions[4][5]=IntegerToString(loss_trades)+"("+DoubleToString(loss_trades_pp, 2)+"%)";

   captions[5][3]=DoubleToString(largest_profit_trade, 2);
   captions[5][5]=DoubleToString(largest_loss_trade, 2);

   captions[6][3]=DoubleToString(average_profit_trade, 2);
   captions[6][5]=DoubleToString(average_loss_trade, 2);

   captions[7][3]=IntegerToString(maximum_consecutive_wins)+"("+DoubleToString(maximum_consecutive_wins_usd, 2)+")";
   captions[7][5]=IntegerToString(maximum_consecutive_losses)+"("+DoubleToString(maximum_consecutive_losses_usd, 2)+")";

   captions[8][3]=DoubleToString(maximum_consecutive_profit_usd, 2)+"("+IntegerToString(maximum_consecutive_profit)+")";
   captions[8][5]=DoubleToString(maximum_consecutive_loss_usd, 2)+"("+IntegerToString(maximum_consecutive_loss)+")";

   captions[9][3]=IntegerToString(average_consecutive_wins);
   captions[9][5]=IntegerToString(average_consecutive_losses);

   if(Chart_ratio<100) //если график баланса занимает не все пространство на табло
     {
      for(int i=0;i<10;i++) //переберем элементы интерфейса
        {
         for(int j=0;j<6;j++)
           {
            //установим их положение
            cells[i][j].X_Distance(j*(x_size/6.0));
            cells[i][j].Y_Distance(i*(chart_border/10.0));
            //и размеры
            cells[i][j].X_Size(x_size/6.0);
            cells[i][j].Y_Size(chart_border/10.0);
            //и текст
            cells[i][j].SetString(OBJPROP_TEXT,captions[i][j]);
            //и размер шрифта
            cells[i][j].FontSize(GetFontSize(x_size/6.0,chart_border/10.0));
           }
        }
     }

   if(Chart_ratio>0)//если график баланса необходим
     {
      //обновим график баланса
      int X=x_size*0.75,Y=y_size-chart_border;
      //получим график
      GetChart(X,Y,CreateGoogleRequest(X,Y,true),"board_balance_chart");
      //установим его положение
      BalanceChart.Y_Distance(chart_border);
      //установим имя файла для отображения
      BalanceChart.BmpFileOn("board_balance_chart.bmp");
      BalanceChart.BmpFileOff("board_balance_chart.bmp");
      //обновим круговую диаграмму
      X=x_size*0.25;
      //получим график
      GetChart(X,Y,CreateGoogleRequest(X,Y,false),"pie_chart");
      //установим новое положение
      PieChart.Y_Distance(chart_border);
      PieChart.X_Distance(x_size*0.75);
      //установим имя файла для отображения
      PieChart.BmpFileOn("pie_chart.bmp");
      PieChart.BmpFileOff("pie_chart.bmp");
     }

   ChartRedraw(); //перерисуем окно
  }

Много кода, аналогично методу CreateInterface(), сначала функция Calculate() рассчитывает показатели, после они заносятся в графические объекты, и параллельно с этим размеры объектов подгоняются под размеры окна методами  X_Size и Y_Size. Методы  X_Distance и Y_Distance меняют положение объекта.

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

Рассмотрим эту функцию поближе:

//импорт DLL с функцией определения метрик строки
#import "String_Metrics.dll" 
void GetStringMetrics(int font_size,int &X,int &Y);
#import
//+------------------------------------------------------------------+
///Функция-член определения оптимального размера шрифта
//+------------------------------------------------------------------+
int Board::GetFontSize(int x,int y)
  {
   int res=8;
   for(int i=15;i>=1;i--)//перебираем размеры шрифтов
     {
      int X,Y; //сюда примем метрики строки
      //определим метрики
      GetStringMetrics(i,X,Y);
      //если строка "втискивается" в заданные рамки - вернем размер шрифта
      if(X<=x && Y<=y) return i;
     }
   return res;
  }

Функция GetStringMetrics() импортируется из написанной мною DLL, код которой можно будет найти в архиве DLL_Sources.zip, и при желании модифицировать ее. Думаю, она может пригодится вам при проектировании собственного интерфейса в проекте.

С интерфейсом покончено, займемся расчетом торговых показателей.


4. Расчет торговых показателей

Метод Calculate() занимается расчетами. Но ему не обойтись без метода GetData(), который получает необходимые данные:

//+------------------------------------------------------------------+
///Функция-член получения данных о сделках и балансе
//+------------------------------------------------------------------+
void Board::GetData()
  {
   //удалим старые данные
   Data.Shutdown();
   ChartData.Shutdown();
   pie_data.Shutdown();
   //подготовим всю историю сделок
   HistorySelect(0,TimeCurrent()); 
   CAccountInfo acc_inf;   //объект для работы со счетом
   //извлечем значение баланса
   double balance=acc_inf.Balance();
   double store=0; //накопитель баланса
   long_positions=0;
   short_positions=0;
   long_positions_won=0;
   short_positions_won=0;
   for(int i=0;i<HistoryDealsTotal();i++) //переберем все сделки в истории
     {
      CDealInfo deal;  //информация о сделках будет здесь
      deal.Ticket(HistoryDealGetTicket(i));//получим тикет сделки
       //если сделка имела финансовый результат (выход из рынка)
      if(deal.Ticket()>=0 && deal.Entry()==DEAL_ENTRY_OUT)
        {
         pie_data.Add(deal.Symbol()); //добавим данные для круговой диаграммы
          //проверка на символ 
         if(!For_all_symbols && deal.Symbol()!=Symbol()) continue;
         double profit=deal.Profit(); //ивлечем профит сделки
         profit+=deal.Swap();         //и своп
         profit+=deal.Commission();   //и комиссию
         store+=profit;               //накопитель баланса
         Data.Add(new CArrayDouble);  //добавим новый кортеж в массив
         ((CArrayDouble *)Data.At(Data.Total()-1)).Add(profit);  //и сами данные
         ((CArrayDouble *)Data.At(Data.Total()-1)).Add(deal.Type());
        }
     }

   //рассчитаем стартовый депозит
   double initial_deposit=(balance-store);
   for(int i=0;i<Data.Total();i++) //переберем подготовленные сделки
     {
      //рассчитаем значение баланса
      initial_deposit+=((CArrayDouble *)Data.At(i)).At(0);
      ChartData.Add(initial_deposit); //и запишем в массив
     }
  }

Сначала рассмотрим способ хранения данных. Стандартная библиотека предоставляет классы организации данных, с помощью которых вполне можно обойтись безо всяких массивов. Нам нужен двухмерный массив, в котором будем хранить данные по профиту и типу сделок в истории. Но Стандартная библиотека не предоставляет явных классов для организации двухмерного массива, зато есть классы CArrayDouble (массив данных типа double) и CArrayObj (динамический массив указателей на экземпляры класса CObject и его наследников). Т.е мы можем создать массив массивов типа double, что собственно и делается. 

Конечно, запись вида ((CArrayDouble *)Data.At(Data.Total()-1)).Add(profit) смотрится не так красиво, как data[i][j]=profit, но это только на первый взгляд. Ведь при простом объявлении массива, без использования классов Стандартной библиотеки, мы лишаемся таких преимуществ, как встроенный менеджер памяти, возможность вставки иного массива, сравнения массивов, поиска элементов и.т.д. Таким образом, использование классов организации памяти избавляет нас от необходимости контролировать переполнение массива и снабжает множеством полезных инструментов. 

Метод Total() наследников CArray (см. рис 1.) возвращает количество элементов в массиве, метод Add() добавляет, а метод At() получает элементы.

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

//+------------------------------------------------------------------+
///Класс данных для круговой диаграммы
//+------------------------------------------------------------------+
class PieData
  {
protected:
///количество сделок на символ 
   CArrayInt         val;   
///символы   
   CArrayString      symb;  
public:
///удаление данных
   bool Shutdown()          
     {
      bool res=true;
      res&=val.Shutdown();
      res&=symb.Shutdown();
      return res;
     }
///поиск строки в массиве
   int Search(string str)   
     {  //переберем массив, и найдем нужную сторку
      for(int i=0;i<symb.Total();i++)
         if(symb.At(i)==str) return i;
      return -1;
     }
///добавление новых данных
   void Add(string str)    
     {
      int symb_pos=Search(str);//определим позицию символа в массиве
      if(symb_pos>-1)
         val.Update(symb_pos,val.At(symb_pos)+1);//обновим данные по количеству сделок
      else //если такого символа еще нет
        {
         symb.Add(str); //добавим его
         val.Add(1);
        }
     }

   int Total() const {return symb.Total();}
   int Get_val(int pos) const {return val.At(pos);}
   string Get_symb(int pos) const {return symb.At(pos);}
  };

Не всегда классы Стандартной библиотеки смогут предоставить нам необходимые методы для работы. В данном примере метод Search класса CArrayString нам не подходит,так как для его применения требуется сначала отсортировать массив, что нарушит структуру данных. Поэтому пришлось написать свой метод.

Метод  Calculate() - расчет торговых показателей:

//+------------------------------------------------------------------+
///Функция-член расчета показателей
//+------------------------------------------------------------------+
void Board::Calculate()
  {
   //получим данные
   GetData();
   //обнулим показатели
   gross_profit=0;
   gross_loss=0;
   net_profit=0;
   profit_factor=0;
   expected_payoff=0;
   absolute_drawdown=0;
   maximal_drawdown_pp=0;
   maximal_drawdown=0;
   ralative_drawdown=0;
   ralative_drawdown_pp=0;
   total=Data.Total();
   long_positions=0;
   long_positions_won=0;
   short_positions=0;
   short_positions_won=0;
   profit_trades=0;
   profit_trades_pp=0;
   loss_trades=0;
   loss_trades_pp=0;
   largest_profit_trade=0;
   largest_loss_trade=0;
   average_profit_trade=0;
   average_loss_trade=0;
   maximum_consecutive_wins=0;
   maximum_consecutive_wins_usd=0;
   maximum_consecutive_losses=0;
   maximum_consecutive_losses_usd=0;
   maximum_consecutive_profit=0;
   maximum_consecutive_profit_usd=0;
   maximum_consecutive_loss=0;
   maximum_consecutive_loss_usd=0;
   average_consecutive_wins=0;
   average_consecutive_losses=0;

   if(total==0) return; //сделок нет - возврат
   double max_peak=0,min_peak=0,tmp_balance=0;
   int max_peak_pos=0,min_peak_pos=0;
   int max_cons_wins=0,max_cons_losses=0;
   double max_cons_wins_usd=0,max_cons_losses_usd=0;
   int avg_win=0,avg_loss=0,avg_win_cnt=0,avg_loss_cnt=0;

   for(int i=0; i<total; i++)
     {
      double profit=((CArrayDouble *)Data.At(i)).At(0); //извлечем профит
      int deal_type=((CArrayDouble *)Data.At(i)).At(1); //и тип сделки
      switch(deal_type) //проверим тип сделки
        {
         //и рассчитаем кол-во длинных и коротких позиций
         case DEAL_TYPE_BUY: {long_positions++; if(profit>=0) long_positions_won++; break;}
         case DEAL_TYPE_SELL: {short_positions++; if(profit>=0) short_positions_won++; break;}
        }

      if(profit>=0)//сделка прибыльная 
        {
         gross_profit+=profit; //суммарный профит
         profit_trades++;      //кол-во прибыльных сделок
         //самая прибыльная сделка и наибольшая прибыльная серия
         if(profit>largest_profit_trade) largest_profit_trade=profit;

         if(maximum_consecutive_losses<max_cons_losses || 
            (maximum_consecutive_losses==max_cons_losses && maximum_consecutive_losses_usd>max_cons_losses_usd))
           {
            maximum_consecutive_losses=max_cons_losses;
            maximum_consecutive_losses_usd=max_cons_losses_usd;
           }
         if(maximum_consecutive_loss_usd>max_cons_losses_usd || 
            (maximum_consecutive_loss_usd==max_cons_losses_usd && maximum_consecutive_losses<max_cons_losses))
           {
            maximum_consecutive_loss=max_cons_losses;
            maximum_consecutive_loss_usd=max_cons_losses_usd;
           }
         //средняя прибыль на сделку
         if(max_cons_losses>0) {avg_loss+=max_cons_losses; avg_loss_cnt++;}
         max_cons_losses=0;
         max_cons_losses_usd=0;
         max_cons_wins++;
         max_cons_wins_usd+=profit;
        }
      else //сделка убыточная
        {
         gross_loss-=profit; //суммарный профит
         loss_trades++;      //кол-во убыточных сделок
         //самая убыточная сделка и наибольшая убыточная серия
         if(profit<largest_loss_trade) largest_loss_trade=profit;
         if(maximum_consecutive_wins<max_cons_wins || 
            (maximum_consecutive_wins==max_cons_wins && maximum_consecutive_wins_usd<max_cons_wins_usd))
           {
            maximum_consecutive_wins=max_cons_wins;
            maximum_consecutive_wins_usd=max_cons_wins_usd;
           }
         if(maximum_consecutive_profit_usd<max_cons_wins_usd || 
            (maximum_consecutive_profit_usd==max_cons_wins_usd && maximum_consecutive_profit<max_cons_wins))
           {
            maximum_consecutive_profit=max_cons_wins;
            maximum_consecutive_profit_usd=max_cons_wins_usd;
           }
         //средний убыток на сделку
         if(max_cons_wins>0) {avg_win+=max_cons_wins; avg_win_cnt++;}
         max_cons_wins=0;
         max_cons_wins_usd=0;
         max_cons_losses++;
         max_cons_losses_usd+=profit;
        }

      tmp_balance+=profit; //расчет абсолютной просадки
      if(tmp_balance>max_peak) {max_peak=tmp_balance; max_peak_pos=i;}
      if(tmp_balance<min_peak) {min_peak=tmp_balance; min_peak_pos=i;}
      if((max_peak-min_peak)>maximal_drawdown && min_peak_pos>max_peak_pos) maximal_drawdown=max_peak-min_peak;
     }
   //расчет максимальной просадки
   double min_peak_rel=max_peak;
   tmp_balance=0;
   for(int i=max_peak_pos;i<total;i++)
     {
      double profit=((CArrayDouble *)Data.At(i)).At(0);
      tmp_balance+=profit;
      if(tmp_balance<min_peak_rel) min_peak_rel=tmp_balance;
     }
   //расчет относительной просадки
   ralative_drawdown=max_peak-min_peak_rel;
   //суммарный фин. результат торговли
   net_profit=gross_profit-gross_loss;
   //профит фактор
   profit_factor=(gross_loss!=0) ?  gross_profit/gross_loss : gross_profit;
   //мат. ожидание
   expected_payoff=net_profit/total;
   double initial_deposit=AccountInfoDouble(ACCOUNT_BALANCE)-net_profit;
   absolute_drawdown=MathAbs(min_peak); 
   //просадки
   maximal_drawdown_pp=(initial_deposit!=0) ?(maximal_drawdown/initial_deposit)*100.0 : 0;
   ralative_drawdown_pp=((max_peak+initial_deposit)!=0) ?(ralative_drawdown/(max_peak+initial_deposit))*100.0 : 0;
   
   //процент прибыльных и убыточных сделок
   profit_trades_pp=((double)profit_trades/total)*100.0;
   loss_trades_pp=((double)loss_trades/total)*100.0;
   
   //средн. прибыльная и убыточная сделки
   average_profit_trade=(profit_trades>0) ? gross_profit/profit_trades : 0;
   average_loss_trade=(loss_trades>0) ? gross_loss/loss_trades : 0;
   
   //макс. непрерывный убыток и прибыль
   if(maximum_consecutive_losses<max_cons_losses || 
      (maximum_consecutive_losses==max_cons_losses && maximum_consecutive_losses_usd>max_cons_losses_usd))
     {
      maximum_consecutive_losses=max_cons_losses;
      maximum_consecutive_losses_usd=max_cons_losses_usd;
     }
   if(maximum_consecutive_loss_usd>max_cons_losses_usd || 
      (maximum_consecutive_loss_usd==max_cons_losses_usd && maximum_consecutive_losses<max_cons_losses))
     {
      maximum_consecutive_loss=max_cons_losses;
      maximum_consecutive_loss_usd=max_cons_losses_usd;
     }

   if(maximum_consecutive_wins<max_cons_wins || 
      (maximum_consecutive_wins==max_cons_wins && maximum_consecutive_wins_usd<max_cons_wins_usd))
     {
      maximum_consecutive_wins=max_cons_wins;
      maximum_consecutive_wins_usd=max_cons_wins_usd;
     }
   if(maximum_consecutive_profit_usd<max_cons_wins_usd || 
      (maximum_consecutive_profit_usd==max_cons_wins_usd && maximum_consecutive_profit<max_cons_wins))
     {
      maximum_consecutive_profit=max_cons_wins;
      maximum_consecutive_profit_usd=max_cons_wins_usd;
     }
   //средний убыток и прибыль
   if(max_cons_losses>0) {avg_loss+=max_cons_losses; avg_loss_cnt++;}
   if(max_cons_wins>0) {avg_win+=max_cons_wins; avg_win_cnt++;}
   average_consecutive_wins=(avg_win_cnt>0) ? round((double)avg_win/avg_win_cnt) : 0;
   average_consecutive_losses=(avg_loss_cnt>0) ? round((double)avg_loss/avg_loss_cnt) : 0;
   
   //кол-во прибыльных длинных и коротких позиций
   long_positions_won=(long_positions>0) ?((double)long_positions_won/long_positions)*100.0 : 0;
   short_positions_won=(short_positions>0) ?((double)short_positions_won/short_positions)*100.0 : 0;
  }


5. Использование Google Chart API для создания графика баланса

Google Chart API позволяет разработчикам создавать диаграммы различного типа на лету. Google Chart API хранится по ссылке на ресурс (URL) на веб-серверах компании Google и при получении правильно форматированной ссылки (URL) возвращает диаграмму в виде изображения.

Характеристики диаграммы (цвета, заголовки, оси, точки на графике и т.д.) указываются посредством строки запроса ссылки (URL). Полученное изображение может быть сохранено в файловой системе,  либо в базе данных. Самым приятным аспектом является то, что Google Chart API бесплатный и не требует наличия никакой учетной записи и прохождения процесса регистрации. 

Метод GetChart() получает график с сервера Google и сохраняет его на диске:

#import "PNG_to_BMP.dll"//импорт DLL с функцией конвертирования PNG изображений в BMP
bool Convert_PNG(string src,string dst);
#import

#import "wininet.dll"//импорт DLL с функциями для работы с интернет
int InternetAttemptConnect(int x);
int InternetOpenW(string sAgent,int lAccessType,
                  string sProxyName="",string sProxyBypass="",
                  int lFlags=0);
int InternetOpenUrlW(int hInternetSession,string sUrl,
                     string sHeaders="",int lHeadersLength=0,
                     int lFlags=0,int lContext=0);
int InternetReadFile(int hFile,char &sBuffer[],int lNumBytesToRead,
                     int &lNumberOfBytesRead[]);
int InternetCloseHandle(int hInet);
#import

//+------------------------------------------------------------------+
///Функция-член создания графика баланса
//+------------------------------------------------------------------+
void Board::GetChart(int X_size,int Y_size,string request,string file_name)
  {
   if(X_size<1 || Y_size<1) return; //слишком маленький!
   //попытаемся создать подключение
   int rv=InternetAttemptConnect(0);
   if(rv!=0) {Alert("Ошибка при вызове InternetAttemptConnect()"); return;}
   //инициализируем структуры
   int hInternetSession=InternetOpenW("Microsoft Internet Explorer", 0, "", "", 0);
   if(hInternetSession<=0) {Alert("Ошибка при вызове InternetOpenW()"); return;}
   //отправим запрос
   int hURL=InternetOpenUrlW(hInternetSession, request, "", 0, 0, 0);
   if(hURL<=0) Alert("Ошибка при вызове InternetOpenUrlW()");
   //файл, куда считаем результат
   CFileBin chart_file;
   //создадим его
   chart_file.Open(file_name+".png",FILE_BIN|FILE_WRITE);
   int dwBytesRead[1]; //кол-во считанных данных
   char readed[1000];  //сами данные
   //читаем данные, полученные от сервера после запроса
   while(InternetReadFile(hURL,readed,1000,dwBytesRead))
     {
      if(dwBytesRead[0]<=0) break; //данных нет - выходим
      chart_file.WriteCharArray(readed,0,dwBytesRead[0]); //пишем данные в файл
     }
   InternetCloseHandle(hInternetSession);//закроем соединение
   chart_file.Close();//и файл
   //******************************
   //подготовим пути для конвертера 
   CString src;
   src.Assign(TerminalInfoString(TERMINAL_PATH));
   src.Append("\MQL5\Files\\"+file_name+".png");
   src.Replace("\\","\\\\");
   CString dst;
   dst.Assign(TerminalInfoString(TERMINAL_PATH));
   dst.Append("\MQL5\Images\\"+file_name+".bmp");
   dst.Replace("\\","\\\\");
   //конвертируем файл
   if(!Convert_PNG(src.Str(),dst.Str())) Alert("Ошибка при вызове Convert_PNG()");
  }

Подробности работы с интернет средствами API Windows и MQL5 вы можете получить из статьи Использование WinInet.dll для обмена данными между терминалами через Интернет, я на этом останавливаться не буду. Импортируемая функция Convert_PNG() была написана мной для конвертирования  PNG изображений в BMP. Это необходимость т.к. Google Chart возвращает график в формате PNG или GIF, а объект "графическая метка" принимает только BMP изображения. Код соответствующих функций библиотеки PNG_to_BMP.dll вы можете найти в архиве DLL_Sources.zip.

Также в этой функции показаны примеры работы со строками и файлами средствами Стандартной библиотеки. Методы класса CString позволяют проводить те же операции, что и строковые функции. Класс CFile является базовым для классов CFileBin и CFileTxt, с их помощью можно производить чтение и запись в бинарные и текстовые файлы соответственно. Методы схожи с функциями для работы с файлами.

Напоследок опишем функцию CreateGoogleRequest() - она создает запрос из данных по балансу:

//+------------------------------------------------------------------+
///Функция-член создания запрса для сервера Google Charts
//+------------------------------------------------------------------+
string Board::CreateGoogleRequest(int X_size,int Y_size,bool type)
  {
   if(X_size>1000) X_size=1000; //проверим размеры графика
   if(Y_size>1000) Y_size=300;  //чтобы не были слишком большими
   if(X_size<1) X_size=1;       //и маленькими
   if(Y_size<1) Y_size=1;
   if(X_size*Y_size>300000) {X_size=1000; Y_size=300;}//и подходили по площади
   CString res; //строка с результатом
   if(type) //формируем запрос для графика баланса
     {
      //соберем запрос
      res.Assign("http://chart.apis.google.com/chart?cht=lc&chs=");
      res.Append(IntegerToString(X_size));
      res.Append("x");
      res.Append(IntegerToString(Y_size));
      res.Append("&chd=t:");
      for(int i=0;i<ChartData.Total();i++)
         res.Append(DoubleToString(ChartData.At(i),2)+",");
      res.TrimRight(",");
      //сортируем массив
      ChartData.Sort();
      res.Append("&chxt=x,r&chxr=0,0,");
      res.Append(IntegerToString(ChartData.Total()));
      res.Append("|1,");
      res.Append(DoubleToString(ChartData.At(0),2)+",");
      res.Append(DoubleToString(ChartData.At(ChartData.Total()-1),2));
      res.Append("&chg=10,10&chds=");
      res.Append(DoubleToString(ChartData.At(0),2)+",");
      res.Append(DoubleToString(ChartData.At(ChartData.Total()-1),2));
     }
   else //формируем запрос для круговой диаграммы
     {
      //соберем запрос
      res.Assign("http://chart.apis.google.com/chart?cht=p3&chs=");
      res.Append(IntegerToString(X_size));
      res.Append("x");
      res.Append(IntegerToString(Y_size));
      res.Append("&chd=t:");
      for(int i=0;i<pie_data.Total();i++)
         res.Append(IntegerToString(pie_data.Get_val(i))+",");
      res.TrimRight(",");
      res.Append("&chdl=");
      for(int i=0;i<pie_data.Total();i++)
         res.Append(pie_data.Get_symb(i)+"|");
      res.TrimRight("|");
      res.Append("&chco=");
      int cnt=0;
      for(int i=0;i<pie_data.Total();i++)
        {
         if(cnt>11) cnt=0;
         res.Append(colors[cnt]+"|");
         cnt++;
        }
      res.TrimRight("|");
     }
   return res.Str(); //и вернем его
  }

Заметьте, что запросы для графика баланса и круговой диаграммы собираются отдельно. Метод Append добавляет строку в конец существующей, а метод TrimRight позволяет удалить лишние указанные символы конца строки.


6. Окончательная сборка и тестирование

Класс готов, протестируем его. Начнем с OnInit() индикатора:

Board *tablo;   //указатель на объект "табло"
int prev_x_size=0,prev_y_size=0,prev_deals=0;
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   //установим короткое имя индикатора
   IndicatorSetString(INDICATOR_SHORTNAME,"IT");
   //запустим таймер
   EventSetTimer(1); 
   //создадим экземпляр объекта
   tablo=new Board;
   //и его интерфейс 
   tablo.CreateInterface(); 
   prev_deals=HistoryDealsTotal(); //текущее кол-во сделок
   //текущие размеры окна
   prev_x_size=ChartGetInteger(0,CHART_WIDTH_IN_PIXELS); 
   prev_y_size=ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
//---
   return(0);
  }

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

Сразу-же пропишем функцию OnDeinit(), там удалим объект (что автоматически приведет к вызову деструктора), и остановим таймер:

void OnDeinit(const int reason)
{
   EventKillTimer(); //остановим таймер
   delete tablo;    //и удалим табло
}

Функция OnCalculate() будет потиково следить за поступлением новых закрытых сделок и обновлять табло, если это произойдет:

int OnCalculate(const int rates_total,
              const int prev_calculated,
              const datetime& time[],
              const double& open[],
              const double& high[],
              const double& low[],
              const double& close[],
              const long& tick_volume[],
              const long& volume[],
              const int& spread[])
  {
//---
      HistorySelect(0, TimeCurrent());//подготовим историю
      int deals=HistoryDealsTotal();
      //если изменилось кол-во сделок - обновим табло
      if(deals!=prev_deals) tablo.Refresh(); 
      prev_deals=deals;
//--- return value of prev_calculated for next call
   return(rates_total);
  }

Функция OnTimer() следит за изменением размеров окна, и подгоняет размеры табло в случае необходимости, также следит за поступлением сделок как и OnCalculate(), на случай, если тики приходят реже, чем 1 в секунду.  

void OnTimer()
{
   int x_size=ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
   int y_size=ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
   //если изменились размеры окна - обновим табло
   if(x_size!=prev_x_size || y_size!=prev_y_size) tablo.Refresh();
   prev_x_size=x_size;   
   prev_y_size=y_size;  
   //если изменилось кол-во сделок - обновим табло
   HistorySelect(0, TimeCurrent());
   int deals=HistoryDealsTotal();
   if(deals!=prev_deals) tablo.Refresh();
   prev_deals=deals;
}

Компилируем и запускаем индикатор:

Рисунок 3. Окончательный вид табло

Рисунок 3. Окончательный вид табло


Заключение

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

Удачи. 

Для начала работы распаковать архив MQL5.rar в папку терминала, и разрешить использование DLL. В архиве DLL_Sources.zip находятся исходные коды библиотек String_Metrics.dll и PNG_to_BMP.dll, они были написаны мною в среде Borland C++ Builder с установленным GDI. 

 
Прикрепленные файлы |
infoboard.zip (200.67 KB)
dll_sources.zip (1.62 KB)
infoboard-doc.zip (757.48 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (20)
Евгений
Евгений | 4 июн. 2010 в 11:50
sergey1294:

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


а вот так после перезагрузки терминала



попробуйте удалить файлы с картинками из папок Files и Images, и посмотреть, появляются ли они снова при запуске индикатора

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

Sergey Gritsay
Sergey Gritsay | 4 июн. 2010 в 12:26
space_cowboy:

попробуйте удалить файлы с картинками из папок Files и Images, и посмотреть, появляются ли они снова при запуске индикатора

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

удалил , теперь выдает ошибку 2010.06.04 12:20:40    InfoBoard (EURUSD,M5)    Ошибка при вызове Convert_PNG()


файлы появились снова в директории C:\Users\user\AppData\Roaming\MetaQuotes\Terminal\E885B7972A0C831E41EB39B7A9849BBC\MQL5\Files

Евгений
Евгений | 4 июн. 2010 в 12:53

попробуйте заменить в функции void Board::GetChart(int X_size,int Y_size,string request,string file_name)

//подготовим пути для конвертера 
   CString src;
   src.Assign(TerminalInfoString(TERMINAL_PATH));
   src.Append("\MQL5\Files\\"+file_name+".png");
   src.Replace("\\","\\\\");
   CString dst;
   dst.Assign(TerminalInfoString(TERMINAL_PATH));
   dst.Append("\MQL5\Images\\"+file_name+".bmp");
   dst.Replace("\\","\\\\");

на

//подготовим пути для конвертера 
   CString src;
   src.Assign(TerminalInfoString(TERMINAL_DATA_PATH));
   src.Append("\MQL5\Files\\"+file_name+".png");
   src.Replace("\\","\\\\");
   CString dst;
   dst.Assign(TerminalInfoString(TERMINAL_DATA_PATH));
   dst.Append("\MQL5\Images\\"+file_name+".bmp");
   dst.Replace("\\","\\\\");

 

Sergey Gritsay
Sergey Gritsay | 4 июн. 2010 в 13:03
все заработало, спасибо!

Denis Kirichenko
Denis Kirichenko | 30 июн. 2011 в 16:43
Все классы библиотеки (кроме торговых) происходят от базового класса CObject.

А от какого класса происходит CTrade?

Смотрю в декларацию торговых классов и вижу:

class CTrade : public CObject
Новые возможности с MetaTrader 5 Новые возможности с MetaTrader 5
MetaTrader 4 завоевал популярность у трейдеров по всему миру, и казалось бы, нельзя желать большего. Высокая производительность и стабильность, широкие возможности по написанию индикаторов, экспертов и торгово-информационных систем, возможность выбора любого из нескольких сотен брокеров - вот те основные преимущества, которые выделяют этот терминал на фоне всех остальных. Но время не стоит на месте, и вот мы уже стоим перед выбором - MetaTrader 4 или MetaTrader 5. В этой статье мы опишем основные отличия терминала 5-го поколения от нынешнего фаворита.
Руководство по написанию DLL для MQL5 на Delphi Руководство по написанию DLL для MQL5 на Delphi
Статья рассматривает механизм написания модудя DLL на популярном языке программирования ObjectPascal в среде разработки Delphi. Изложенный в статье материал ориентирован в первую очередь на начинающих программистов, решающих задачи, выходящие за рамки встроенного языка программирования MQL5, путем подключения внешних DLL модулей.
Обработка торговых событий в эксперте при помощи функции OnTrade() Обработка торговых событий в эксперте при помощи функции OnTrade()
В 5-ой версии языка MQL появилась масса нововведений, в том числе работа с событиями различных типов (события таймера, торговые события, пользовательские и т.д.). Возможность обработки событий позволяет создавать совершенно новый тип программ для автоматического и полуавтоматического трейдинга. В этой статье мы рассмотрим торговые события и напишем для функции OnTrade() код, который будет обрабатывать событие Trade.
Генетические алгоритмы - это просто! Генетические алгоритмы - это просто!
В статье автор расскажет об эволюционных вычислениях с использованием генетического алгоритма собственной реализации. Будет показано на примерах функционирование алгоритма, даны практические рекомендации по его использованию.