English 中文 Español Deutsch 日本語 Português
preview
Бегущая строка котировок: базовая версия

Бегущая строка котировок: базовая версия

MetaTrader 5Трейдинг | 13 января 2023, 11:28
1 935 0
Daniel Jose
Daniel Jose

Введение

Многим нравтся такие бегущие строки, в которых отображаются котировки отдельных активов — такие панели есть в некоторых платформах. Если вы не понимаете, о чем идет речь, посмотрите на картинку ниже:

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

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

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


Планирование

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

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

Это, пожалуй, самая сложная часть из всех, поскольку в некоторых случаях вам может захотеться иметь активы, представляющие наибольший интерес, а в другие моменты - просто держать те активы, которые находятся в портфеле. Поэтому, возможно, лучше использовать один файл, содержащий все активы, которые вы хотите использовать в панели котировок. Давайте начнем: Мы будем использовать ФАЙЛ, который содержит активы для отображения.

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

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

Итак, у нас осталось два варианта — советник и индикатор. Однако мне не нравится идея использования советника, потому что я люблю использовать советники только по прямому назначению, то есть для управления системой ордеров. Всё это оставляет нам только одно возможное решение — использовать индикатор.

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


Основные принципы

Давайте начнем с создания файла индикатора, тогда можно будет увидеть его внешний вид на этой ранней стадии:

#property copyright "Daniel Jose"
#property description "Program for a panel of quotes."
#property description "It creates a band that displays asset prices."
#property description "For details on how to use it visit:\n"
#property description "https://www.mql5.com/ru/articles/10941"
#property link "https://www.mql5.com/ru/articles/10941"
#property indicator_separate_window
#property indicator_plots 0
//+------------------------------------------------------------------+
int OnInit()
{
        return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[])
{
        return rates_total;
}
//+------------------------------------------------------------------+
void OnTimer()
{
}
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
}
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
}
//+------------------------------------------------------------------+

Хотя код индикатора полностью чист (это означает, что он не будет делать ничего особенного), у нас уже есть небольшое представление о том, чего ожидать, поэтому мы будем использовать отдельное окно, и нам понадобится обрабатывать большее количество событий, чем обычно в классическом индикаторе, например, OnTime, который обычно не появляется ни в одном индикаторе. Также не следует забывать о следующем: Мы вообще ничего не будем строить, потому что всё, что будет создавать и отображать индикатор, будет исключительно он сам.

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

Итак, вы, наверное, уже задумались о том, что нам понадобится еще много разных вещей, чтобы всё это действительно работало. В каком-то смысле так и будет, но речь идет лишь о небольшом количестве вещей. Первое, о чем нужно начать думать - это как управлять графиком, и для этого у нас есть класс. Хотя многие видели это класс в других моих опубликованных статьях, здесь у него будет немного другой вид, так как нам нужно будет использовать гораздо меньше вещей. Поэтому сейчас я презентую класс для тех, кто его не знает - это класс C_Terminal. Он находится в заголовочном файле C_Terminal.mqh, его код довольно простой, поэтому ниже мы рассмотрим его полностью:

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
class C_Terminal
{
//+------------------------------------------------------------------+
        private :
                struct st00
                {
                        long    ID;
                        int     Width,
                                Height,
                                SubWin;
                }m_Infos;
//+------------------------------------------------------------------+
        public  :
//+------------------------------------------------------------------+          
                void Init(const int WhatSub)
                        {
                                ChartSetInteger(m_Infos.ID = ChartID(), CHART_EVENT_OBJECT_DELETE, m_Infos.SubWin = WhatSub, true);
                                Resize();
                        }
//+------------------------------------------------------------------+
inline long Get_ID(void)   const { return m_Infos.ID; }
inline int GetSubWin(void) const { return m_Infos.SubWin; }
inline int GetWidth(void)  const { return m_Infos.Width; }
inline int GetHeight(void) const { return m_Infos.Height; }
//+------------------------------------------------------------------+
                void Resize(void)
                        {
                                m_Infos.Width = (int) ChartGetInteger(m_Infos.ID, CHART_WIDTH_IN_PIXELS);
                                m_Infos.Height = (int) ChartGetInteger(m_Infos.ID, CHART_HEIGHT_IN_PIXELS);
                        }
//+------------------------------------------------------------------+
inline string ViewDouble(double Value)
                        {
                                Value = NormalizeDouble(Value, 8);
                                return DoubleToString(Value, ((Value - MathFloor(Value)) * 100) > 0 ? 2 : 0);
                        }
//+------------------------------------------------------------------+
                void Close(void)
                        {
                                ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, m_Infos.SubWin, false);
                        }
//+------------------------------------------------------------------+          
};
//+------------------------------------------------------------------+

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

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

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

Обратите внимание, до этого момента всё было довольно просто и понятно, но сейчас мы начнем усложнять.


Реализация основных объектов

Как бы странно это ни показалось, но в этой самой базовой модели панели мы будем использовать только два объекта. Также я буду использовать модель, которую часто можно увидеть в моей авторской серии статей Разработка торгового советника с нуля, опубликованной в сообществе MQL5, поэтому и систему я возьму оттуда. И хотя в серии статей о советниках есть более подробное (в отличие от данной статьи) описание и применение, я всё же дам короткое объяснение тому, как работает система, чтобы вы не запутались, если вам не хватает знаний о принципе работы MetaTrader 5 с объектами.

Итак, давайте начнем с базового класса объектов, код которого мы можем увидеть чуть ниже:

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#include "..\Auxiliar\C_Terminal.mqh"
//+------------------------------------------------------------------+
class C_Object_Base
{
        public  :
//+------------------------------------------------------------------+
virtual void Create(string szObjectName, ENUM_OBJECT typeObj)
                        {
                                ObjectCreate(Terminal.Get_ID(), szObjectName, typeObj, Terminal.GetSubWin(), 0, 0);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_SELECTABLE, false);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_SELECTED, false);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BACK, true);
                                ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_TOOLTIP, "\n");
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BACK, false);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_CORNER, CORNER_LEFT_UPPER);
                        };
//+------------------------------------------------------------------+
                void PositionAxleX(string szObjectName, int X)
                        {
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_XDISTANCE, X);
                        };
//+------------------------------------------------------------------+
                void PositionAxleY(string szObjectName, int Y, int iArrow = 0)
                        {
                                int desl = (int)ObjectGetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_YSIZE);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_YDISTANCE, (iArrow == 0 ? Y - (int)(desl / 2) : (iArrow == 1 ? Y : Y - desl)));
                        };
//+------------------------------------------------------------------+
virtual void SetColor(string szObjectName, color cor)
                        {
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_COLOR, cor);
                        }
//+------------------------------------------------------------------+
                void Size(string szObjectName, int Width, int Height)
                        {
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_XSIZE, Width);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_YSIZE, Height);
                        };
//+------------------------------------------------------------------+
};

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

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

Хотя это может показаться абсурдом, но создание такой абстракции очень поможет нам в дальнейшем, поскольку все объекты будут обрабатываться уникальным образом, каким бы ни был сам объект. И это дает нам некоторые преимущества, которые мы, однако, оставим на другой раз, поэтому давайте посмотрим, какой объект будет выбран для создания панели. В качестве объекта выбираем OBJ_EDIT. Ниже мы приводим полный его код:

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#include "C_Object_Base.mqh"
//+------------------------------------------------------------------+
#define def_ColorNegative       clrCoral
#define def_ColoPositive        clrPaleGreen
//+------------------------------------------------------------------+
class C_Object_Edit : public C_Object_Base
{
        public  :
//+------------------------------------------------------------------+
                template < typename T >
                void Create(string szObjectName, color corTxt, color corBack, T InfoValue)
                        {
                                C_Object_Base::Create(szObjectName, OBJ_EDIT);
                                ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_FONT, "Lucida Console");
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_FONTSIZE, 10);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_ALIGN, ALIGN_LEFT);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_COLOR, corTxt);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BGCOLOR, corBack);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BORDER_COLOR, corBack);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_READONLY, true);
                                if (typename(T) == "string") ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_TEXT, (string)InfoValue); else SetTextValue(szObjectName, (double)InfoValue);
                        };
//+------------------------------------------------------------------+
                void SetTextValue(string szObjectName, double InfoValue, color cor = clrNONE)
                        {
                                color clr;
                                clr = (cor != clrNONE ? cor : (InfoValue < 0.0 ? def_ColorNegative : def_ColoPositive));                                
                                ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_TEXT, Terminal.ViewDouble(InfoValue < 0.0 ? -(InfoValue) : InfoValue));
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_COLOR, clr);
                        };
//+------------------------------------------------------------------+
};
//+------------------------------------------------------------------+
#undef def_ColoPositive
#undef def_ColorNegative
//+------------------------------------------------------------------+

Неужели это всё? Да, хотя он немного отличается от кода, который мы найдем в системе ордеров советника, но это всё. Здесь у нас есть функция для размещения значений типа double, а это тип, который мы действительно часто используем в MQL5; у нас есть также создание объекта редактирования типа Obj_Edit, но также есть одна вещь, которая может оставить вас в замешательстве, если вы только начали программировать. Посмотрите внимательнее на функцию создания объекта, в приведенном ниже фрагменте:

template < typename T >
void Create(string szObjectName, color corTxt, color corBack, T InfoValue)

Эти две строки на самом деле рассматриваются компилятором как одна строка, но знаете ли вы, что на самом деле здесь происходит, или вы совсем запутались? Может вам кажется, что я нарочно усложняю?

Итак, когда мы используем 'template < typename T > ', T может быть заменен на что угодно другое до тех пор, пока это соответствует действующим правилам наименования. На самом деле мы показываем как выглядит перегрузка, которая встречается довольно часто. Много раз нам приходилось создавать одинаковые функции, но очень распространено то, что они получают различные аргументы или типы данных. Именно поэтому, чтобы облегчить нашу жизнь в такие моменты, мы используем такой синтаксис, который может показаться немного странным для некоторых, но он очень распространен в случаях, когда мы не хотим переписывать всю функцию только потому что, например, одни из наших данных отличаются, но всё внутреннее тело функции одинаково.

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

if (typename(T) == "string") ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_TEXT, (string)InfoValue); else SetTextValue(szObjectName, (double)InfoValue);

Этот код делает следущее, а именно проверяет, о каком виде данных сообщается в переменной InfoValue. Важно отметить, что я говорю именно ТИП, а не ЗНАЧЕНИЕ, не путайте эти два понятия.

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

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

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

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

Но прежде чем мы закончим эту тему об объектах, я не могу не обратить внимания на объект, который будет находиться в нижней части панели, потому что нам нужно создать фон, вы же не ожидаете, что всё будет хорошо работать и без фона? Но не волнуйтесь, код для этого очень прост, и его можно увидеть ниже:

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#include "C_Object_Base.mqh"
//+------------------------------------------------------------------+
class C_Object_BackGround : public C_Object_Base
{
        public:
//+------------------------------------------------------------------+
                void Create(string szObjectName, color cor)
                        {
                                C_Object_Base::Create(szObjectName, OBJ_RECTANGLE_LABEL);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BORDER_TYPE, BORDER_FLAT);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_CORNER, CORNER_LEFT_UPPER);
                                this.SetColor(szObjectName, cor);
                        }
//+------------------------------------------------------------------+
virtual void SetColor(string szObjectName, color cor)
                        {
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_COLOR, cor);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BGCOLOR, cor);
                        }
//+------------------------------------------------------------------+
};
//+------------------------------------------------------------------+

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

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


Реализация основного класса

Итак, пока что мы подготовили почву для этого этапа, который, кстати, является самым интересным из всех, поскольку именно здесь мы действительно заставим систему работать. Код для этого находится в заголовочном файле C_Widget.mqh. Давайте просмотрим его с начальных объявлений, которые вы можете увидеть чуть ниже:

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#include "Elements\C_Object_Edit.mqh"
#include "Elements\C_Object_BackGround.mqh"
//+------------------------------------------------------------------+
C_Terminal Terminal;
//+------------------------------------------------------------------+
#define def_PrefixName          "WidgetPrice"
#define def_NameObjBackGround	def_PrefixName + "BackGround"
#define def_MaxWidth            80
//+------------------------------------------------------------------+
#define def_CharSymbol          "S"
#define def_CharPrice           "P"
//+------------------------------------------------------------------+
#define macro_MaxPosition (Terminal.GetWidth() >= (m_Infos.nSymbols * def_MaxWidth) ? Terminal.GetWidth() : m_Infos.nSymbols * def_MaxWidth)
#define macro_ObjectName(A, B) (def_PrefixName + (string)Terminal.GetSubWin() + A + "#" + B)
//+------------------------------------------------------------------+

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

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

Когда это сделано, мы объявляем класс с его начальными переменными.

class C_Widget
{
        protected:
                enum EventCustom {Ev_RollingTo};
        private :
                struct st00
                {
                        color   CorBackGround,
                                CorSymbol,
                                CorPrice;
                        int     nSymbols,
                                MaxPositionX;
                        struct st01
                        {
                                string szCode;
                        }Symbols[];
                }m_Infos;

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

Теперь перейдем к реальным действиям, и первое из которых показано ниже:

void CreateBackGround(void)
{
        C_Object_BackGround backGround;
                        
        backGround.Create(def_NameObjBackGround, m_Infos.CorBackGround);
        backGround.Size(def_NameObjBackGround, Terminal.GetWidth(), Terminal.GetHeight());
}

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

void AddSymbolInfo(const string szArg, const bool bRestore = false)
        {
#define macro_Create(A, B, C)   {                                               \
                edit.Create(A, m_Infos.CorSymbol, m_Infos.CorBackGround, B);    \
                edit.PositionAxleX(A, def_MaxWidth * m_Infos.nSymbols);         \
                edit.PositionAxleY(A, C);                                       \
                edit.Size(A, def_MaxWidth - 1, 22);                             \
                                }
                        
                C_Object_Edit edit;

                macro_Create(macro_ObjectName(def_CharSymbol, szArg), szArg, 10);
                macro_Create(macro_ObjectName(def_CharPrice, szArg), 0.0, 32);
                if (!bRestore)
                {
                        ArrayResize(m_Infos.Symbols, m_Infos.nSymbols + 1, 10);
                        m_Infos.Symbols[m_Infos.nSymbols].szCode = szArg;
                        m_Infos.nSymbols++;
                }
#undef macro_Create
        }

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

Здесь мы создаем объект типа C_Object_Edit, выполняем его временное позиционирование и сообщаем о размере, который он должен иметь . И всё это в пределах макроса. Уже в этих точках мы делаем запрос через макрос, чтобы код было легче читать, так как весь процесс практически одинаков. Конечно, есть вопрос о значениях, но процедура одна и та же, поэтому мы используем макрос, чтобы максимально облегчить всё. Запомните: "меньше печатаем, больше производим".

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

Продолжая в порядке появления кодов, следующая функция довольно интересна, давайте посмотрим на нее:

inline void UpdateSymbolInfo(const int x, const string szArg)
{
        C_Object_Edit edit;
        string sz0 = macro_ObjectName(def_CharPrice, szArg);
        MqlRates Rate[1];
                                
        CopyRates(szArg, PERIOD_M1, 0, 1, Rate);                                
        edit.PositionAxleX(macro_ObjectName(def_CharSymbol, szArg), x);
        edit.SetTextValue(sz0, Rate[0].close, m_Infos.CorPrice);
        edit.PositionAxleX(sz0, x);
}

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

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

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

Ниже показана следующая последовательность действий:

bool LoadConfig(const string szFileConfig)
{
        int file;
        string sz0;
        bool ret;
                                
        if ((file = FileOpen("Widget\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE)
        {
                PrintFormat("Configuration file %s not found.", szFileConfig);
                return false;
        }
        m_Infos.nSymbols = 0;
        ArrayResize(m_Infos.Symbols, 30, 30);
        for (int c0 = 1; (!FileIsEnding(file)) && (!_StopFlag); c0++)
        {
                if ((sz0 = FileReadString(file)) == "") continue;
                if (SymbolExist(sz0, ret)) AddSymbolInfo(sz0); else
                {
                        FileClose(file);
                        PrintFormat("Ativo na linha %d não foi reconhecido.", c0);
                        return false;
                }
        }
        FileClose(file);
        m_Infos.MaxPositionX = macro_MaxPosition;
                
        return !_StopFlag;
}

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

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

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

~C_Widget()
{
        Terminal.Close();
        ObjectsDeleteAll(Terminal.Get_ID(), def_PrefixName);
        ArrayFree(m_Infos.Symbols);
}

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

В следующем коде у нас есть система инициализации класса:

bool Initilize(const string szFileConfig, const string szNameShort, color corText, color corPrice, color corBack)
{
        IndicatorSetString(INDICATOR_SHORTNAME, szNameShort);
        Terminal.Init(ChartWindowFind());
        Terminal.Resize();
        m_Infos.CorBackGround = corBack;
        m_Infos.CorPrice = corPrice;
        m_Infos.CorSymbol = corText;
        CreateBackGround();

        return LoadConfig(szFileConfig);
}

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

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

void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        static int tx = 0;
        string szRet[];
                                                        
        switch (id)
        {
                case (CHARTEVENT_CUSTOM + Ev_RollingTo):
                        tx = (int) (tx + lparam);
                        tx = (tx < -def_MaxWidth ? m_Infos.MaxPositionX : (tx > m_Infos.MaxPositionX ? -def_MaxWidth : tx));
                        for (int c0 = 0, px = tx; (c0 < m_Infos.nSymbols); c0++)
                        {
                                if (px < Terminal.GetWidth()) UpdateSymbolInfo(px, m_Infos.Symbols[c0].szCode);
                                px += def_MaxWidth;
                                px = (px > m_Infos.MaxPositionX ? -def_MaxWidth + (px - m_Infos.MaxPositionX) : px);
                        }
                        ChartRedraw();
                        break;
                case CHARTEVENT_CHART_CHANGE:
                        Terminal.Resize();
                        m_Infos.MaxPositionX = macro_MaxPosition;
                        ChartRedraw();
                        break;
                case CHARTEVENT_OBJECT_DELETE:
                        if (StringSubstr(sparam, 0, StringLen(def_PrefixName)) == def_PrefixName) if (StringSplit(sparam, '#', szRet) == 2)
                        {
                                AddSymbolInfo(szRet[1], true);
                                ChartRedraw();
                        }else if (sparam == def_NameObjBackGround)
                        {
                                ObjectsDeleteAll(Terminal.Get_ID(), def_PrefixName);
                                CreateBackGround();
                                for (int c0 = 0; c0 < m_Infos.nSymbols; c0++) AddSymbolInfo(m_Infos.Symbols[c0].szCode, true);
                                ChartRedraw();
                        }
                        break;
        }
}

Большая часть этого кода довольно проста, у нас есть 2 события, сгенерированные платформой, которые передаются индикатору для обработки, но у нас также есть тип события, который для многих не имеет смысла, поскольку это пользовательское событие. Этот тип события довольно распространен в некоторых типах проектов, но здесь он служит больше для того, чтобы мы могли централизовать обработку сообщений или событий, которые могут происходить. Хотя многие этого не понимают, платформа MetaTrader 5 и язык MQL5 являются событийно-ориентированными, и это означает, что мы не работаем с процедурами, а работаем с событиями и обрабатываем их по мере появления.

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

#property copyright "Daniel Jose"
#property description "Program for a panel of quotes."
#property description "It creates a band that displays asset prices."
#property description "For details on how to use it visit:\n"
#property description "https://www.mql5.com/ru/articles/10941"
#property link "https://www.mql5.com/ru/articles/10941"
#property indicator_separate_window
#property indicator_plots 0
#property indicator_height 45
//+------------------------------------------------------------------+
#include <Widget\Rolling Price\C_Widget.mqh>
//+------------------------------------------------------------------+
input string    user00 = "Config.cfg";  //Файл настроек
input int       user01 = -1;            //Смещение
input int       user02 = 60;            //Пауза в миллисекундах
input color     user03 = clrWhiteSmoke; //Цвет актива
input color     user04 = clrYellow;     //Цвет цены
input color     user05 = clrBlack;      //Цвет фона
//+------------------------------------------------------------------+
C_Widget Widget;
//+------------------------------------------------------------------+
int OnInit()
{
        if (!Widget.Initilize(user00, "Widget Price", user03, user04, user05))
                return INIT_FAILED;
        EventSetMillisecondTimer(user02);
        
        return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[])
{
        return rates_total;
}
//+------------------------------------------------------------------+
void OnTimer()
{
        EventChartCustom(Terminal.Get_ID(), C_Widget::Ev_RollingTo, user01, 0.0, "");
}
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        Widget.DispatchMessage(id, lparam, dparam, sparam);
}
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
        EventKillTimer();
}
//+------------------------------------------------------------------+

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

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

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

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

И тот факт, что мы реализуем вызов через EventChartCustom, имеет еще одно преимущество - этот вызов может прийти из любой точки кода, т.е. независимо от того, откуда мы осуществляем вызов, событие ChartEvent всегда сработает, а это, соответственно, вызовет OnChartEvent, так что всё будет развиваться гораздо более естественным образом.

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

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

void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        static int tx = 0;
        string szRet[];
                                                        
        switch (id)
        {
                case (CHARTEVENT_CUSTOM + Ev_RollingTo):
                        tx = (int) (tx + lparam);
                        tx = (tx < -def_MaxWidth ? m_Infos.MaxPositionX : (tx > m_Infos.MaxPositionX ? -def_MaxWidth : tx));
                        for (int c0 = 0, px = tx; (c0 < m_Infos.nSymbols); c0++)
                        {
                                if (px < Terminal.GetWidth()) UpdateSymbolInfo(px, m_Infos.Symbols[c0].szCode);
                                px += def_MaxWidth;
                                px = (px > m_Infos.MaxPositionX ? -def_MaxWidth + (px - m_Infos.MaxPositionX) : px);
                        }
                        ChartRedraw();
                        break;

Математика, задействованная в приведенном выше фрагменте, многим может показаться запутанной, но то, что мы тут видим - это использование значения, предоставленного пользователем, для перемещения объектов на определенное расстояние. Т.е., если значение положительное, объект будет двигаться слева направо; если отрицательное - справа налево; а если нулевое, объект останется неподвижным. Идея проста, но где же расчеты, которые не видны?! Вот почему было сказано, что приведенный выше код кажется запутанным, поскольку именно эти две строки выполняют вычисления.

Однако, вы можете до конца не понимать, как это возможно, и как такой простой расчет умудряется это делать, но если вы будете достаточно внимательны, то увидите, что в расчете задействованы границы, и когда граница достигается сразу, то позиция пересчитывается на непосредственно соседнюю границу. Таким образом мы замыкаем цикл, что приводит к тому, что когда значение достигается в данной точке, то оно корректируется, чтобы начать в той точке, которая будет находиться на противоположном пределе. Чтобы лучше понять это, можно сказать так: если бы мы считали от 0 до 99 и не знали, как считать дальше этих значений, то что бы произошло, если бы мы попытались добавить 1 к 99? Следуя логике, мы получили бы 100.

Верно... но не в этом случае, мы бы сразу вернулись к 0, но если вы попытаетесь прибавить 3 к 98, мы не получим значение больше 99, мы получим значение 1; это кажется странным, но это так; то же самое, если мы вычитаем: когда мы попытаемся отнять 3 от 2, мы получим в результате 99.... Это не бред 😵 😵 😵... но это основа системы подсчета компьютера, если вы спокойно изучите, то увидите, что компьютер не может считать бесконечные числа, наоборот, есть граница максимально получаемого значения, и это относится к другой области, которая называется криптографией, но это уже другая история. 

Давайте вернемся к коду. Если вы не понимаете то, что я объяснил выше, тогда постарайтесь сначала понять это, потому что, когда мы перейдем к циклу FOR, всё будет выглядеть еще более странно.

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

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

А затем цикл повторится до тех пор, пока индикатор не закроется, так что на экране появится вся информация, и сколько бы её было, она появятся в полном объёме.

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

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

На видео ниже можно увидеть систему в действии с данными из активов IBOV (Ibovespa Index), но это только для наглядности работы системы...




Заключение

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


Перевод с португальского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/pt/articles/10941

Прикрепленные файлы |
Как работать с линиями средствами MQL5 Как работать с линиями средствами MQL5
В этой статье мы поговорим о том, как работать с наиболее важными линиями, такими как линии тренда, поддержка и сопротивление, используя средства языка MQL5.
Популяционные алгоритмы оптимизации: Алгоритм летучих мышей (Bat algorithm - BA) Популяционные алгоритмы оптимизации: Алгоритм летучих мышей (Bat algorithm - BA)
Сегодня изучим алгоритм летучих мышей (Bat algorithm - BA), который отличается удивительной сходимостью на гладких функциях.
Эксперименты с нейросетями (Часть 3): Практическое применение Эксперименты с нейросетями (Часть 3): Практическое применение
Нейросети наше все. Проверяем на практике, так ли это. MetaTrader 5 как самодостаточное средство для использования нейросетей в трейдинге. Простое объяснение.
Возможности Мастера MQL5, которые вам нужно знать (Часть 04): Линейный дискриминантный анализ Возможности Мастера MQL5, которые вам нужно знать (Часть 04): Линейный дискриминантный анализ
Современный трейдер почти всегда находится в поиске новых идей. Он постоянно пробует новые стратегии, модифицирует их и отбрасывает те, что не оправдали себя. В этой серии статей я постараюсь доказать, что Мастер MQL5 является настоящей опорой трейдера в его поисках.