3D-моделирование на MQL5

Sergey Pavlov | 16 декабря, 2016

3D-моделирование позволяет исследовать сложные процессы и явления, интересующие исследователя, чтобы спрогнозировать их результаты.

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

Рис. 1. Примеры трёхмерного представления временных рядов.

Рис. 1. Примеры трёхмерного представления временных рядов.

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

Цель статьи — показать привычные графики в трёхмерном пространстве. Поскольку на данный момент готовых решений 3D-моделирования на MQL5 еще нет, то начнём с базовых принципов и методов, а именно — с 3D-объектов и систем координат. Возможно, многие читатели скептически отнесутся к этой теме и просмотрят ее "по диагонали", но всё же некоторые алгоритмы и методы из нее могут оказаться полезными в других задачах, не связанных с трёхмерной визуализацией.


Интерактивный графический объект

Первое, с чего мы начнем — 3D-объекты. Богатый функционал MQL5 позволяет оперировать с двумерными объектами и создавать сложные графические построения. Достаточно добавить несколько функций — и в терминале МТ5 станет доступна и трехмерная графика.

Для начала перечислим, какие требования должны быть выполнены при проектировании базовых классов 3D объектов.

  1. Простота использования.
  2. Высокая живучесть.
  3. Самостоятельность.
  4. Интерактивность.

Простота использования.

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

Высокая живучесть.

3D объект должен быть "неубиваемым" в течение всего времени жизни программы создавшего экземпляр класса. Его надо защитить от случайного или осознанного удаления или изменения базовых свойств.

Самостоятельность.

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

Интерактивность.

3D-визуализация предполагает возможность изменения точки взгляда на трёхмерную модель (вращение системы координат). Поэтому нужно создать такой функционал, при котором не потребуется использование дополнительных панелей управления или чего-то в этом роде. Строго говоря, графические объекты в языке MQL5 уже обладают свойством интерактивности: объект можно выделить, переместить, изменить его свойства и т.п. Достаточно немного усовершенствовать эту способность до уровня коллективного управления и взаимодействия. Например, мы изменили центр координат — и все взаимосвязанные объекты перестроились самостоятельно, причём безошибочно.

Если выполнить все эти требования, то 3D-объект превратится в интерактивный графический объект (IGO). Интерактивный графический объект обязательно связан с графическим объектом из языка MQL5. Приступим к рассмотрению базового класса CIGO интерактивных графических объектов.

class CIGO
  {
protected:
   bool              on_event;      // флаг обработки событий
   int               m_layer;       // слой, которому принадлежит IGO
   //---
   double            SetPrice(double def,int prop_modifier=0);
public:
   string            m_name;        // имя IGO объекта
   double            m_price;       // базовая точка привязки IGO [цена]
   double            m_angle;       // значение угла привязки IGO [град]
                     CIGO();
                    ~CIGO();
   //---
   virtual     // Метод: создать IGO
   void              Create(string name) {on_event=true;}
   virtual     // Метод: перерисовать IGO
   void              Redraw() {ChartRedraw();}
   virtual     // Метод обработки события OnChartEvent
   bool              OnEvent(const int id,         // идентификатор события
                             const long &lparam,   // параметр события типа long
                             const double &dparam, // параметр события типа double
                             const string &sparam, // параметр события типа string
                             int iparamemr=0,      // идентификатор события IGO
                             double dparametr=0.0);// параметр события IGO типа double
  };

Базовый класс содержит минимум полей и методов, которые можно в дальнейшем переопределить или дополнить в классах наследниках. Подробно рассмотрим только два метода класса: виртуальный метод OnEvent() обработки событий OnChartEvent и метод установки базовой точки привязки SetPrice(). Остановимся на них, поскольку именно в них реализованы главные принципы интерактивных графических объектов.

Метод: обработка поступающих событий OnEvent.

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

  1. Удаление графического объекта. При возникновении данного события объекта на графике больше нет — он удалён. Но поскольку нам необходимо выполнить требование по живучести, то его надо немедленно восстановить, т.е. создать заново, причём с теми же свойствами, которые он имел до удаления. Обратите внимание: удаляется графический объект, а не связанный с ним экземпляр класса CIGO. Экземпляр класса продолжает существовать, и он помнит информацию об удалённом графическом объекте, поэтому легко может его восстановить с помощью метода Create().
  2. Изменение размеров или свойств графика. Событий такого типа очень много: возникновение нового бара, изменение масштаба графика, переход на другой таймфрейм и другие. Реакция на него должна быть одна — перерисовать объект с учётом изменившегося окружения, использовав метод Redraw(). Важно отметить, что при переходе на другой таймфрейм экземпляр класса инициализируется заново и он теряет данные о созданном графическом объекте, которые хранились в полях класса, хотя сам графический объект остался на графике. Поэтому поля экземпляра класса восстанавливаются значениями свойств графического объекта, что несомненно повышает живучесть IGO.
  3. Перетаскивание графического объекта. На этом событии построена интерактивность графического объекта. При его перетаскивании изменяются базовые точки привязки. Перед тем, как перетащить объект, его нужно отселектировать (выделить двойным щелчком мышки). Если такое событие произошло, то метод возвращает true, а в противном случае — false. Это значение нам пригодится, когда будем организовывать коллективную работу интерактивных графических объектов. Следует отметить, что если нам нужен графический объект, который нельзя было бы перетащить, то достаточно при его создании запретить возможность селектирования.
  4. Нажатие мыши на графическом объекте. Если нажатие мышки произошло на любом другом объекте, кроме этого, то снимаем выделение объекта, чтобы исключить его случайное перемещение. Таким образом, на графике можно выделить только один объект IGO.
    bool CIGO::OnEvent(const int id,
                       const long &lparam,
                       const double &dparam,
                       const string &sparam,
                       int iparamemr=0,
                       double dparametr=0.0)
      {
       bool res=false;
       if(on_event) // обработка событий разрешена
         {
          // Удаление графического объекта
          if((ENUM_CHART_EVENT)id==CHARTEVENT_OBJECT_DELETE && sparam==m_name)
            {
             Create(m_name);
            }
          // Изменение размеров графика или изменение свойств графика через диалог свойств
          if((ENUM_CHART_EVENT)id==CHARTEVENT_CHART_CHANGE)
            {
             Redraw();
            }
          // Перетаскивание графического объекта
          if((ENUM_CHART_EVENT)id==CHARTEVENT_OBJECT_DRAG)
             if(ObjectGetInteger(0,sparam,OBJPROP_SELECTED)==1 && sparam==m_name)
               {
                m_price=ObjectGetDouble(0,m_name,OBJPROP_PRICE);
                Redraw();
                res=true;   // сообщаем, что изменилась базовая точка привязки
               }
          // нажатие мышки не на этом графическом объекте
          if((ENUM_CHART_EVENT)id==CHARTEVENT_OBJECT_CLICK && sparam!=m_name)
            {
             ObjectSetInteger(0,m_name,OBJPROP_SELECTED,0);
             ChartRedraw();
            }
         }
       return(res);
      }

Параметры: 

 id

   [in] Идентификатор события. Существуют 9 видов событий, которые можно обрабатывать с помощью данного метода.

 lparam

   [in] Параметр события типа long.

 dparam

   [in] Параметр события типа double.

 sparam

   [in] Параметр события типа string.

 iparametr

   [in] Идентификатор пользовательского события.

 dparametr

   [in] Параметр пользовательского события типа double.

Возвращаемое значение:

 Возвращает true, если произошло изменение координат базовой точки привязки объекта. В противном случае — false. 

 

Метод: установка координаты базовой точки привязки SetPrice.

Устанавливает значение "price" в системе координат "График" для поля m_price экземпляра класса.

Поясню, что происходит, когда запрашивается координата базовой точки привязки данным методом.

  1. При инициализации экземпляра класса поле m_price (координата базовой точки привязки) не содержит входного значения, поэтому m_price=NULL. Инициализация происходит или при создании экземпляра класса, или при переключении графика на другой таймфрейм. Однако сам графический объект может существовать на графике. Возможно, он остался от предыдущего вызова программы или при переключении таймфрейма. Следовательно, полю m_price присвоим значение соответствующего свойства графического объекта.
  2. Если графического объекта с именем m_name не существует, то после первого шага координата базовой точки привязки по-прежнему не определена: m_price=NULL. В таком случае значению поля m_price присваивается значение по умолчанию def.
double CIGO::SetPrice(double def,int prop_modifier=0)
  {
   if(m_price==NULL)             // если нет значения переменной
      m_price=ObjectGetDouble(0,m_name,OBJPROP_PRICE,prop_modifier);
   if(m_price==NULL)             // если нет координат
      m_price=def;               // значение по умолчанию
   return(m_price);
  }

Параметры:

 def

   [in] Значение переменной по умолчанию.

 prop_modifier

   [in] Модификатор запрашиваемого свойства графического объекта.

Возвращаемое значение:

 Значение координаты базовой точки привязки.  

Теперь познакомимся с прямыми наследниками базового класса интерактивных объектов. В первую очередь нас интересуют 3D-объекты для создания необходимого окружения трёхмерного моделирования.

 

Класс C_OBJ_ARROW_RIGHT_PRICE: объект "Правая ценовая метка"

Прямой потомок базового класса CIGO

//+------------------------------------------------------------------+
//| Класс OBJ_ARROW_RIGHT_PRICE: объект "Правая ценовая метка"       |
//+------------------------------------------------------------------+
class C_OBJ_ARROW_RIGHT_PRICE:public CIGO
  {
public:
   virtual     // Метод: создать объект
   void              Create(string name);
   virtual     // Метод: перерисовать объект
   void              Redraw();
  };
//+------------------------------------------------------------------+
//| Метод: создать объект                                            |
//+------------------------------------------------------------------+
void C_OBJ_ARROW_RIGHT_PRICE::Create(string name)
  {
   m_name=name;
   m_price=SetPrice((ChartGetDouble(0,CHART_PRICE_MAX)+ChartGetDouble(0,CHART_PRICE_MIN))/2);
   ObjectCreate(0,m_name,OBJ_ARROW_RIGHT_PRICE,0,0,0);
   ObjectSetInteger(0,m_name,OBJPROP_SELECTABLE,true);
   ObjectSetInteger(0,m_name,OBJPROP_WIDTH,1);
   ObjectSetInteger(0,m_name,OBJPROP_COLOR,clrISO);
//---
   ObjectSetInteger(0,m_name,OBJPROP_TIME,0,T(0));
   ObjectSetDouble(0,m_name,OBJPROP_PRICE,0,m_price);
//---
   ChartRedraw();
   on_event=true; // разрешаем обработку событий
  }
//+------------------------------------------------------------------+
//| Метод: перерисовать объект                                       |
//+------------------------------------------------------------------+
void C_OBJ_ARROW_RIGHT_PRICE::Redraw()
  {
   ObjectSetInteger(0,m_name,OBJPROP_TIME,0,T(0));
   ChartRedraw();
  }

Этот класс лучше всего подходит для организации центра трёхмерной системы координат. Экземпляр класса привязан к текущему бару и фактически находится на оси Z. 

 

Класс C_OBJ_TREND: объект "Трендовая линия" 

Прямой потомок базового класса CIGO.

//+------------------------------------------------------------------+
//| Класс OBJ_TREND: объект "Трендовая линия"                        |
//+------------------------------------------------------------------+
class C_OBJ_TREND:public CIGO
  {
public:
   virtual     // Метод: создать объект
   void              Create(string name);
   virtual     // Метод: перерисовать объект
   void              Redraw();
  };
//+------------------------------------------------------------------+
//| Метод: создать объект                                            |
//+------------------------------------------------------------------+
void C_OBJ_TREND::Create(string name)
  {
   m_name=name;
   m_price=(ChartGetDouble(0,CHART_PRICE_MAX)+ChartGetDouble(0,CHART_PRICE_MIN))/2;
   ObjectCreate(0,m_name,OBJ_TREND,0,0,0);
   ObjectSetInteger(0,m_name,OBJPROP_COLOR,clrISO);
   ObjectSetInteger(0,m_name,OBJPROP_WIDTH,0);
   ObjectSetInteger(0,m_name,OBJPROP_STYLE,styleISO);
   ObjectSetDouble(0,m_name,OBJPROP_PRICE,0,m_price);
   ObjectSetInteger(0,m_name,OBJPROP_TIME,0,T(0));
   ObjectSetDouble(0,m_name,OBJPROP_PRICE,1,m_price+1);
   ObjectSetInteger(0,m_name,OBJPROP_TIME,1,T(0));
   ObjectSetInteger(0,m_name,OBJPROP_RAY_RIGHT,true);
   ObjectSetInteger(0,m_name,OBJPROP_RAY_LEFT,true);
   ObjectSetInteger(0,m_name,OBJPROP_BACK,true);
   ObjectSetInteger(0,m_name,OBJPROP_SELECTABLE,false);
//---
   ChartRedraw();
   on_event=true; // разрешаем обработку событий
  }
//+------------------------------------------------------------------+
//| Метод: перерисовать объект                                       |
//+------------------------------------------------------------------+
void C_OBJ_TREND::Redraw()
  {
   ObjectSetInteger(0,m_name,OBJPROP_TIME,0,T(0));
   ObjectSetInteger(0,m_name,OBJPROP_TIME,1,T(0));
   ChartRedraw();
  }

Ось Z логичнее создавать с помощью данного класса. Возможности минимальны, но и их достаточно для 3D-моделирования.

 

Класс C_OBJ_TRENDBYANGLE: объект "Трендовая линия по углу" 

Прямой потомок базового класса CIGO.

//+------------------------------------------------------------------+
//| Класс OBJ_TRENDBYANGLE: объект "Трендовая линия по углу"         |
//+------------------------------------------------------------------+
class C_OBJ_TRENDBYANGLE:public CIGO
  {
protected:
   int               m_bar;   // номер бара для привязки 2-ой базовой точки
   //---
   double SetAngle(double def)
     {
      if(m_angle==NULL) // если нет значения переменной
         m_angle=ObjectGetDouble(0,m_name,OBJPROP_ANGLE);
      if(m_angle==NULL)       // если нет координат
         m_angle=def;         // значение по умолчанию
      return(m_angle);
     }
public:
                     C_OBJ_TRENDBYANGLE();
   virtual     // Метод: создать объект
   void              Create(string name,double price,double angle);
   virtual     // Метод: перерисовать объект
   void              Redraw();
   virtual     // Метод обработки события OnChartEvent
   bool              OnEvent(const int id,
                             const long &lparam,
                             const double &dparam,
                             const string &sparam,
                             int iparamemr=0,
                             double dparametr=0.0);
  };
//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
C_OBJ_TRENDBYANGLE::C_OBJ_TRENDBYANGLE()
  {
   m_bar=c_bar;
  }
//+------------------------------------------------------------------+
//| Метод: создать объект                                            |
//+------------------------------------------------------------------+
void C_OBJ_TRENDBYANGLE::Create(string name,double price,double angle)
  {
   datetime time=T(0);
   datetime deltaT=T(m_bar)-time;
   m_name=name;
   m_price=SetPrice(price);
   m_angle=SetAngle(angle);
   ObjectCreate(0,m_name,OBJ_TRENDBYANGLE,0,time,m_price,time+deltaT,m_price);
   ObjectSetInteger(0,m_name,OBJPROP_COLOR,clrISO);
   ObjectSetInteger(0,m_name,OBJPROP_WIDTH,0);
   ObjectSetInteger(0,m_name,OBJPROP_STYLE,styleISO);
   ObjectSetInteger(0,m_name,OBJPROP_RAY_RIGHT,true);
   ObjectSetInteger(0,m_name,OBJPROP_RAY_LEFT,true);
   ObjectSetInteger(0,m_name,OBJPROP_BACK,true);
   ObjectSetInteger(0,m_name,OBJPROP_SELECTABLE,true);
//--- изменяем угол наклона трендовой линии; в процессе изменения угла координата второй
//--- точки линии переопределится автоматически в соответствии с новым значением угла
   ObjectSetDouble(0,m_name,OBJPROP_ANGLE,m_angle);
   ChartRedraw();
//---
   on_event=true; // разрешаем обработку событий
  }
//+------------------------------------------------------------------+
//| Метод: перерисовать объект                                       |
//+------------------------------------------------------------------+
void C_OBJ_TRENDBYANGLE::Redraw()
  {
   ObjectSetInteger(0,m_name,OBJPROP_TIME,T(0));
   ObjectSetInteger(0,m_name,OBJPROP_TIME,1,T(0)+T(m_bar)-T(0));
   ObjectSetDouble(0,m_name,OBJPROP_PRICE,m_price);
   ObjectSetDouble(0,m_name,OBJPROP_ANGLE,m_angle);
   ChartRedraw();
  }
//+------------------------------------------------------------------+
//| Метод обработки события OnChartEvent                             |
//+------------------------------------------------------------------+
bool C_OBJ_TRENDBYANGLE::OnEvent(const int id,
                                 const long &lparam,
                                 const double &dparam,
                                 const string &sparam,
                                 int iparamemr=0,
                                 double dparametr=0.0)
  {
//---
   bool res=false;
   if(on_event) // обработка событий разрешена
     {
      // Удаление графического объекта
      if((ENUM_CHART_EVENT)id==CHARTEVENT_OBJECT_DELETE && sparam==m_name)
        {
         Create(m_name,m_price,m_angle);
        }
      // Изменение размеров графика или изменение свойств графика через диалог свойств
      if((ENUM_CHART_EVENT)id==CHARTEVENT_CHART_CHANGE)
        {
         Redraw();
        }
      // Перетаскивание графического объекта
      if((ENUM_CHART_EVENT)id==CHARTEVENT_OBJECT_DRAG)
        {
         //---
         if(ObjectGetInteger(0,sparam,OBJPROP_SELECTED)==1 && sparam==m_name)
           {
            m_angle=ObjectGetDouble(0,m_name,OBJPROP_ANGLE);
            Create(m_name,m_price,m_angle);
            res=true;   // сообщаем, что изменилась базовая точка привязки
           }
         if(iparamemr==Event_1)// получили сообщение об изменении базовой точки
           {
            m_price=dparametr;
            Create(m_name,m_price,m_angle);
           }
         if(iparamemr==Event_2)// получили сообщение об изменении базового угла
           {
            m_angle=dparametr;
            Create(m_name,m_price,m_angle);
           }
        }
      // нажатие мышки не на этом графическом объекте
      if((ENUM_CHART_EVENT)id==CHARTEVENT_OBJECT_CLICK && sparam!=m_name)
        {
         ObjectSetInteger(0,m_name,OBJPROP_SELECTED,0);
         ChartRedraw();
        }
     }
   return(res);
  }

Первоначально планировалось использовать для осей X и Y наследников класса "трендовая линия", но в процессе работы стало понятно, что для этих целей идеально подходит "трендовая линия по углу". Перечитывая документацию по MQL5, я открыл для себя этот мощный инструмент: после того, как я построил трендовую линию по углу, при переключении таймфреймов или изменении масштабов она оставалась как вкопанная, то есть угол не менялся.

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

 

Графическая память (GM)

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

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

class CGM
  {
private:
public:
   string            m_name;        // имя графического объекта
                     CGM(){}
                    ~CGM(){}
   void              Create(string name) {m_name=name;}
   // чтение свойства OBJPROP_PRICE
   double            R_OBJPROP_PRICE(int prop_modifier=0)
     {
      return(ObjectGetDouble(0,m_name,OBJPROP_PRICE,prop_modifier));
     }
   // чтение свойства OBJPROP_TIME
   datetime          R_OBJPROP_TIME(int prop_modifier=0)
     {
      return((datetime)ObjectGetInteger(0,m_name,OBJPROP_TIME,prop_modifier));
     }
   // чтение свойства OBJPROP_ANGLE
   double            R_OBJPROP_ANGLE()
     {
      return(ObjectGetDouble(0,m_name,OBJPROP_ANGLE));
     }
   // чтение свойства OBJPROP_TEXT
   string            R_OBJPROP_TEXT()
     {
      return(ObjectGetString(0,m_name,OBJPROP_TEXT));
     }
   // возвращает значение цены для указанного времени
   double            R_ValueByTime(datetime time)
     {
      return(ObjectGetValueByTime(0,m_name,time));
     }
   // запись свойства OBJPROP_TEXT
   void              W_OBJPROP_TEXT(string text)
     {
      ObjectSetString(0,m_name,OBJPROP_TEXT,text);
     }
  };

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

Графическую память можно образно представить как доску объявлений, на которой размещены предложения и спрос на некую информацию. Таким образом происходит обмен данными между интерактивными графическими объектами. Какие данные нужны для правильной работы 3D-объекта, программируется на этапе проектирования 3D модели. Наиболее ценной информацией обладают объекты системы координат, т.к. при вращении её или переносе центра координат, вся "стая" 3D-объектов должна изменить свои свойства и своё местоположение в трёхмерном пространстве.

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

Система координат

Трёхмерный визуальный анализ позволяет анализировать данные в 3D-пространстве: например, строить трёхмерное изображение последовательностей исходных данных (наблюдений) для одной или нескольких выбранных переменных. Выбранные переменные представляются по оси Y, последовательные наблюдения — по оси X, а значения переменных (для данного наблюдения) откладываются по оси Z.

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

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

Прежде чем приступить к 3D-моделированию, давайте рассмотрим те системы координат, с которыми придётся иметь дело. Две системы координат "График" и "Картина" созданы разработчиками языка MQL5, а вот трёхмерную систему (аксонометрию) мы реализуем самостоятельно. Итак, рассмотрим, в чём отличия и целевое назначение каждой из них.

Рис. 2. Система координат "График".

Рис. 2. Система координат "График".

Система координат "График" (рис. 2). Это двумерная система координат для отображения ценовых данных, таймсерий и индикаторов. На горизонтальной оси расположена шкала времени (time), направленная слева направо, а на вертикальной оси — цена (price) финансового инструмента. Именно в этой системе координат работает большинство графических объектов MQL5. Особенность — в том, что текущая цена и бар находятся на одной вертикальной оси,  и при появлении нового бара график автоматически смещается влево.

Рис. 3. Система координат "Картина".

Рис. 3. Система координат "Картина".

Система координат "Картина" (рис. 3). Каждой точке на экране монитора соответствует один пиксель. Координаты точек отсчитываются от верхнего левого угла графика. Она, как и "График", двумерная и используется объектами, не привязанным к таймсериям. Эта система координат в данной статье не пригодится.

Рис. 4. Трёхмерная система координат.

Рис. 4. Трёхмерная система координат.

Трёхмерная система координат (рис. 4). В этой системе координат три оси перпендикулярны между собой. Однако визуально в системе координат "Картина" углы между осями XYZ не равны 90 градусам. Например, ось X и ось Y между собой образуют угол в 120 градусов. Этот угол может быть и другим, но в рамках статьи примем его таким.

Ось Z привязана к текущему бару, и масштаб оси совпадает со шкалой "price" в системе координат "График". Это очень удобно, поскольку не надо создавать отдельную шкалу для оси Z, а можно воспользоваться шкалой "price", при этом Z=price.

Ось X направлена справа налево, то есть напротив шкалы "time" в системе координат "График". По этой оси мы будем откладывать значение переменной Bar; значит, проекция любого бара на ось X будет совпадать с его значением (X=Bar).

Ось Y предназначена для рядов двумерных данных XZ. Например, можно раздвинуть линии таймсерий Open, Close, High, Low по этой оси, и они будут располагаться каждая в своей плоскости. Если же соединить все точки этих линий в плоскостях, параллельных плоскости YZ, то получим сетку поверхности, т.е. трёхмерный объект таймсерий (см. рис. 5). 

Рис. 5. 3D объект в трёхмерной системе координат.

Рис. 5. 3D объект в трёхмерной системе координат.

 

Интерактивная система координат: Класс CUSC

Прямой потомок базового класса CIGO

class CUSC:public CIGO
  {
private:
   datetime          m_prev_bar;
   datetime          m_next_bar;
   //---
   C_OBJ_ARROW_RIGHT_PRICE Centr;   // объявление экземпляра класса C_OBJ_ARROW_RIGHT_PRICE
   C_OBJ_TREND       AxisZ;         // объявление экземпляра класса C_OBJ_TREND
   C_OBJ_TRENDBYANGLE AxisY,AxisX;  // объявление экземпляров класса C_OBJ_TRENDBYANGLE
   //---
   CGM               gCentr;        // объявление экземпляра класса CGM
   CGM               gAxisY,gAxisX; // объявление экземпляров класса CGM

public:
                     CUSC();
                    ~CUSC();
   //--- Расчёт координаты Z
   sPointCoordinates Z(double price,   // цена в системе координат "График"
                       int barX,       // смещение по оси X
                       int barY);      // смещение по оси Y
   //--- новый бар
   bool on_bar()
     {
      m_next_bar=T(0);
      if(m_next_bar>m_prev_bar)
        {
         m_prev_bar=m_next_bar;
         return(true);
        }
      return(false);
     }
   //---
   virtual     // Метод: создать IGO объект
   void              Create(string name);
   virtual     // Метод обработки события OnChartEvent
   void              OnEvent(const int id,
                             const long &lparam,
                             const double &dparam,
                             const string &sparam);
  };

Рассмотрим подробно только три метода этого класса: Create(), OnEvent(), Z().

Метод Create: создание трёхмерной системы координат 

Создаёт 3D-объект интерактивной системы координат. Интерактивность включает: вращение системы координат вокруг оси Z и перемещение точки начала координат.

void CUSC::Create(string name)
  {
//--- Центр трехмерной системы координат
   Centr.Create("Axis XYZ");        // создаём центр системы координат
   gCentr.Create(Centr.m_name);     // создаём объект графической памяти
   m_price=gCentr.R_OBJPROP_PRICE();
//--- Ось Z
   AxisZ.Create("Axis Z");          // создаём ось Z системы координат
//--- Ось Y
   AxisY.Create("Axis Y",                 // создаём ось Y системы координат
                gCentr.R_OBJPROP_PRICE(), // получим значение из GM
                30);                      // задаём угол наклона оси 30 градусов
   gAxisY.Create(AxisY.m_name);           // создаём объект графической памяти
   m_angle=gAxisY.R_OBJPROP_ANGLE();
//--- Ось X
   AxisX.Create("Axis X",                       // создаём ось X системы координат
                gCentr.R_OBJPROP_PRICE(),       // получим значение из GM
                gAxisY.R_OBJPROP_ANGLE()+ISO);  // получим значение из GM и увеличим его на ISO градусов
   gAxisX.Create(AxisX.m_name);                 // создаём объект графической памяти
//---
   ChartRedraw();
   on_event=true; // разрешаем обработку событий
  }

Параметры:

 name

   [in] Имя системы координат.

Возвращаемое значение:

 Нет возвращаемого значения. В случае успеха создаётся интерактивная система координат.

 

Метод OnEvent: обработка поступающих сообщений

Обрабатывает события, поступающие от клиентского терминала при работе с графиком. Метод реагирует только на одно стандартное событие: перетаскивание графического объекта. Остальные события транслируются далее по цепочке всем экземплярам классов, созданных при программировании системы координат.

void CUSC::OnEvent(const int id,
                   const long &lparam,
                   const double &dparam,
                   const string &sparam)
  {
   if(on_event) // обработка событий разрешена
     {
      //--- трансляция событий OnChartEvent
      AxisZ.OnEvent(id,lparam,dparam,sparam);
      //---
      if(Centr.OnEvent(id,lparam,dparam,sparam))
        {// Перетаскивание графического объекта
         AxisY.OnEvent(id,lparam,dparam,sparam,Event_1,gCentr.R_OBJPROP_PRICE());
         AxisX.OnEvent(id,lparam,dparam,sparam,Event_1,gCentr.R_OBJPROP_PRICE());
        }
      else
        {
         if(AxisY.OnEvent(id,lparam,dparam,sparam))
           {// Изменение угла графического объекта
            AxisX.OnEvent(id,lparam,dparam,sparam,Event_2,gAxisY.R_OBJPROP_ANGLE()+ISO);
           }
         else
           {
            if(AxisX.OnEvent(id,lparam,dparam,sparam))
              {// Изменение угла графического объекта
               AxisY.OnEvent(id,lparam,dparam,sparam,Event_2,gAxisX.R_OBJPROP_ANGLE()-ISO);
              }
           }
        }
      ChartRedraw();
      m_price=gCentr.R_OBJPROP_PRICE();
      m_angle=gAxisY.R_OBJPROP_ANGLE();
     }
  }

Параметры: 

 id

   [in] Идентификатор события. Существуют 9 видов событий, которые можно обрабатывать с помощью данного метода.

 lparam

   [in] Параметр события типа long.

 dparam

   [in] Параметр события типа double.

 sparam

   [in] Параметр события типа string.

 iparametr

   [in] Идентификатор пользовательского события.

 dparametr

   [in] Параметр пользовательского события типа double.

Возвращаемое значение:

 Нет возвращаемого значения.

 

Структура для получения значений координат (sPointCoordinates).

Структура для хранения значений координат в системе координат "График". Предназначена для получения координат 3D-точки. 

struct sPointCoordinates
  {
   datetime          time;    // координата в системе "График"
   double            price;   // координата в системе "График"
  };

Переменная типа sPointCoordinates позволяет за один вызов функции Z() получить значения координат 3D точки в системе координат "График".

 

Метод Z: расчёт координаты Z. 

Метод производит расчёт координаты Z.

sPointCoordinates CUSC::Z(double price,int barX,int barY)
  {
   sPointCoordinates res;
   res.price=0;
   res.time=0;
   double dX,dY;
   dX=0;
   dX=gAxisX.R_ValueByTime(T(barX))-m_price;
   dY=0;
   dY=gAxisY.R_ValueByTime(T(-barY))-m_price;
   res.price=price+dX-dY;
   res.time=T(barX-barY);
   return(res);
  }

Параметры: 

 price

   [in] Координата Z 3D-объекта.

 barX

   [in] Координата X 3D-объекта. Значение задаётся в барах.

 barY

   [in] Координата Y 3D-объекта. Значение задаётся в барах.

Возвращаемое значение:

  В случае успеха возвращает значение переменной типа sPointCoordinates. 

 

Пример создания 3D поверхности в виде сетки

В качестве примера рассмотрим построение трёхмерной поверхности по функции:

fun[i][j]=close[i*StepX]-_Point*j*j
Практического смысла в этой формуле нет — это довольно простая и удачная демонстрация возможностей разработанных классов. Код достаточно лаконичен и доступен для понимания и различных экспериментов программистами разного уровня подготовки. Запустив этот индикатор, получим на графике 3D-сетку поверхности по заданной функции (см. рис. 6). Можно менять точку взгляда путём изменения углов осей Y или X. При этом фактически происходит вращение объекта вокруг оси Z. Перемещение центра координат приведёт к изменению расскраски модели: красный цвет — значения узлов по оси Z выше центральной точки 0, а синий — соответственно, ниже центра координат.
//--- Объявление констант
#define  StepX    10    // шаг по оси Х [bar]
#define  StepY    5     // шаг по оси Y [bar]
#define  _X       50    // количество точек по оси Х
#define  _Y       15    // количество точек по оси Y
#define  W1       1     // толщина линий
#define  W2       3     // толщина крайних линий
//--- Подключаем файлы классов
#include <3D\USC.mqh>
//---
#property indicator_chart_window
//--- Количество буферов для расчета индикатора
#property indicator_buffers 0
//--- Количество графических серий в индикаторе
#property indicator_plots   0
//---
CUSC        USC;
double fun[_X][_Y];
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
   USC.Create("3D");
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
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[])
  {
   ArraySetAsSeries(close,true);
//--- Функция 3D-поверхности
   for(int i=0;i<_X;i++)
      for(int j=0;j<_Y;j++)
        {
         fun[i][j]=close[i*StepX]-_Point*j*j;
         //fun[i][j]=close[i*StepX]-_Point*j*j*i/7;
        }

////--- линии X
   for(int i=1;i<_X;i++)
      for(int j=0;j<_Y;j++)
        {
         sPointCoordinates a0=USC.Z(fun[(i-1)][j], // функция
                                    (i-1)*StepX,   // X
                                    -j*StepY);     // Y
         sPointCoordinates a1=USC.Z(fun[i][j],     // функция
                                    i*StepX,       // X
                                    -j*StepY);     // Y
         string name="line x "+"x"+(string)i+"y"+(string)+j;
         ObjectCreate(0,name,OBJ_TREND,0,a0.time,a0.price,a1.time,a1.price);
         if(fun[i][j]>USC.m_price && fun[i-1][j]>USC.m_price)
            ObjectSetInteger(0,name,OBJPROP_COLOR,clrRed);
         else
            ObjectSetInteger(0,name,OBJPROP_COLOR,clrBlue);
         ObjectSetInteger(0,name,OBJPROP_WIDTH,W1);
         if(j==0 || j==_Y-1)
            ObjectSetInteger(0,name,OBJPROP_WIDTH,W2);
         ObjectSetInteger(0,name,OBJPROP_STYLE,STYLE_SOLID);
         ObjectSetInteger(0,name,OBJPROP_RAY_RIGHT,false);
         ObjectSetInteger(0,name,OBJPROP_RAY_LEFT,false);
         ObjectSetInteger(0,name,OBJPROP_BACK,true);
         ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
        }
////--- линии Y
   for(int i=0;i<_X;i++)
      for(int j=1;j<_Y;j++)
        {
         sPointCoordinates a0=USC.Z(fun[i][j-1],   // функция
                                    i*StepX,       // X
                                    -(j-1)*StepY); // Y
         sPointCoordinates a1=USC.Z(fun[i][j],     // функция
                                    i*StepX,       // X
                                    -j*StepY);     // Y
         string name="line y "+"x"+(string)i+"y"+(string)+j;
         ObjectCreate(0,name,OBJ_TREND,0,a0.time,a0.price,a1.time,a1.price);
         ObjectSetInteger(0,name,OBJPROP_COLOR,clrGreen);
         ObjectSetInteger(0,name,OBJPROP_WIDTH,1);
         ObjectSetInteger(0,name,OBJPROP_STYLE,STYLE_SOLID);
         ObjectSetInteger(0,name,OBJPROP_RAY_RIGHT,false);
         ObjectSetInteger(0,name,OBJPROP_RAY_LEFT,false);
         ObjectSetInteger(0,name,OBJPROP_BACK,true);
         ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
        }
//--- return value of prev_calculated for next call
   return(rates_total);
  }
//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
   USC.OnEvent(id,lparam,dparam,sparam);
  }

Линии в плоскостях, параллельных плоскости XZ и YZ рисуются раздельно. Можно поварьировать количеством узлов по каждой из осей и самой функцией.

Рис. 6. Пример построения 3D поверхности.

Рис. 6. Пример построения 3D-поверхности.

Рис. 7. Пример построения 3D индикатора Moving Average.

Рис. 7. Пример построения 3D-индикатора Moving Average. 

Ответ на вопрос скептиков: Зачем нужно 3D-моделирование и как оно поможет в торговле?

Конечно же, терминал MetaTrader 5 предназначен для торговли на финансовых рынках. Трейдеров и разработчиков АТС в первую очередь интересуют торговые алгоритмы и построение прибыльных стратегий. Пока рассмотренное направление находится в зачаточном состоянии и конкретных результатов в торговле не даст, но 3D-моделирование может быть использовано в презентации ваших торговых идей и стратегий для потенциальных инвесторов. Кроме того, в терминале создаются динамические 3D-модели, которые реагируют на поступление рыночной информации и деформируются во времени (перестраивают своё тело), а это даёт возможность создания 3D-индикаторов.

Приведу в качестве аргумента аналогию с  Windows: первые версии системы были неуклюжими и медленными. Сторонники Norton Commander скептически относились к идее графических интерфейсов. И где сейчас NC?

Заключение

  1. 3D-моделирование — достаточно сложная задача в любой среде программирования, но разработчики языка MQL5 сработали на опережение и предоставили пользователям достаточно мощный функционал для реализации трёхмерной визуализации в торговом терминале.
  2. Получены пока только первые результаты программирования библиотеки классов 3D-объектов. Тема 3D-моделирования настолько масштабна, что одной статьёй охватить все ее возможности и перспективы невозможно. Надеюсь, найдутся ее последователи и предложат пути развития 3D-направления в трейдинге и в программировании на языке MQL5.
  3. Созданный инструмент 3D-моделирования может оказать существенное влияние на создание нового направления в техническом анализе: трёхмерные индикаторы и их анализ.
  4. Есть проблемы с быстродействием рендеринга, но на данном этапе разработки библиотеки это пока не критично.
  5. Принцип построения 3D-объектов, предложенный в статье, скорее всего, подойдет для переноса на OpenCL.

Примечание:

Файлы библиотеки должны быть размещены в папке ..\Include\3D.