Как анализировать сделки выбранного Сигнала на графике

Dmitriy Gizlyk | 24 мая, 2018

Содержание

Введение

В сервисе Сигналы постоянно появляются новые сигналы — платные и бесплатные. Команда MetaTrader позаботилась о том, чтобы сервисом можно было воспользоваться, не выходя из терминала. Остается только выбрать именно тот сигнал, который принесет максимальную прибыль при допустимых рисках. Эта проблема обсуждается давно. Уже предложен метод автоматического отбора сигналов по указанным критериям [1]. Но, как гласит народная мудрость, лучше один раз увидеть, чем сто раз услышать. В статье я предлагаю изучить и проанализировать историю сделок выбранного сигнала на графике инструмента. Возможно, этот подход позволит лучше понять стратегию совершения сделок и оценить риски. 

1. Формулируем цели предстоящей работы

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

Команда "Показать сделки на графике"

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

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

2. Собираем статистику сделок

2.1. Класс для хранения информации об ордере

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

class COrder : public CObject
  {
private:
   long                 l_Ticket;
   double               d_Lot;
   double               d_Price;
   ENUM_POSITION_TYPE   e_Type;
   ENUM_DEAL_ENTRY      e_Entry;
   datetime             dt_OrderTime;
   string               s_Symbol;
   
public:
                        COrder();
                       ~COrder();
   bool                 Create(string symbol, long ticket, double volume, double price, datetime time, ENUM_POSITION_TYPE type);
//---
   string               Symbol(void)   const {  return s_Symbol;     }
   long                 Ticket(void)   const {  return l_Ticket;     }
   double               Volume(void)   const {  return d_Lot;        }
   double               Price(void)    const {  return d_Price;      }
   datetime             Time(void)     const {  return dt_OrderTime; } 
   ENUM_POSITION_TYPE   Type(void)           {  return e_Type;       }
   ENUM_DEAL_ENTRY      DealEntry(void)const {  return e_Entry;      }
   void                 DealEntry(ENUM_DEAL_ENTRY value) {  e_Entry=value; }
//--- methods for working with files
   virtual bool         Save(const int file_handle);
   virtual bool         Load(const int file_handle);
//---
   //--- method of comparing the objects
   virtual int          Compare(const CObject *node,const int mode=0) const;
  };

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

Для сравнения двух ордеров нам потребуется переписать виртуальную функцию Compare. Это функция базового класса, предназначенная для сравнения двух объектов CObject. Поэтому в ее параметрах передается ссылка на объект CObject и метод сортировки. Свои ордера мы будем сортировать только в одном направлении (по возрастанию даты исполнения), поэтому параметр mode в коде функции использовать не будем. А вот для работы с объектом COrder, полученным по ссылке, нам нужно его сначала привести к соответствующему типу. После этого сравним даты полученного и текущего ордеров. Если полученный ордер старше — вернем "-1", если младше — "1". При равенстве дат исполнения ордеров функция вернет "0".

int COrder::Compare(const CObject *node,const int mode=0) const
  {
   const COrder *temp=GetPointer(node);
   if(temp.Time()>dt_OrderTime)
      return -1;
//---
   if(temp.Time()<dt_OrderTime)
      return 1;
//---
   return 0;
  }

2.2. Собираем информацию с графиков

Для работы с ордерами создадим класс COrdersCollection на базе класса CArrayObj. В нем будет собираться и обрабатываться информация. Для хранения данных объявим экземпляр объекта для непосредственной работы с конкретным ордером и массив для хранения списка используемых инструментов. Хранить массив ордеров будем с помощью функций базового класса.

class COrdersCollection : public CArrayObj
  {
private:
   COrder            *Temp;
   string            ar_Symbols[];
   
public:

                     COrdersCollection();
                    ~COrdersCollection();
//--- Инициализация
   bool              Create(void);
//--- Добавление ордера
   bool              Add(COrder *element);
//--- Доступ к данным
   int               Symbols(string &array[]);
   bool              GetPosition(const string symbol, const datetime time, double &volume, double &price, ENUM_POSITION_TYPE &type);
   datetime          FirstOrder(const string symbol=NULL);
   datetime          LastOrder(const string symbol=NULL);
//--- Получение тайм-серий
   bool              GetTimeSeries(const string symbol, const datetime start_time, const datetime end_time, const int direct,
                                   double &balance[], double &equity[], double &time[], double &profit, double &loss,int &long_trades, int &short_trades);
//---
   void              SetDealsEntry(void);
  };

Непосредственно за сбор данных отвечает функция Create. В теле метода организуем цикл по перебору всех открытых в терминале графиков. На каждом графике будем искать графические объекты типа OBJ_ARROW_BUY и OBJ_ARROW_SELL.

bool COrdersCollection::Create(void)
  {
   long chart=ChartFirst();
   while(chart>0)
     {
      int total_buy=ObjectsTotal(chart,0,OBJ_ARROW_BUY);
      int total_sell=ObjectsTotal(chart,0,OBJ_ARROW_SELL);
      if((total_buy+total_sell)<=0)
        {
         chart=ChartNext(chart);
         continue;
        }

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

      int symb=ArraySize(ar_Symbols);
      string symbol=ChartSymbol(chart);
      bool found=false;
      for(int i=0;(i<symb && !found);i++)
         if(ar_Symbols[i]==symbol)
           {
            found=true;
            symb=i;
            break;
           }
      if(!found)
        {
         if(ArrayResize(ar_Symbols,symb+1,10)<=0)
            return false;
         ar_Symbols[symb]=symbol;
        }

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

Наименование графического объекта

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

      int total=fmax(total_buy,total_sell);
      for(int i=0;i<total;i++)
        {
         if(i<total_buy)
           {
            string name=ObjectName(chart,i,0,OBJ_ARROW_BUY);
            datetime time=(datetime)ObjectGetInteger(chart,name,OBJPROP_TIME);
            StringTrimLeft(name);
            StringTrimRight(name);
            StringReplace(name,"#","");
            string split[];
            StringSplit(name,' ',split);
            Temp=new COrder;
            if(CheckPointer(Temp)!=POINTER_INVALID)
              {
               if(Temp.Create(ar_Symbols[symb],StringToInteger(split[1]),StringToDouble(split[3]),StringToDouble(split[6]),time,POSITION_TYPE_BUY))
                  Add(Temp);
              }
           }
//---
         if(i<total_sell)
           {
            string name=ObjectName(chart,i,0,OBJ_ARROW_SELL);
            datetime time=(datetime)ObjectGetInteger(chart,name,OBJPROP_TIME);
            StringTrimLeft(name);
            StringTrimRight(name);
            StringReplace(name,"#","");
            string split[];
            StringSplit(name,' ',split);
            Temp=new COrder;
            if(CheckPointer(Temp)!=POINTER_INVALID)
              {
               if(Temp.Create(ar_Symbols[symb],StringToInteger(split[1]),StringToDouble(split[3]),StringToDouble(split[6]),time,POSITION_TYPE_SELL))
                  Add(Temp);
              }
           }
        }
      chart=ChartNext(chart);
     }

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

   SetDealsEntry();
//---
   return true;
  }

Чтобы избежать дублирование сделок в нашей базе, перепишем функцию Add: добавим в нее проверку наличия ордера по тикету.

bool COrdersCollection::Add(COrder *element)
  {
   for(int i=0;i<m_data_total;i++)
     {
      Temp=m_data[i];
      if(Temp.Ticket()==element.Ticket())
         return true;
     }
//---
   return CArrayObj::Add(element);
  }

Чтобы расставить типы операций в сделках, создадим функцию SetDealsEntry. В ее начале вызовем функцию сортировки базового класса. Затем организуем цикл по перебору всех инструментов и сделок по каждому из них. Алгоритм определения типа операции прост. Если на момент операции не существует открытой позиции или она есть в том же направлении, что и сделка — значит, определяем операцию как вход в позицию. Если операция противоположна существующей позиции, то ее объем сначала используется для закрытия открытой позиции, а остаток открывает новую позицию (по аналогии с неттинговой системой MetaTrader 5).

COrdersCollection::SetDealsEntry(void)
  {
   Sort(0);
//---
   int symbols=ArraySize(ar_Symbols);
   for(int symb=0;symb<symbols;symb++)
     {
      double volume=0;
      ENUM_POSITION_TYPE type=-1;
      for(int ord=0;ord<m_data_total;ord++)
        {
         Temp=m_data[ord];
         if(Temp.Symbol()!=ar_Symbols[symb])
            continue;
//---
         if(volume==0 || type==Temp.Type())
           {
            Temp.DealEntry(DEAL_ENTRY_IN);
            volume=NormalizeDouble(volume+Temp.Volume(),2);
            type=Temp.Type();
           }
         else
           {
            if(volume>=Temp.Volume())
              {
               Temp.DealEntry(DEAL_ENTRY_OUT);
               volume=NormalizeDouble(volume-Temp.Volume(),2);
              }
            else
              {
               Temp.DealEntry(DEAL_ENTRY_INOUT);
               volume=NormalizeDouble(volume-Temp.Volume(),2);
               type=Temp.Type();
              }
           }
        }
     }
  }

2.3. Создаем таймсерии баланса и средств по каждому инструменту

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

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

Забегая вперед, сфокусирую ваше внимание на том, что массив для таймсерии временных меток определен как double. Эта небольшая хитрость — вынужденная мера. Далее графики баланса и средств мы будем строить с использованием стандартного класса CGraphic, который принимает только массивы типа double.

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

bool COrdersCollection::GetTimeSeries(const string symbol,const datetime start_time,const datetime end_time,const int direct,double &balance[],double &equity[], double &time[], double &profit, double &loss,int &long_trades, int &short_trades)
  {
   profit=loss=0;
   long_trades=short_trades=0;
//---
   if(symbol==NULL)
      return false;
//---
   double tick_value=SymbolInfoDouble(symbol,SYMBOL_TRADE_TICK_VALUE)/SymbolInfoDouble(symbol,SYMBOL_POINT);
   if(tick_value==0)
      return false;

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

   ENUM_TIMEFRAMES timeframe=PERIOD_M5;
//---
   double volume=0;
   double price=0;
   ENUM_POSITION_TYPE type=-1;
   int order=-1;
//---
   MqlRates rates[];
   int count=0;
   count=CopyRates(symbol,timeframe,start_time,end_time,rates);
   if(count<=0 && !ReloadHistory)
     {
      //--- send notification
      ReloadHistory=EventChartCustom(CONTROLS_SELF_MESSAGE,1222,0,0.0,symbol);
      return false;
     }

После загрузки котировок приведем размер массивов таймсерий в соответствие с размером загруженных котировок.

   if(ArrayResize(balance,count)<count || ArrayResize(equity,count)<count || ArrayResize(time,count)<count)
      return false;
   ArrayInitialize(balance,0);

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

   do
     {
      order++;
      if(order<m_data_total)
         Temp=m_data[order];
      else
         Temp=NULL;
     }
   while(CheckPointer(Temp)==POINTER_INVALID && order<m_data_total);
//---
   for(int i=0;i<count;i++)
     {
      while(order<m_data_total && Temp.Time()<(rates[i].time+PeriodSeconds(timeframe)))
        {
         if(Temp.Symbol()!=symbol)
           {
            do
              {
               order++;
               if(order<m_data_total)
                  Temp=m_data[order];
               else
                  Temp=NULL;
              }
            while(CheckPointer(Temp)==POINTER_INVALID && order<m_data_total);
            continue;
           }
//---
         if(Temp!=NULL)
           {
            if(type==Temp.Type())
              {
               price=volume*price+Temp.Volume()*Temp.Price();
               volume+=Temp.Volume();
               price=price/volume;
               switch(type)
                 {
                  case POSITION_TYPE_BUY:
                    long_trades++;
                    break;
                  case POSITION_TYPE_SELL:
                    short_trades++;
                    break;
                 }
              } 
            else
              {
               if(i>0 && (direct<0 || direct==type))
                 {
                  double temp=(Temp.Price()-price)*tick_value*(type==POSITION_TYPE_BUY ? 1 : -1)*MathMin(volume,Temp.Volume());
                  balance[i]+=temp;
                  if(temp>=0)
                     profit+=temp;
                  else
                     loss+=temp;
                 }
               volume-=Temp.Volume();
               if(volume<0)
                 {
                  volume=MathAbs(volume);
                  price=Temp.Price();
                  type=Temp.Type();
                  switch(type)
                    {
                     case POSITION_TYPE_BUY:
                       long_trades++;
                       break;
                     case POSITION_TYPE_SELL:
                       short_trades++;
                       break;
                    }
                 }
              }
           }
         do
           {
            order++;
            if(order<m_data_total)
               Temp=m_data[order];
            else
               Temp=NULL;
           }
         while(CheckPointer(Temp)==POINTER_INVALID && order<m_data_total);
        }
      if(i>0)
        {
         balance[i]+=balance[i-1];
        }
      if(volume>0 && (direct<0 || direct==type))
         equity[i]=(rates[i].close-price)*tick_value*(type==POSITION_TYPE_BUY ? 1 : -1)*MathMin(volume,(Temp!=NULL ? Temp.Volume(): DBL_MAX));
      else
         equity[i]=0;
      equity[i]+=balance[i];
      time[i]=(double)rates[i].time;
     }
//---
   return true;
  }

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

3. Добавляем графическую оболочку

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

Графический интерфейс

Графический интерфейс строим в классе CStatisticsPanel (это наследник класса CAppDialog). Для выбора дат начала и окончания анализа воспользуемся экземплярами класса CDatePicker. Чек-боксы для выбора отображаемой информации объединим в 3 группы:

3.1. Создание графической панели

Для создания блоков чек-боксов воспользуемся экземплярами класса CCheckGroup. Текстовую статистику выведем с помощью экземпляров класса CLabel. А графики будем строить при помощи экземпляра класса CGraphic. И конечно, для доступа к нашей статистике ордеров объявим экземпляр класса COrdersCollection.

class CStatisticsPanel : public CAppDialog
  {
private:
   CDatePicker       StartDate;
   CDatePicker       EndDate;
   CLabel            Date;
   CGraphic          Graphic;
   CLabel            ShowLabel;
   CCheckGroup       Symbols;
   CCheckGroup       BalEquit;
   CCheckGroup       Deals;
   string            ar_Symbols[];
   CLabel            TotalProfit;
   CLabel            TotalProfitVal;
   CLabel            GrossProfit;
   CLabel            GrossProfitVal;
   CLabel            GrossLoss;
   CLabel            GrossLossVal;
   CLabel            TotalTrades;
   CLabel            TotalTradesVal;
   CLabel            LongTrades;
   CLabel            LongTradesVal;
   CLabel            ShortTrades;
   CLabel            ShortTradesVal;
   //---
   COrdersCollection Orders;

public:
                     CStatisticsPanel();
                    ~CStatisticsPanel();
   //--- main application dialog creation and destroy
   virtual bool      Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2);
   virtual void      Destroy(const int reason=REASON_PROGRAM);
   //--- chart event handler
   virtual bool      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam);

protected:
   virtual bool      CreateLineSelector(const string name,const int x1,const int y1,const int x2,const int y2);
   virtual bool      CreateDealsSelector(const string name,const int x1,const int y1,const int x2,const int y2);
   virtual bool      CreateCheckGroup(const string name,const int x1,const int y1,const int x2,const int y2);
   virtual bool      CreateGraphic(const string name,const int x1,const int y1,const int x2,const int y2);
   //---
   virtual void      Maximize(void);
   virtual void      Minimize(void);
   //---
   virtual bool      UpdateChart(void);

  };

В методе Create сначала вызовем соответствующий метод родительского класса, а затем расставим все объекты по своим местам и проинициализируем экземпляр класса коллекции ордеров. После инициализации каждого элемента не забываем присвоить начальные значения и добавить объект в коллекцию управляющих элементов. Подробно о работе с базовым классом рассказано в статьях [2] и [3], поэтому не буду подробно останавливаться на описании метода, приведу лишь его код.

bool CStatisticsPanel::Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2)
  {
   if(!CAppDialog::Create(chart,name,subwin,x1,y1,x2,y2))
      return false;
//---
   if(!TotalProfit.Create(m_chart_id,m_name+"Total Profit",m_subwin,5,80,115,95))
      return false;
   if(!TotalProfit.Text("Total Profit"))
      return false;
   if(!Add(TotalProfit))
      return false;
//---
   if(!TotalProfitVal.Create(m_chart_id,m_name+"Total Profit Value",m_subwin,135,80,250,95))
      return false;
   if(!TotalProfitVal.Text("0"))
      return false;
   if(!Add(TotalProfitVal))
      return false;
//---
   if(!GrossProfit.Create(m_chart_id,m_name+"Gross Profit",m_subwin,5,100,115,115))
      return false;
   if(!GrossProfit.Text("Gross Profit"))
      return false;
   if(!Add(GrossProfit))
      return false;
//---
   if(!GrossProfitVal.Create(m_chart_id,m_name+"Gross Profit Value",m_subwin,135,100,250,115))
      return false;
   if(!GrossProfitVal.Text("0"))
      return false;
   if(!Add(GrossProfitVal))
      return false;
//---
   if(!GrossLoss.Create(m_chart_id,m_name+"Gross Loss",m_subwin,5,120,115,135))
      return false;
   if(!GrossLoss.Text("Gross Loss"))
      return false;
   if(!Add(GrossLoss))
      return false;
//---
   if(!GrossLossVal.Create(m_chart_id,m_name+"Gross Loss Value",m_subwin,135,120,250,135))
      return false;
   if(!GrossLossVal.Text("0"))
      return false;
   if(!Add(GrossLossVal))
      return false;
//---
   if(!TotalTrades.Create(m_chart_id,m_name+"Total Trades",m_subwin,5,150,115,165))
      return false;
   if(!TotalTrades.Text("Total Trades"))
      return false;
   if(!Add(TotalTrades))
      return false;
//---
   if(!TotalTradesVal.Create(m_chart_id,m_name+"Total Trades Value",m_subwin,135,150,250,165))
      return false;
   if(!TotalTradesVal.Text("0"))
      return false;
   if(!Add(TotalTradesVal))
      return false;
//---
   if(!LongTrades.Create(m_chart_id,m_name+"Long Trades",m_subwin,5,170,115,185))
      return false;
   if(!LongTrades.Text("Long Trades"))
      return false;
   if(!Add(LongTrades))
      return false;
//---
   if(!LongTradesVal.Create(m_chart_id,m_name+"Long Trades Value",m_subwin,135,170,250,185))
      return false;
   if(!LongTradesVal.Text("0"))
      return false;
   if(!Add(LongTradesVal))
      return false;
//---
   if(!ShortTrades.Create(m_chart_id,m_name+"Short Trades",m_subwin,5,190,115,215))
      return false;
   if(!ShortTrades.Text("Short Trades"))
      return false;
   if(!Add(ShortTrades))
      return false;
//---
   if(!ShortTradesVal.Create(m_chart_id,m_name+"Short Trades Value",m_subwin,135,190,250,215))
      return false;
   if(!ShortTradesVal.Text("0"))
      return false;
   if(!Add(ShortTradesVal))
      return false;
//---
   if(!Orders.Create())
      return false;
//---
   if(!ShowLabel.Create(m_chart_id,m_name+"Show Selector",m_subwin,285,8,360,28))
      return false;
   if(!ShowLabel.Text("Symbols"))
      return false;
   if(!Add(ShowLabel))
      return false;
   if(!CreateLineSelector("LineSelector",2,30,115,70))
      return false;
   if(!CreateDealsSelector("DealsSelector",135,30,250,70))
      return false;
   if(!CreateCheckGroup("CheckGroup",260,30,360,ClientAreaHeight()-5))
      return false;
//---
   if(!Date.Create(m_chart_id,m_name+"->",m_subwin,118,8,133,28))
      return false;
   if(!Date.Text("->"))
      return false;
   if(!Add(Date))
      return false;
//---
   if(!StartDate.Create(m_chart_id,m_name+"StartDate",m_subwin,5,5,115,28))
      return false;
   if(!Add(StartDate))
      return false;
//---
   if(!EndDate.Create(m_chart_id,m_name+"EndDate",m_subwin,135,5,250,28))
      return false;
   if(!Add(EndDate))
      return false;
//---
   StartDate.Value(Orders.FirstOrder());
   EndDate.Value(Orders.LastOrder());
//---
   if(!CreateGraphic("Chraphic",370,5,ClientAreaWidth()-5,ClientAreaHeight()-5))
      return false;
//---
   UpdateChart();
//---
   return true;
  }

Наблюдательный читатель может заметить, что создаваемый график не добавлен в коллекцию управляющих элементов. Это связано с тем, что объект CGraphic не наследуется от класса CWnd, а в коллекцию можно добавить только объекты-наследники CWnd. Поэтому нам предстоит переписать функции сворачивания и разворачивания панели.
После инициализации всех объектов вызовем функцию обновления графика.

3.2. Функция создания графика

Остановимся ненадолго на функции создания графика CreateGraphic. В параметрах она получает имя создаваемого объекта и координаты расположения графика. В начале функции непосредственно создается график (вызов функции Create класса CGraphic). Поскольку класс CGraphic не наследуется от класса CWnd и мы не сможем его добавить в коллекцию управляющих элементов панели, то координаты графика сразу смещаем в соответствии с расположением клиентской области.

bool CStatisticsPanel::CreateGraphic(const string name,const int x1,const int y1,const int x2,const int y2)
  {
   if(!Graphic.Create(m_chart_id,m_name+name,m_subwin,ClientAreaLeft()+x1,ClientAreaTop()+y1,ClientAreaLeft()+x2,ClientAreaTop()+y2))
      return false;

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

   int total=Orders.Symbols(ar_Symbols);
   CColorGenerator ColorGenerator;
   double array[];
   ArrayFree(array);
   for(int i=0;i<total;i++)
     {
      //---
      CCurve *curve=Graphic.CurveAdd(array,array,ColorGenerator.Next(),CURVE_LINES,ar_Symbols[i]+" Balance");
      curve.Visible(false);
      curve=Graphic.CurveAdd(array,array,ColorGenerator.Next(),CURVE_LINES,ar_Symbols[i]+" Equity");
      curve.Visible(false);
     }

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

   CAxis *axis=Graphic.XAxis();
   axis.AutoScale(false);
   axis.Type(AXIS_TYPE_DATETIME);
   axis.ValuesDateTimeMode(TIME_DATE);
   Graphic.HistorySymbolSize(20);
   Graphic.HistoryNameSize(10);
   Graphic.HistoryNameWidth(60);
   Graphic.CurvePlotAll();
   Graphic.Update();
//---
   return true;
  }

3.3. Метод обновления графика и статистических данных

Обновлять данные о сигнале будем методом UpdateChart. В начале функции подготовим переменные и массивы для сбора данных.

bool CStatisticsPanel::UpdateChart(void)
  {
   double balance[];
   double equity[];
   double time[];
   double total_profit=0, total_loss=0;
   int total_long=0, total_short=0;
   CCurve *Balance, *Equity;

Затем получим даты начала и окончания анализируемого периода.

   datetime start=StartDate.Value();
   datetime end=EndDate.Value();

Проверим отметки на показ статистики длинных и коротких позиций.

   int deals=-2;
   if(Deals.Check(0))
      deals=(Deals.Check(1) ? -1 : POSITION_TYPE_BUY);
   else
      deals=(Deals.Check(1) ? POSITION_TYPE_SELL : -2);

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

   int total=ArraySize(ar_Symbols);
   for(int i=0;i<total;i++)
     {
      Balance  =  Graphic.CurveGetByIndex(i*2);
      Equity   =  Graphic.CurveGetByIndex(i*2+1);
      double profit,loss;
      int long_trades, short_trades;
      if(deals>-2 && Symbols.Check(i) && Orders.GetTimeSeries(ar_Symbols[i],start,end,deals,balance,equity,time,profit,loss,long_trades,short_trades))
        {
         if(BalEquit.Check(0))
           {
            Balance.Update(time,balance);
            Balance.Visible(true);
           }
         else
            Balance.Visible(false);
         if(BalEquit.Check(1))
           {
            Equity.Update(time,equity);
            Equity.Visible(true);
           }
         else
            Equity.Visible(false);
         total_profit+=profit;
         total_loss+=loss;
         total_long+=long_trades;
         total_short+=short_trades;
        }
      else
        {
         Balance.Visible(false);
         Equity.Visible(false);
        }
     }

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

   CAxis *axis=Graphic.XAxis();
   axis.Min((double)start);
   axis.Max((double)end);
   axis.DefaultStep((end-start)/5);
   if(!Graphic.Redraw(true))
      return false;
   Graphic.Update();

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

   if(!TotalProfitVal.Text(DoubleToString(total_profit+total_loss,2)))
      return false;
   if(!GrossProfitVal.Text(DoubleToString(total_profit,2)))
      return false;
   if(!GrossLossVal.Text(DoubleToString(total_loss,2)))
      return false;
   if(!TotalTradesVal.Text(IntegerToString(total_long+total_short)))
      return false;
   if(!LongTradesVal.Text(IntegerToString(total_long)))
      return false;
   if(!ShortTradesVal.Text(IntegerToString(total_short)))
      return false;
//---
   return true;
  }

3.4. "Оживление" панели

Чтобы "оживить" панель, нам нужно построить обработчик событий действий с объектами. Какие же возможные события программе предстоит обрабатывать?

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

EVENT_MAP_BEGIN(CStatisticsPanel)
   ON_EVENT(ON_CHANGE,Symbols,UpdateChart)
   ON_EVENT(ON_CHANGE,BalEquit,UpdateChart)
   ON_EVENT(ON_CHANGE,Deals,UpdateChart)
   ON_EVENT(ON_CHANGE,StartDate,UpdateChart)
   ON_EVENT(ON_CHANGE,EndDate,UpdateChart)
   ON_NO_ID_EVENT(1222,UpdateChart)
EVENT_MAP_END(CAppDialog)

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

4. Создание индикатора для анализа сигнала

Всё вышеописанное я предлагаю объединить в форме индикатора. Это позволит создать графическую панель в подокне, не затронув сам график.

Весь функционал нашей программы скрыт в классе CStatisticsPanel. Значит, для создания индикатора достаточно создать экземпляр этого класса в нашей программе. Инициализируем класс в функции OnInit.

int OnInit()
  {
//---
   long chart=ChartID();
   int subwin=ChartWindowFind();
   IndicatorSetString(INDICATOR_SHORTNAME,"Signal Statistics");
   ReloadHistory=false;
//---
   Dialog=new CStatisticsPanel;
   if(CheckPointer(Dialog)==POINTER_INVALID)
     {
      ChartIndicatorDelete(chart,subwin,"Signal Statistics");
      return INIT_FAILED;
     }
   if(!Dialog.Create(chart,"Signal Statistics",subwin,0,0,0,250))
     {
      ChartIndicatorDelete(chart,subwin,"Signal Statistics");
      return INIT_FAILED;
     }
   if(!Dialog.Run())
     {
      ChartIndicatorDelete(chart,subwin,"Signal Statistics");
      return INIT_FAILED;
     }
//---
   return(INIT_SUCCEEDED);
  }

 Функцию OnCalculate оставим пустой, поскольку программа не будет реагировать на наступление очередного тика. Остается лишь добавить вызов соответствующих методов в функции OnDeinit и OnChartEvent.

void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//---
   Dialog.ChartEvent(id,lparam,dparam,sparam);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   Dialog.Destroy(reason);
   delete Dialog;
  }

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

Пример работы индикатора

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

Заключение

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

Ссылки

  1. Автоматический подбор перспективных сигналов
  2. Как создать графическую панель любой сложности и как это работает
  3. Улучшаем работу с панелями: добавляем прозрачность, меняем цвет фона и наследуемся от CAppDialog/CWndClient

Программы, используемые в статье:

#
 Имя
Тип 
Описание 
1 Order.mqh  Библиотека класса  Класс для хранения информации о сделке
2 OrdersCollection.mqh  Библиотека класса  Класс коллекции сделок
3 StatisticsPanel.mqh  Библиотека класса  Класс графического интерфейса
4 SignalStatistics.mq5  Индикатор  Код индикатора для анализа сделок