График PairPlot на основе CGraphic для анализа зависимостей между массивами данных (таймсериями)

Dmitriy Gizlyk | 1 августа, 2018

Содержание

Введение

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

На просторах Интернета можно найти немало информации об анализе изменения стоимости валюты в различных валютных парах и поиске корреляции между различными валютными парами. И на страницах этого сайта публиковались статьи о торговле корзинами валютных пар [1, 2, 3]. Тем не менее, вопрос анализа зависимостей временных рядов остается открытым. В этой статье я предлагаю построить инструмент для графического анализа зависимостей временных рядов, что позволит визуализировать наличие зависимостей между временными рядами котировок анализируемых валютных пар.


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

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

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

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

Макет


2. Создание базовых классов

2.1. "Основа"

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

Создание нашего инструмента начнем с подготовки основы для построения графиков. В базовой поставке MetaTrader 5 имеется класс CGraphic, предназначенный для построения научных графиков. Подробно о данном классе рассказывается в статье [4]. Именно его мы и возьмем за основу для построения наших графиков. Создадим базовый класс CPlotBase и назначим его наследником стандартного класса CGraphic. В данном классе мы создадим методы для создания полотна графика. Таких методов будет два, один для построения квадратного поля графика с заданным размером стороны и второй для построения прямоугольной области по заданным координатам. Также добавим методы для возможности вывода произвольного текста по сторонам графика (они нам помогут в отображении наименований инструментов). Добавим метод для изменения цвета отображения на графике таймсерии.

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

class CPlotBase : public CGraphic
  {
protected:
   long              m_chart_id;                // chart ID
   int               m_subwin;                  // chart subwindow

public:
                     CPlotBase();
                    ~CPlotBase();
//--- Create of object
   virtual bool      Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int size);
   virtual bool      Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2);
//--- Change color of timeserie's label
   virtual bool      SetTimeseriesColor(uint clr, uint timeserie=0);
//--- Add text to chart
   virtual void      TextUp(string text, uint clr);
   virtual void      TextDown(string text, uint clr);
   virtual void      TextLeft(string text, uint clr);
   virtual void      TextRight(string text, uint clr);
//--- geometry
   virtual bool      Shift(const int dx,const int dy);
//--- state
   virtual bool      Show(void);
   virtual bool      Hide(void);
  };

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

CPlotBase::CPlotBase()
  {
   HistoryNameWidth(0);
   HistorySymbolSize(0);
   m_x.MaxLabels(3);
   m_y.MaxLabels(3);
  }

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

2.2. График рассеяния

Следующим шагом создадим класс CScatter для отображения графика рассеяния. Этот класс будет содержать только два метода создания и обновление данных таймесерии.

class CScatter : public CPlotBase
  {

public:
                     CScatter();
                    ~CScatter();
//---
   int               AddTimeseries(const double &timeseries_1[],const double &timeseries_2[]);
   bool              UpdateTimeseries(const double &timeseries_1[],const double &timeseries_2[],uint timeserie=0);

  };

В метод создания кривой AddTimeseries мы будем передавать два массива таймсерий анализируемых инструментов, по которым и будем строить график рассеяния. Фактически, стандартный класс CGraphic умеет отображать точечный график по двум массивам данным. Этим мы и воспользуемся. В начале метода создадим точечную кривую по двум массивам полученных данных. В случае ошибки создания кривой выходим из функции с результатом "-1". При успешном создании кривой зададим размер точек кривой и установим флаг отображения трендовой линии. После выполнения всех операций метод возвращает индекс созданной кривой.

int CScatter::AddTimeseries(const double &timeseries_1[],const double &timeseries_2[])
  {
   CCurve *curve=CGraphic::CurveAdd(timeseries_1,timeseries_2,CURVE_POINTS);
   if(curve==NULL)
      return -1;
   curve.PointsSize(2);
   curve.TrendLineVisible(true);
   return (m_arr_curves.Total()-1);
  }

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

Затем сравним размерность полученных таймсерий. Если размеры массивов отличаются или массивы пусты, завершаем функцию с результатом false.

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

После всех проверок передаем таймсерии кривой и завершаем функцию с результатом true.

bool CScatter::UpdateTimeseries(const double &timeseries_1[],const double &timeseries_2[], uint timeserie=0)
  {
   if((int)timeserie>=m_arr_curves.Total())
      return false;
   if(ArraySize(timeseries_1)!=ArraySize(timeseries_2) || ArraySize(timeseries_1)==0)
      return false;
//---
   CCurve *curve=m_arr_curves.At(timeserie);
   if(CheckPointer(curve)==POINTER_INVALID)
      return false;
//---
   curve.Update(timeseries_1,timeseries_2);
//---
   return true;
  }

2.3. Гистограмма

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

Следует обратить внимание, что базовый класс CGraphic может строить гистограмму только в ее классическом виде. Чтобы добавить возможность построения вертикальной гистограммы типа Market Profile, нам придется переписать метод HistogramPlot. Кроме того, добавим переменную e_orientation для хранения типа построения гистограммы и перепишем методы создания полотна графика, в которые добавим возможность указания типа гистограммы.

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

class CHistogram : public CPlotBase
  {
private:
   ENUM_HISTOGRAM_ORIENTATION    e_orientation;
   uint                          i_cells;

public:
                                 CHistogram();
                                ~CHistogram();
//---
   bool              Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int size, ENUM_HISTOGRAM_ORIENTATION orientation=HISTOGRAM_HORIZONTAL);
   bool              Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2, ENUM_HISTOGRAM_ORIENTATION orientation=HISTOGRAM_HORIZONTAL);
   int               AddTimeserie(const double &timeserie[]);
   bool              UpdateTimeserie(const double &timeserie[],uint timeserie=0);
   bool              SetCells(uint value)    {  i_cells=value; }

protected:
   virtual void      HistogramPlot(CCurve *curve);
   bool              CalculateHistogramArray(const double &data[],double &intervals[],double &frequency[], 
                                             double &maxv,double &minv);
};

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

После этого в цикле задаются центры интервалов и обнуляется массив частот.

В следующем цикле перебираем таймсерию и подсчитываем попадание значений в соответствующий интервал.

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

bool CHistogram::CalculateHistogramArray(const double &data[],double &intervals[],double &frequency[], 
                             double &maxv,double &minv) 
  { 
   int size=ArraySize(data); 
   if(size<(int)i_cells*10) return (false); 
   minv=data[ArrayMinimum(data)]; 
   maxv=data[ArrayMaximum(data)]; 
   double range=maxv-minv; 
   double width=range/i_cells; 
   if(width==0) return false; 
   ArrayResize(intervals,i_cells); 
   ArrayResize(frequency,i_cells); 
//--- зададим центры интервалов 
   for(uint i=0; i<i_cells; i++) 
     { 
      intervals[i]=minv+(i+0.5)*width; 
      frequency[i]=0; 
     } 
//--- заполним частоты попадания в интервал 
   for(int i=0; i<size; i++) 
     { 
      uint ind=int((data[i]-minv)/width); 
      if(ind>=i_cells) ind=i_cells-1; 
      frequency[ind]++; 
     } 
//--- нормализуем частоты в процентное представление
   for(uint i=0; i<i_cells; i++) 
      frequency[i]*=(100.0/(double)size); 
   return (true); 
  } 

Непосредственно построение гистограммы на графике осуществляется методом HistogramPlot. Эта функция построена по алгоритму базового класса CGraphic с поправкой на использование таймсерии и ориентации построения гистограммы.

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

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

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

CHistogram::HistogramPlot(CCurve *curve)
  {
   double data[],intervals[],frequency[];
   double max_value, min_value;
   curve.GetY(data);
   if(!CalculateHistogramArray(data,intervals,frequency,max_value,min_value))
      return;
//--- historgram parameters
   int histogram_width=fmax(curve.HistogramWidth(),2);
//--- check
   if(ArraySize(frequency)==0 || ArraySize(intervals)==0)
      return;
//---
   switch(e_orientation)
     {
      case HISTOGRAM_HORIZONTAL:
        m_y.AutoScale(false);
        m_x.Min(intervals[ArrayMinimum(intervals)]);
        m_x.Max(intervals[ArrayMaximum(intervals)]);
        m_x.MaxLabels(3);
        m_x.ValuesFormat("%.0f");
        m_y.Min(0);
        m_y.Max(frequency[ArrayMaximum(frequency)]);
        m_y.ValuesFormat("%.2f");
        break;
      case HISTOGRAM_VERTICAL:
        m_x.AutoScale(false);
        m_y.Min(intervals[ArrayMinimum(intervals)]);
        m_y.Max(intervals[ArrayMaximum(intervals)]);
        m_y.MaxLabels(3);
        m_y.ValuesFormat("%.0f");
        m_x.Min(0);
        m_x.Max(frequency[ArrayMaximum(frequency)]);
        m_x.ValuesFormat("%.2f");
        break;
     }
//---
   CalculateXAxis();
   CalculateYAxis();
//--- calculate original of y
   int originalY=m_height-m_down;
   int originalX=m_width-m_right;
   int yc0=ScaleY(0.0);
   int xc0=ScaleX(0.0);
//--- gets curve color
   uint clr=curve.Color();
//--- draw 
   for(uint i=0; i<i_cells; i++)
     {
      //--- check coordinates
      if(!MathIsValidNumber(frequency[i]) || !MathIsValidNumber(intervals[i]))
         continue;
      if(e_orientation==HISTOGRAM_HORIZONTAL)
        {
         int xc=ScaleX(intervals[i]);
         int yc=ScaleY(frequency[i]);
         int xc1 = xc - histogram_width/2;
         int xc2 = xc + histogram_width/2;
         int yc1 = yc;
         int yc2 = (originalY>yc0 && yc0>0) ? yc0 : originalY;
         //---
         if(yc1>yc2)
            yc2++;
         else
            yc2--;
         //---
         m_canvas.FillRectangle(xc1,yc1,xc2,yc2,clr);
        }
      else
        {
         int yc=ScaleY(intervals[i]);
         int xc=ScaleX(frequency[i]);
         int yc1 = yc - histogram_width/2;
         int yc2 = yc + histogram_width/2;
         int xc1 = xc;
         int xc2 = (originalX>xc0 && xc0>0) ? xc0 : originalX;
         //---
         if(xc1>xc2)
            xc2++;
         else
            xc2--;
         //---
         m_canvas.FillRectangle(xc1,yc1,xc2,yc2,clr);
        }
     }
//---
  }

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

2.4. Класс для работы с таймсериями

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

class CTimeserie :  public CObject
  {
protected:
   string               s_symbol;
   ENUM_TIMEFRAMES      e_timeframe;
   ENUM_APPLIED_PRICE   e_price;
   double               d_timeserie[];
   int                  i_bars;
   datetime             dt_last_load;
   
public:
                     CTimeserie(void);
                    ~CTimeserie(void);
   bool              Create(const string symbol=NULL, const ENUM_TIMEFRAMES timeframe=PERIOD_CURRENT, const ENUM_APPLIED_PRICE price=PRICE_CLOSE);
//--- Change settings of time series
   void              SetBars(const int value)            {  i_bars=value;  }
   void              Symbol(string value)                {  s_symbol=value;      dt_last_load=0;  }
   void              Timeframe(ENUM_TIMEFRAMES value)    {  e_timeframe=value;   dt_last_load=0;  }
   void              Price(ENUM_APPLIED_PRICE value)     {  e_price=value;       dt_last_load=0;  }
//---
   string            Symbol(void)                        {  return s_symbol;     }
   ENUM_TIMEFRAMES   Timeframe(void)                     {  return e_timeframe;  }
   ENUM_APPLIED_PRICE Price(void)                        {  return e_price;      }
//--- Load data
   virtual bool      UpdateTimeserie(void);
   bool              GetTimeserie(double &timeserie[])   {  return ArrayCopy(timeserie,d_timeserie)>0;   }
  };

Основная работа по подготовке данных будет производиться в методе UpdateTimeserie. В начале метода проверим, была ли ранее загружена необходимая информация на текущем баре. Если информация уже готова, то выходим из функции с результатом true. При необходимости подготовки данных загружаем необходимые исторические данные в соответствии с заданной ценой. При невозможности загрузки информации выходим из функции с результатом false. Следует обратить внимание, что в нашем анализе нас интересует не сама цена, а ее изменение. Поэтому загрузка исторических данных осуществляется на 1 бар больше заданного количества и на следующем этапе в цикле пересчитываем изменение цены на каждом баре и сохраняем в массив. В последующем пользователь может получить эту информацию методом GetTimeserie.

bool CTimeserie::UpdateTimeserie(void)
  {
   datetime cur_date=(datetime)SeriesInfoInteger(s_symbol,e_timeframe,SERIES_LASTBAR_DATE);
   if(dt_last_load>=cur_date && ArraySize(d_timeserie)>=i_bars)
      return true;
//---
   MqlRates rates[];
   int bars=0,i;
   double data[];
   switch(e_price)
     {
      case PRICE_CLOSE:
        bars=CopyClose(s_symbol,e_timeframe,1,i_bars+1,data);
        break;
      case PRICE_OPEN:
        bars=CopyOpen(s_symbol,e_timeframe,1,i_bars+1,data);
      case PRICE_HIGH:
        bars=CopyHigh(s_symbol,e_timeframe,1,i_bars+1,data);
      case PRICE_LOW:
        bars=CopyLow(s_symbol,e_timeframe,1,i_bars+1,data);
      case PRICE_MEDIAN:
        bars=CopyRates(s_symbol,e_timeframe,1,i_bars+1,rates);
        bars=ArrayResize(data,bars);
        for(i=0;i<bars;i++)
           data[i]=(rates[i].high+rates[i].low)/2;
        break;
      case PRICE_TYPICAL:
        bars=CopyRates(s_symbol,e_timeframe,1,i_bars+1,rates);
        bars=ArrayResize(data,bars);
        for(i=0;i<bars;i++)
           data[i]=(rates[i].high+rates[i].low+rates[i].close)/3;
        break;
      case PRICE_WEIGHTED:
        bars=CopyRates(s_symbol,e_timeframe,1,i_bars+1,rates);
        bars=ArrayResize(data,bars);
        for(i=0;i<bars;i++)
           data[i]=(rates[i].high+rates[i].low+2*rates[i].close)/4;
        break;
     }
//---
   if(bars<=0)
      return false;
//---
   dt_last_load=cur_date;
//---
   if(ArraySize(d_timeserie)!=(bars-1) && ArrayResize(d_timeserie,bars-1)<=0)
      return false;
   double point=SymbolInfoDouble(s_symbol,SYMBOL_POINT);
   for(i=0;i<bars-1;i++)
      d_timeserie[i]=(data[i+1]-data[i])/point;
//---
   return true;
  }

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


3. Собираем PairPlot

После создания "кирпичиков" можно приступить непосредственно к построению нашего инструмента. Создадим класс CPairPlot наследником класса CWndClient. Подобный подход облегчит использование нашего инструмента в графических панелях, построенных с использованием стандартного класса CAppDialog (о его использовании подробно рассказано в статьях [5,6]).

В блоке private нашего класса объявим массив указателей на объекты класса CPlotBase для хранения указателей на графики, объект класса CArrayObj для хранения указателей на объекты таймсерий, а также переменные для хранения используемых таймфрейма, цены, ориентации гистограмм, глубины истории и цвета для отображения наименований инструмента на графике.

class CPairPlot : public CWndClient
  {
private:
   CPlotBase                    *m_arr_graphics[];
   CArrayObj                     m_arr_symbols;
   ENUM_TIMEFRAMES               e_timeframe;
   ENUM_APPLIED_PRICE            e_price;
   int                           i_total_symbols;
   uint                          i_bars;
   ENUM_HISTOGRAM_ORIENTATION    e_orientation;
   uint                          i_text_color;
      
public:
                     CPairPlot();
                    ~CPairPlot();
//---
   bool              Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2, const string &symbols[],const ENUM_TIMEFRAMES timeframe=PERIOD_CURRENT, const int bars=1000, const uint cells=10, const ENUM_APPLIED_PRICE price=PRICE_CLOSE);
   bool              Refresh(void);
   bool              HistogramOrientation(ENUM_HISTOGRAM_ORIENTATION value);
   ENUM_HISTOGRAM_ORIENTATION    HistogramOrientation(void)    {  return e_orientation;   }
   bool              SetTextColor(color value);
//--- geometry
   virtual bool      Shift(const int dx,const int dy);
//--- state
   virtual bool      Show(void);
   virtual bool      Hide(void);
  };

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

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

bool CPairPlot::Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2, const string &symbols[],const ENUM_TIMEFRAMES timeframe=PERIOD_CURRENT, const int bars=1000, const uint cells=10, const ENUM_APPLIED_PRICE price=PRICE_CLOSE)
  {
   i_total_symbols=0;
   int total=ArraySize(symbols);
   if(total<=1 || bars<100)
      return false;
//---
   e_timeframe=timeframe;
   i_bars=bars;
   e_price=price;

На следующем этапе, в цикле создаем экземпляры классов CTimeserie для каждого инструмента. При невозможности создания таймсерий для каждого ззаданного инструмента выходим из функции с результатом false.

   for(int i=0;i<total;i++)
     {
      CTimeserie *temp=new CTimeserie;
      if(temp==NULL)
         return false;
      temp.SetBars(i_bars);
      if(!temp.Create(symbols[i],e_timeframe,e_price))
         return false;
      if(!m_arr_symbols.Add(temp))
         return false;
     }
   i_total_symbols=m_arr_symbols.Total();
   if(i_total_symbols<=1)
      return false;

После успешного проведения подготовительных работ переходим к непосредственному созданию графических объектов. В начале вызовем метод Create родительского класса. Затем размер массива m_arr_graphics (массив для хранения указателей на графики) приведем в соответствие с количеством анализируемых инструментов. Рассчитаем ширину и высоту каждого графика исходя их размеров всего инструмента и количества анализируемых инструментов.

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

   if(!CWndClient::Create(chart,name,subwin,x1,y1,x2,y2))
      return false;
//---
   if(ArraySize(m_arr_graphics)!=(i_total_symbols*i_total_symbols))
      if(ArrayResize(m_arr_graphics,i_total_symbols*i_total_symbols)<=0)
         return false;
   int width=Width()/i_total_symbols;
   int height=Height()/i_total_symbols;
   for(int i=0;i<i_total_symbols;i++)
     {
      CTimeserie *timeserie1=m_arr_symbols.At(i);
      if(timeserie1==NULL)
         continue;
      for(int j=0;j<i_total_symbols;j++)
        {
         string obj_name=m_name+"_"+(string)i+"_"+(string)j;
         int obj_x1=m_rect.left+j*width;
         int obj_x2=obj_x1+width;
         int obj_y1=m_rect.top+i*height;
         int obj_y2=obj_y1+height;
         if(i==j)
           {
            CHistogram *temp=new CHistogram();
            if(CheckPointer(temp)==POINTER_INVALID)
               return false;
            if(!temp.Create(m_chart_id,obj_name,m_subwin,obj_x1,obj_y1,obj_x2,obj_y2,e_orientation))
               return false;
            m_arr_graphics[i*i_total_symbols+j]=temp;
            temp.SetCells(cells);
           }
         else
           {
            CScatter *temp=new CScatter();
            if(CheckPointer(temp)==POINTER_INVALID)
               return false;
            if(!temp.Create(m_chart_id,obj_name,m_subwin,obj_x1,obj_y1,obj_x2,obj_y2))
               return false;
            CTimeserie *timeserie2=m_arr_symbols.At(j);
            if(timeserie2==NULL)
               continue;
            m_arr_graphics[i*i_total_symbols+j]=temp;
           }
        }
     }
//---
   return true;
  }

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

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

bool CPairPlot::Refresh(void)
  {
   bool updated=true;
   for(int i=0;i<i_total_symbols;i++)
     {
      CTimeserie *timeserie=m_arr_symbols.At(i);
      if(timeserie==NULL)
         continue;
      updated=(updated && timeserie.UpdateTimeserie());
     }
   if(!updated)
      return false;
//---
   for(int i=0;i<i_total_symbols;i++)
     {
      CTimeserie *timeserie1=m_arr_symbols.At(i);
      if(CheckPointer(timeserie1)==POINTER_INVALID)
         continue;
      double ts1[];
      if(!timeserie1.GetTimeserie(ts1))
         continue;
//---
      for(int j=0;j<i_total_symbols;j++)
        {
         if(i==j)
           {
            CHistogram *temp=m_arr_graphics[i*i_total_symbols+j];
            if(CheckPointer(temp)==POINTER_INVALID)
               return false;
            if(temp.CurvesTotal()==0)
              {
               if(temp.AddTimeserie(ts1)<0)
                  continue;
              }
            else
              {
               if(!temp.UpdateTimeserie(ts1))
                  continue;
              }
            if(!temp.CurvePlotAll())
               continue;
            if(i==0)
               temp.TextUp(timeserie1.Symbol(),i_text_color);
            if(i==(i_total_symbols-1))
               temp.TextDown(timeserie1.Symbol(),i_text_color);
            if(j==0)
               temp.TextLeft(timeserie1.Symbol(),i_text_color);
            if(j==(i_total_symbols-1))
               temp.TextRight(timeserie1.Symbol(),i_text_color);
            temp.Update(false);
           }
         else
           {
            CScatter *temp=m_arr_graphics[i*i_total_symbols+j];
            if(CheckPointer(temp)==POINTER_INVALID)
               return false;
            CTimeserie *timeserie2=m_arr_symbols.At(j);
            if(CheckPointer(timeserie2)==POINTER_INVALID)
               continue;
            double ts2[];
            if(!timeserie2.GetTimeserie(ts2))
               continue;
            if(temp.CurvesTotal()==0)
              {
               if(temp.AddTimeseries(ts1,ts2)<0)
                  continue;
              }
            else
               if(!temp.UpdateTimeseries(ts1,ts2))
                  continue;
            if(!temp.CurvePlotAll())
               continue;
            if(i==0)
               temp.TextUp(timeserie2.Symbol(),i_text_color);
            if(i==(i_total_symbols-1))
               temp.TextDown(timeserie2.Symbol(),i_text_color);
            if(j==0)
               temp.TextLeft(timeserie1.Symbol(),i_text_color);
            if(j==(i_total_symbols-1))
               temp.TextRight(timeserie1.Symbol(),i_text_color);
            temp.Update(false);
           }
        }
     }
//---
   ChartRedraw(m_chart_id);
//---
   return true;
  }

Ранее, я уже обращал внимание читателя на то обстоятельство, что наши графические элементы построены на базе класса CGraphic, который не является наследником класса CObject. По этой причине мы и добавляли в наш базовый класс CPlotBase методы Shift, Hide, Show. По этой же причине нам предстоит также переписать соответствующие методы в классе CPairPlot. С полным кодом всех классов и их методов можно ознакомиться во вложении.


4. Пример использования класса CPairPlot

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

Как уже писалось выше, инструмент построен для использования в графических панелях. Поэтому вначале создадим класс CPairPlotDemo и назначим его наследником класса CAppDialog. Подробно о работе с классом CAppDialog писалось в статьях [5, 6], поэтому здесь я укажу только на особенности использования нашего инструмента.

В блоке private объявим экземпляр нашего класса CPairPlot. В блоке public объявим метод Create с указанием всех входных параметров, необходимых для инициализации и работы нашего инструмента. Здесь же мы объявим методы Refresh и HistogramOrientation, которые будут вызывать соответствующие методы нашего инструмента.

class CPairPlotDemo : public CAppDialog
  {
private:
   CPairPlot         m_PairPlot;
public:
                     CPairPlotDemo();
                    ~CPairPlotDemo();
//---
   bool              Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2,const string &symbols[],const ENUM_TIMEFRAMES timeframe=PERIOD_CURRENT, const int bars=1000, const uint cells=10);
   bool              Refresh(void);
//---
   bool              HistogramOrientation(ENUM_HISTOGRAM_ORIENTATION value)   {  return m_PairPlot.HistogramOrientation(value);   }
   ENUM_HISTOGRAM_ORIENTATION    HistogramOrientation(void)                   {  return m_PairPlot.HistogramOrientation();   }
   };

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

bool CPairPlotDemo::Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2,const string &symbols[],const ENUM_TIMEFRAMES timeframe=PERIOD_CURRENT, const int bars=1000, const uint cells=10)
  {
   if(!CAppDialog::Create(chart,name,subwin,x1,y1,x2,y2))
      return false;
   if(!m_PairPlot.Create(m_chart_id,m_name+"PairPlot",m_subwin,0,0,ClientAreaWidth(),ClientAreaHeight(),symbols,timeframe,bars,cells))
      return false;
   if(!Add(m_PairPlot))
      return false;
//---
   return true;
  }

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

input string   i_Symbols   =  "EURUSD, GBPUSD, EURGBP";
input uint     i_Bars      =  1000;
input uint     i_Cells     =  50;
input ENUM_HISTOGRAM_ORIENTATION i_HistogramOrientation  =  HISTOGRAM_HORIZONTAL;

В глобальных переменных объявим только экземпляр нашего класса CPairPlotDemo.

CPairPlotDemo     *PairPlot;

В функции OnInit сначала создадим массив используемых инструментов из строки внешних параметров индикатора. Затем создадим экземпляр класса CPairPlotDemo, передадим в него заданную ориентацию гистограмм и вызовем его метод Create. После успешной инициализации запустим выполнение класса методом Run и обновим данные методом Refresh.

int OnInit()
  {
//---
   string symbols[];
   int total=StringSplit(i_Symbols,',',symbols);
   if(total<=0)
      return INIT_FAILED;
   for(int i=0;i<total;i++)
     {
      StringTrimLeft(symbols[i]);
      StringTrimRight(symbols[i]);
     }
//---
   PairPlot=new CPairPlotDemo;
   if(CheckPointer(PairPlot)==POINTER_INVALID)
      return INIT_FAILED;
//---
   if(!PairPlot.HistogramOrientation(i_HistogramOrientation))
      return INIT_FAILED;
   if(!PairPlot.Create(0,"Pair Plot",0,20,20,620,520,symbols,PERIOD_CURRENT,i_Bars,i_Cells))
      return INIT_FAILED;
   if(!PairPlot.Run())
      return INIT_FAILED;
   PairPlot.Refresh();
//---
   return INIT_SUCCEEDED;
  }

В функции OnCalculate будем вызывать метод Refresh при наступлении каждого нового бара. Из функции OnChartEvent и OnDeinit будем вызывать соответствующие методы нашего класса.

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

Ниже вы можете видеть, как работает наш индикатор.

Демонстрация работы PairPlot


Заключение

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


Ссылки

  1. Работа с корзинами валютных пар на рынке форекс
  2. Тестирование паттернов, возникающих при торговле корзинами валютных пар. Часть I
  3. Тестирование паттернов, возникающих при торговле корзинами валютных пар. Часть II
  4. Визуализируй это! Графическая библиотека в MQL5 как аналог PLOT из R
  5. Как создать графическую панель любой сложности и как это работает
  6. Улучшаем работу с панелями: добавляем прозрачность, меняем цвет фона и наследуемся от CAppDialog/CWndClient


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

#
 Имя
Тип 
Описание 
1 PlotBase.mqh Библиотека класса Базовый класс для построения графиков
2 Scatter.mqh Библиотека класса Класс для построения графика рассеяния
3 Histogram.mqh Библиотека класса Класс для построения гистограммы
4 PairPlot.mqh Библиотека класса Класс инструмента PairPlot
5 PairPlotDemo.mqh Библиотека класса Класс для демонстрации подключения инструмента
6 PairPlot.mq5 Индикатор Индикатор для демонстрации работы инструмента