English 中文 Español Deutsch 日本語 Português 한국어 Français Italiano Türkçe
preview
Разработка торгового советника с нуля (Часть 7): Добавляем Volume At Price (I)

Разработка торгового советника с нуля (Часть 7): Добавляем Volume At Price (I)

MetaTrader 5Трейдинг | 18 мая 2022, 15:05
1 876 1
Daniel Jose
Daniel Jose

Введение

Те, кто торгует и старается иметь определенную степень уверенности, не могут не иметь этот индикатор на своем графике. Чаще всего его используют те, кто торгует, наблюдая за лентой сделок (tape reading). Но также его могут использовать и те, кто торгует только по Price Action. Это чрезвычайно полезный индикатор горизонтального объема, с помощью которого можно проанализировать объем сделок, которые появились во время определенной цены. Однако многие не могут правильно его прочитать. В конце статьи я оставлю ссылку, чтобы вы могли узнать больше по этому вопросу.

Здесь же мы не будем останавливаться на том, как читать показатели индикатора, потому что это выходит за рамки данной статьи. Задумка этой статьи в том, чтобы показать способ разработки и создания этого индикатора таким образом, чтобы он не ухудшал производительность платформы MetaTrader 5. Интересным фактом является то, что хотя многие представляют, что этот индикатор должен обновляться в реальном времени (Real Time), в действительности небольшая задержка приемлема, если она очень мала. Исходя из собственного опыта, я не видел больших проблем в задержке около 1 секунды в обновлении информации, но если вам важно использовать его действительно в Real Time, придется сделать небольшие изменения, но не в самом индикаторе, а в точках, где к нему обращается советник, чтобы обращение происходило в реальном времени. Однако я считаю, что влияние этого на показатели будет минимальным, поэтому задержкой можно пренебречь.


Интерфейс

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

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

    

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

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


Реализация

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

class C_WallPaper : public C_Canvas
{
        protected:
                enum eTypeImage {IMAGEM, LOGO, COR};
//+------------------------------------------------------------------+
        private :
        public  :
//+------------------------------------------------------------------+
                ~C_WallPaper()
                        {
                                Destroy();
                        }
//+------------------------------------------------------------------+
                bool Init(const string szName, const eTypeImage etype, const char cView = 100)
                        {
                                if (etype == C_WallPaper::COR) return true;
                                if (!Create(szName, 0, 0, Terminal.GetWidth(), Terminal.GetHeight())) return false;
                                if(!LoadBitmap(etype == C_WallPaper::IMAGEM ? "WallPapers\\" + szName : "WallPapers\\Logos\\" + _Symbol, cView)) return false;
                                ObjectSetInteger(Terminal.Get_ID(), szName, OBJPROP_BACK, true);

                                return true;
                        }
//+------------------------------------------------------------------+
                void Resize(void)
                        {
                                ResizeBitMap(Terminal.GetWidth(), Terminal.GetHeight());
                        }
//+------------------------------------------------------------------+
};

Посмотрите, код стал намного компактнее, мы удалили части, которые являются общими между классами C_Wallpaper и C_VolumeAtPrice, и поместили всё в другой класс, а именно в класс C_C_Canvas.

Но почему бы не использовать класс MetaTrader 5 C_Canvas? Вопрос скорее личный, чем практический. Мне нравится иметь больше контроля над всем, что я пишу и разрабатываю, но это скорее плохая привычка C-программиста, чем что-то действительно необходимое. Отсюда и возникла необходимость создать класс для рисования объектов на экране. Вам же ничто не мешает использовать класс, уже имеющийся в MetaTrader 5. Теперь давайте сосредоточимся на классе C_VolumeAtPrice, который является основной темой этой статьи. В классе семь функций, они показаны в следующей таблице.

Функция Описание Тип доступа 
Init Инициализирует класс с заданными пользователем значениями. Общий
Update Обновляет данные по Volume At Price в определенные промежутки. Общий
Resize Изменяет размер изображения Volume At Price на графике, что позволяет легче анализировать некоторые детали. Общий
DispatchMessage  Используется для отправки сообщений классу объекта. Общий
FromNowOn  Инициализирует системные переменные Приватный
SetMatrix Создает и поддерживает матрицу с данными об объеме Приватный
Redraw Создает изображение объема Приватный

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

#define def_SizeMaxBuff                 4096
//+------------------------------------------------------------------+
#define def_MsgLineLimit                "Начальная точка от Volume At Price"
//+------------------------------------------------------------------+
class C_VolumeAtPrice : private C_Canvas
{
#ifdef macroSetInteger
        ERROR ...
#endif
#define macroSetInteger(A, B) ObjectSetInteger(Terminal.Get_ID(), m_Infos.szObjEvent, A, B)
        private :
                uint    m_WidthMax,
                        m_WidthPos;
                bool    m_bChartShift,
                        m_bUsing;
                double  m_dChartShift;
                struct st00
                {
                        ulong   nVolBuy,
                                nVolSell,
                                nVolTotal;
                        long    nVolDif;
                }m_InfoAllVaP[def_SizeMaxBuff];
                struct st01
                {
                        ulong    memTimeTick;
                        datetime StartTime,
                                 CurrentTime;
                        int      CountInfos;
                        ulong    MaxVolume;
                        color    ColorSell,
                                 ColorBuy,
                                 ColorBars;
                        int      Transparency;
                        string   szObjEvent;
                        double   FirstPrice;
                }m_Infos;

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


Функция Init: с чего все начинается

Именно эта функция инициализирует все переменные правильно, в советнике она вызывается так:

//.... Начальные данные ....

input color     user10   = clrForestGreen;      //Цвет линии Take Profit
input color     user11   = clrFireBrick;        //Цвет линии Stop
input bool      user12   = true;                //Day Trade?
input group "Volume At Price"
input color     user15  = clrBlack;             //Цвет баров
input char      user16  = 20;                   //Прозрачность (от 0 до 100 )
//+------------------------------------------------------------------+
C_SubWindow             SubWin;
C_WallPaper             WallPaper;
C_VolumeAtPrice         VolumeAtPrice;
//+------------------------------------------------------------------+          
int OnInit()
{
        Terminal.Init();
        WallPaper.Init(user03, user05, user04);
        if ((user01 == "") && (user02 == "")) SubWin.Close(); else if (SubWin.Init())
        {
                SubWin.ClearTemplateChart();
                SubWin.AddThese(C_TemplateChart::SYMBOL, user02);
                SubWin.AddThese(C_TemplateChart::INDICATOR, user01);
        }
        SubWin.InitilizeChartTrade(user06, user07, user08, user09, user10, user11, user12);
        VolumeAtPrice.Init(user10, user11, user15, user16);

// ... Остальной код

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

void Init(color CorBuy, color CorSell, color CorBar, char cView)
{
        m_Infos.FirstPrice = Terminal.GetRatesLastDay().open;
        FromNowOn(macroSetHours(macroGetHour(Terminal.GetRatesLastDay().time), TimeLocal()));
        m_Infos.Transparency = (int)(255 * macroTransparency(cView));
        m_Infos.ColorBars = CorBar;
        m_Infos.ColorBuy = CorBuy;
        m_Infos.ColorSell = CorSell;
        if (m_bUsing) return;
        m_Infos.szObjEvent = "Event" + (string)ObjectsTotal(Terminal.Get_ID(), -1, OBJ_EVENT);
        CreateObjEvent();
        m_bChartShift = ChartGetInteger(Terminal.Get_ID(), CHART_SHIFT);
        m_dChartShift = ChartGetDouble(Terminal.Get_ID(), CHART_SHIFT_SIZE);
        ChartSetInteger(Terminal.Get_ID(), CHART_SHIFT, true);
        ChartSetDouble(Terminal.Get_ID(), CHART_SHIFT_SIZE, 0.1);
        Create("VaP" + (string)MathRand(), 0, 0, 1, 1);
        Resize();
        m_bUsing = true;
};

Как видим, тут все очень просто. Однако и здесь есть некоторые особенности, которые делают код интересным. Одна из них Terminal.GetRatesLastDay().open. Хотя это может для кого-то показаться странным, на самом деле очень распространена ситуация, когда мы следуем принципам объектно-ориентированного программирования (ООП). Один из этих принципов гласит, что никакой код вне класса не должен иметь доступа к внутренним переменным класса. Но как тогда получить значения переменных внутри класса? Правильным способом является использование формы, которая появляется только в ООП, поэтому давайте посмотрим, как функция GetRatesLastDay объявлена внутри класса C_Terminal, это можно увидеть во фрагменте ниже:

inline MqlRates GetRatesLastDay(void) const { return m_Infos.Rates; }

Давайте разберемся, как это работает на самом деле. Начинаем с зарезервированного слова inline. Оно даст команду компилятору, что код должен быть размещен во всех позициях, где он появляется. Вместо того, чтобы компилятор генерировал вызов функции, он фактически копирует весь код из функции в точку, где на эту функцию ссылаются. Это позволяет ускорить выполнение кода за счет меньшего потребления памяти. Но в конкретном случае происходит то, что будет ссылка на переменную m_Infos.Rates, эта переменная имеет тип MqlRates, то есть мы сможем получить доступ к значениям структуры MqlRates. В данном случае мы не передаем адрес ссылки на переменную. Но в некоторых случаях, чтобы сделать код быстрее, мы передаем адрес ссылки, и в этом случае можно изменить значение переменной внутри класса, что в принципе должно быть запрещено. Чтобы этого не произошло, используем зарезервированное слово const, которое гарантирует, что переменная никогда не будет изменена без процедуры самого класса. Хотя многие зарезервированные слова, присутствующие в C++, также присутствуют в MQL5 в документированной форме, некоторые из них еще не документированы, но они являются частью MQL5, потому что он очень близок к C++. В конце статьи я оставлю ссылки для тех, кто хочет узнать немного больше о C++ и использовать эти же знания в программировании на MQL5.

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

m_bChartShift = ChartGetInteger(Terminal.Get_ID(), CHART_SHIFT);
m_dChartShift = ChartGetDouble(Terminal.Get_ID(), CHART_SHIFT_SIZE);
ChartSetInteger(Terminal.Get_ID(), CHART_SHIFT, true);
ChartSetDouble(Terminal.Get_ID(), CHART_SHIFT_SIZE, 0.1);

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


Прикрепление объектов на экране

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


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

ВАЖНОЕ ЗАМЕЧАНИЕ: Если вы хотите изменить точку анализа, обратите внимание на таймфрейм графика. Например, если нужно перенести анализ с 9:00 на 9:02, надо использовать таймфрейм в 1 минуту или 2 минуты. А если использовать график, например, в 5 минут, сделать это уже нельзя.

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

void DispatchMessage(int iMsg, string sparam)
{
        switch (iMsg)
        {

// ... Внутренняя часть кода

                case CHARTEVENT_OBJECT_DELETE:
                        if ((sparam == m_Infos.szObjEvent) && (m_bUsing))
                        {
                                m_bUsing = false;
                                CreateObjEvent();
                                Resize();
                                m_bUsing = true;
                        }
                break;
        }                       
};

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

ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, 0, true);

Эта простая строка гарантирует, что MetaTrader 5 сообщит об удалении объекта. Более подробную информацию смотрите в CHART_EVENT_OBJECT_DELETE.


Сборка графика Volume At Price

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

inline virtual void Update(void)
{
        MqlTick Tick[];
        int i1, p1;

        if (m_bUsing == false) return;
        if ((i1 = CopyTicksRange(Terminal.GetSymbol(), Tick, COPY_TICKS_TRADE, m_Infos.memTimeTick)) > 0)
        {
                if (m_Infos.CountInfos == 0)
                {
                        macroSetInteger(OBJPROP_TIME, m_Infos.StartTime = macroRemoveSec(Tick[0].time));
                        m_Infos.FirstPrice = Tick[0].last;
                }                                               
                for (p1 = 0; (p1 < i1) && (Tick[p1].time_msc == m_Infos.memTimeTick); p1++);
                for (int c0 = p1; c0 < i1; c0++) SetMatrix(Tick[c0]);
                if (p1 == i1) return;
                m_Infos.memTimeTick = Tick[i1 - 1].time_msc;
                m_Infos.CurrentTime = macroRemoveSec(Tick[i1 - 1].time);
                Redraw();
        };      
};

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

inline void SetMatrix(MqlTick &tick)
{
        int pos;
                                
        if ((tick.last == 0) || ((tick.flags & (TICK_FLAG_BUY | TICK_FLAG_SELL)) == (TICK_FLAG_BUY | TICK_FLAG_SELL))) return;
        pos = (int) ((tick.last - m_Infos.FirstPrice) / Terminal.GetPointPerTick()) * 2;
        pos = (pos >= 0 ? pos : (pos * -1) - 1);
        if ((tick.flags & TICK_FLAG_BUY) == TICK_FLAG_BUY) m_InfoAllVaP[pos].nVolBuy += tick.volume; else
        if ((tick.flags & TICK_FLAG_SELL) == TICK_FLAG_SELL) m_InfoAllVaP[pos].nVolSell += tick.volume;
        m_InfoAllVaP[pos].nVolDif = (long)(m_InfoAllVaP[pos].nVolBuy - m_InfoAllVaP[pos].nVolSell);
        m_InfoAllVaP[pos].nVolTotal = m_InfoAllVaP[pos].nVolBuy + m_InfoAllVaP[pos].nVolSell;
        m_Infos.MaxVolume = (m_Infos.MaxVolume > m_InfoAllVaP[pos].nVolTotal ? m_Infos.MaxVolume : m_InfoAllVaP[pos].nVolTotal);
        m_Infos.CountInfos = (m_Infos.CountInfos == 0 ? 1 : (m_Infos.CountInfos > pos ? m_Infos.CountInfos : pos));
}

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


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

void Redraw(void)
{
        uint x, y, y1, p;
        double reason = (double) (m_Infos.MaxVolume > m_WidthMax ? (m_WidthMax / (m_Infos.MaxVolume * 1.0)) : 1.0);
        double desl = Terminal.GetPointPerTick() / 2.0;
        Erase();
        p = m_WidthMax - 8;
        for (int c0 = 0; c0 <= m_Infos.CountInfos; c0++)
        {
                if (m_InfoAllVaP[c0].nVolTotal == 0) continue;
                ChartTimePriceToXY(Terminal.Get_ID(), 0, 0, m_Infos.FirstPrice + (Terminal.GetPointPerTick() * (((c0 & 1) == 1 ? -(c0 + 1) : c0) / 2)) + desl, x, y);
                y1 = y + Terminal.GetHeightBar();
                FillRectangle(p + 2, y, p + 8, y1, macroColorRGBA(m_InfoAllVaP[c0].nVolDif > 0 ? m_Infos.ColorBuy : m_Infos.ColorSell, m_Infos.Transparency));
                FillRectangle((int)(p - (m_InfoAllVaP[c0].nVolTotal * reason)), y, p, y1, macroColorRGBA(m_Infos.ColorBars, m_Infos.Transparency));
        }
        C_Canvas::Update();
};

Эта функция строит график объема, а выделенная часть заботится об инвертировании расчета, сделанного во время захвата объема, и чтобы получить отображение в нужной точке, цена немного сдвигается, так что бары позиционируются правильно. Остальная часть функции — это просто процедуры рисования. Здесь стоит дать разъяснения. Обратите внимание, что есть два вызова FillRectangle. Для чего? Первый вызов указывает, какой объем был больше — продавцов или покупателей, а второй вызов фактически строит график объема. Но почему бы не построить их вместе, разделив полосу объема между покупателями и продавцами? Причина в том, что при увеличении объема в одном ценовом диапазоне, он начинает мешать анализу в других, меньших ценовых диапазонах. Становится трудно определить, какой объем был самым большим, была ли это продажа или покупка, а при размещении таким образом эта проблема исчезает, делая чтение более простым и понятным. В итоге график будет выглядеть так, как показано на рисунке ниже:


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


Заключение

Здесь я представляю вам очень простой Volume as Price, но это чрезвычайно эффективный инструмент. Если вы начинаете изучать разработку и хотите сосредоточиться на объектно-ориентированном программировании (ООП), нужно спокойно изучить этот код, потому что в нем есть несколько концепций, которые очень хороши, потому что весь код основан на 100% объектно-ориентированном подходе.

В приложении находится советник до текущей стадии разработки.


Ссылки



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

Прикрепленные файлы |
EA_1.06.zip (3280.48 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (1)
Alexey Volchanskiy
Alexey Volchanskiy | 18 мая 2022 в 16:57
Ну увидел в статье, как автор определяет объемы. Зато полно мути, типа как отследить, если юзер удалил точку))). Будет время, поковыряюсь в коде, а статьи этого португальца читать - время зря тратить.
DoEasy. Элементы управления (Часть 5): Базовый WinForms-объект, элемент управления "Панель", параметр AutoSize DoEasy. Элементы управления (Часть 5): Базовый WinForms-объект, элемент управления "Панель", параметр AutoSize
В статье создадим базовый объект всех WinForms-объектов библиотеки и приступим к реализации свойства AutoSize WinForms-объекта "Панель" — автоизменение размера под его внутреннее содержимое.
Нейросети — это просто (Часть 14): Кластеризация данных Нейросети — это просто (Часть 14): Кластеризация данных
Должен признаться, что с момента публикации последней статьи прошло уже больше года. За столь длительное время можно многое переосмыслить, выработать новые подходы. И в новой статье я хотел бы немного отойти от используемого ранее метода обучения с учителем, и предложить немного окунуться в алгоритмы обучения без учителя. И, в частности, рассмотреть один из алгоритмов кластеризации — k-средних.
Нейросети — это просто (Часть 15): Кластеризации данных средствами MQL5 Нейросети — это просто (Часть 15): Кластеризации данных средствами MQL5
Продолжаем рассмотрение метода кластеризации. В данной статье мы создадим новый класс CKmeans для реализации одного из наиболее распространённых методов кластеризации k-средних. По результатам тестирования модель смогла выделить около 500 паттернов.
Разработка торговой системы на основе индикатора RSI Разработка торговой системы на основе индикатора RSI
В этой статье мы поговорим об еще одном популярном и часто используемом индикаторе — RSI. Узнаем, как разработать торговую систему на основе показателей от этого индикатора.