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

Разработка торгового советника с нуля (Часть 21): Новая система ордеров (IV)

MetaTrader 5Торговые системы | 25 июля 2022, 15:05
1 398 1
Daniel Jose
Daniel Jose

Введение

В предыдущей статье Разработка торгового советника с нуля (Часть 20) я показал основные изменения, которые нужно внести, чтобы получить визуальную систему ордеров. Но следующие шаги уже требовали большего количества пояснений, поэтому я решил разделить статью на несколько частей. Здесь мы закончим вносить основные изменения, а их будет немало, но они все необходимы, и вся работа будет достаточно интересной. Однако, полностью работу я не завершу, поскольку останется сделать еще кое-что, чтобы действительно закончить систему. Но даже здесь она уже будет почти со всеми функциональными возможностями.

Давайте перейдем непосредственно к приложению.


1.0. Применение

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

1.0.1. Класс C_Object_BtnBitMap

Этот класс отвечает за поддержку растровых кнопок на диаграмме, как можно увидеть ниже.

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#include "C_Object_Base.mqh"
//+------------------------------------------------------------------+
#define def_BtnClose    "Images\\NanoEA-SIMD\\Btn_Close.bmp"
//+------------------------------------------------------------------+
#resource "\\" + def_BtnClose
//+------------------------------------------------------------------+
class C_Object_BtnBitMap : public C_Object_Base
{
        public  :
//+------------------------------------------------------------------+
		void Create(string szObjectName, string szResource1, string szResource2 = NULL)
                        {
                                C_Object_Base::Create(szObjectName, OBJ_BITMAP_LABEL);
                                ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_BMPFILE, 0, "::" + szResource1);
                                ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_BMPFILE, 1, "::" + (szResource2 == NULL ? szResource1 : szResource2));
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_STATE, false);
                        };
//+------------------------------------------------------------------+
                bool GetStateButton(string szObjectName) const
                        {
                                return (bool) ObjectGetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_STATE);
                        }
//+------------------------------------------------------------------+
};

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

Чтобы добавить кнопку CLOSE, мы сделаем следующее:

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#include "C_Object_TradeLine.mqh"
#include "C_Object_BtnBitMap.mqh"
//+------------------------------------------------------------------+
class C_ObjectsTrade
{

// ... Код класса ...

}

Следующий шаг...

enum eEventType {EV_GROUND = 65, EV_LINE, EV_CLOSE};

Следующий шаг...

C_Object_BackGround     m_BackGround;
C_Object_TradeLine      m_TradeLine;
C_Object_BtnBitMap      m_BtnClose;

Следующий шаг...

inline void CreateIndicatorTrade(ulong ticket, eIndicatorTrade it)
                        {
                                color cor1, cor2;
                                string sz0;
                                

// ... Внутренный код функции ...

                                switch (it)
                                {
                                        case IT_TAKE:
                                        case IT_STOP:
                                                m_BackGround.Size(sz0, 92, 22);
                                                break;
                                        case IT_PENDING:
                                                m_BackGround.Size(sz0, 110, 22);
                                                break;
                                }
                                m_BtnClose.Create(MountName(ticket, it, EV_CLOSE), def_BtnClose);
                        }

Следующий шаг...

#define macroDelete(A)  {                                                               \
                ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_GROUND));       \
                ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_LINE));         \
                ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_CLOSE));        \
                        }
                                        
inline void RemoveIndicatorTrade(ulong ticket, eIndicatorTrade it = IT_NULL)
                        {
                                ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false);
                                if ((it != NULL) && (it != IT_PENDING) && (it != IT_RESULT)) macroDelete(it)
                                else
                                {
                                        macroDelete(IT_PENDING);
                                        macroDelete(IT_RESULT);
                                        macroDelete(IT_TAKE);
                                        macroDelete(IT_STOP);
                                }
                                ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true);
                        }
#undef macroDelete

И, наконец, последний шаг...

#define macroSetAxleY(A)        {                                               \
                m_BackGround.PositionAxleY(MountName(ticket, A, EV_GROUND), y); \
                m_TradeLine.PositionAxleY(MountName(ticket, A, EV_LINE), y);    \
                m_BtnClose.PositionAxleY(MountName(ticket, A, EV_CLOSE), y);    \
                                }
                                                                        
#define macroSetAxleX(A, B)     {                                               \
                m_BackGround.PositionAxleX(MountName(ticket, A, EV_GROUND), B); \
                m_TradeLine.PositionAxleX(MountName(ticket, A, EV_LINE), B);    \
                m_BtnClose.PositionAxleX(MountName(ticket, A, EV_CLOSE), B + 3);\
                                }
inline void PositionAxlePrice(double price, ulong ticket, eIndicatorTrade it, int FinanceTake, int FinanceStop, int Leverange, bool isBuy)
                        {

// ... Внутренный код ...
                                
                        }
#undef macroSetAxleX
#undef macroSetAxleY

При запуске системы мы получим следующий результат:


Но эта кнопка по-прежнему не работает, несмотря на то что MetaTrader 5 генерирует событие для обработки его советником, мы еще не реализовали этот момент. Рассмотрим это чуть позже в рамках данной статьи.


1.0.2. Класс C_Object_Edit

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

void Create(string szObjectName, color cor, int 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_CENTER);
        ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_COLOR, clrBlack);
        ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BORDER_COLOR, clrBlack);
        ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_READONLY, true);
        SetTextValue(szObjectName, InfoValue, cor);
}

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

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

void SetTextValue(string szObjectName, int InfoValue, color cor = clrNONE)
{
        color clr;
        clr = (cor != clrNONE ? cor  : (InfoValue < 0 ? def_ColorNegative : def_ColoPositive));
        ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_TEXT, IntegerToString(InfoValue < 0 ? -(InfoValue) : InfoValue));
        ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BGCOLOR, clr);
}

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

Далее у нас идет последняя функция этого класса, показанная ниже.

long GetTextValue(string szObjectName) const
{
        return (StringToInteger(ObjectGetString(Terminal.Get_ID(), szObjectName, OBJPROP_TEXT)) * 
                                (ObjectGetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BGCOLOR) == def_ColorNegative ? -1 : 1));
};

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

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


1.0.3. Класс C_Object_Label

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

Его код супер простой, вы можете посмотреть на него ниже.

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#include "C_Object_Edit.mqh"
//+------------------------------------------------------------------+
class C_Object_Label : public C_Object_Edit
{
        public  :
//+------------------------------------------------------------------+
                void Create(string szObjectName, string Font = "Lucida Console", string szTxt = "", int FontSize = 10, color cor = clrBlack)
                        {
                                C_Object_Base::Create(szObjectName, OBJ_LABEL);
                                ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_FONT, Font);
                                ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_TEXT, szTxt);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_FONTSIZE, FontSize);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_COLOR, cor);
                        };
//+------------------------------------------------------------------+
};

И больше ничего в этом классе нам не нужно, так как вся остальная работа уже реализована объектами-классами ниже него.

Как видите, ООП — очень мощный инструмент: чем больше мы организуем код по классам, тем меньше нам нужно программировать классы, похожие друг на друга.

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

    

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

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

virtual 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)));
                        };

После этого мы можем перейти к следующему шагу — изменение класса C_ObjectsTrade.


1.0.4 Класс C_ObjectsTrade

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

enum eEventType {EV_GROUND = 65, EV_LINE, EV_CLOSE, EV_EDIT, EV_VOLUME, EV_MOVE};

Теперь давайте добавим объекты. В этом случае новые объекты также выделены во фрагменте ниже:

C_Object_BackGround     m_BackGround;
C_Object_TradeLine      m_TradeLine;
C_Object_BtnBitMap      m_BtnClose;
C_Object_Edit           m_EditInfo,
                        m_InfoVol;
C_Object_Label          m_BtnMove;

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

inline void CreateIndicatorTrade(ulong ticket, eIndicatorTrade it)
{
        color cor1, cor2, cor3;
        string sz0;
        int infoValue;
                                
        switch (it)
        {
                case IT_TAKE    :
                        infoValue = m_BaseFinance.FinanceTake;
                        cor1 = clrForestGreen;
                        cor2 = clrDarkGreen;
                        cor3 = clrNONE;
                        break;
                case IT_STOP    :
                        infoValue = - m_BaseFinance.FinanceStop;
                        cor1 = clrFireBrick;
                        cor2 = clrMaroon;
                        cor3 = clrNONE;
                        break;
                case IT_PENDING:
                        infoValue = m_BaseFinance.Leverange;
                        cor1 = clrCornflowerBlue;
                        cor2 = clrDarkGoldenrod;
                        cor3 = clrLightBlue;
                        break;
                case IT_RESULT  :
                default:
                        infoValue = m_BaseFinance.Leverange;
                        cor1 = clrDarkBlue;
                        cor2 = clrDarkBlue;
                        cor3 = clrSilver;
                        break;
                }                               
                m_TradeLine.Create(MountName(ticket, it, EV_LINE), cor2);
                if (ticket == def_IndicatorTicket0) m_TradeLine.SpotLight(MountName(ticket, IT_PENDING, EV_LINE));
                m_BackGround.Create(sz0 = MountName(ticket, it, EV_GROUND), cor1);
                switch (it)
                {
                        case IT_TAKE:
                        case IT_STOP:
                        case IT_PENDING:
                                m_BackGround.Size(sz0, 92, 22);
                                break;
                        case IT_RESULT:
                                m_BackGround.Size(sz0, 84, 34);
                                break;
                }
                m_BtnClose.Create(MountName(ticket, it, EV_CLOSE), def_BtnClose);
                m_EditInfo.Create(sz0 = MountName(ticket, it, EV_EDIT), cor3, infoValue);
                m_EditInfo.Size(sz0, 60, 14);
                if (it != IT_RESULT) m_BtnMove.Create(MountName(ticket, it, EV_MOVE), "Wingdings", "u", 17, cor2);
                else
                {
                        m_InfoVol.Create(sz0 = MountName(ticket, it, EV_VOLUME), clrNONE, infoValue);
                        m_InfoVol.Size(sz0, 60, 14);
                }
}

Все выделенные пункты — это новые дополнения, внесенные в код с момента последней версии, рассмотренной в предыдущей статье. Теперь мы можем написать код следующей функции.

#define macroDelete(A)  {                                                                       \
                ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_GROUND));               \
                ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_LINE));                 \
                ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_CLOSE));                \
                ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_EDIT));                 \
                if (A != IT_RESULT)                                                             \
                        ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_MOVE));         \
                else                                                                            \
                        ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_VOLUME));       \
                        }
                                        
inline void RemoveIndicatorTrade(ulong ticket, eIndicatorTrade it = IT_NULL)
                        {
                                ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false);
                                if ((it != NULL) && (it != IT_PENDING) && (it != IT_RESULT)) macroDelete(it)
                                else
                                {
                                        macroDelete(IT_PENDING);
                                        macroDelete(IT_RESULT);
                                        macroDelete(IT_TAKE);
                                        macroDelete(IT_STOP);
                                }
                                ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true);
                        }
#undef macroDelete

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

#define macroSetAxleY(A)        {                                                                       \
                m_BackGround.PositionAxleY(MountName(ticket, A, EV_GROUND), y);                         \
                m_TradeLine.PositionAxleY(MountName(ticket, A, EV_LINE), y);                            \
                m_BtnClose.PositionAxleY(MountName(ticket, A, EV_CLOSE), y);                            \
                m_EditInfo.PositionAxleY(MountName(ticket, A, EV_EDIT), y, (A == IT_RESULT ? -1 : 0));  \
                if (A != IT_RESULT)                                                                     \
                        m_BtnMove.PositionAxleY(MountName(ticket, A, EV_MOVE), y);                      \
                else                                                                                    \
                        m_InfoVol.PositionAxleY(MountName(ticket, A, EV_VOLUME), y, 1);                 \
                                }
                                                                        
#define macroSetAxleX(A, B)     {                                                               \
                m_BackGround.PositionAxleX(MountName(ticket, A, EV_GROUND), B);                 \
                m_TradeLine.PositionAxleX(MountName(ticket, A, EV_LINE), B);                    \
                m_BtnClose.PositionAxleX(MountName(ticket, A, EV_CLOSE), B + 3);                \
                m_EditInfo.PositionAxleX(MountName(ticket, A, EV_EDIT), B + 21);                \
                if (A != IT_RESULT)                                                             \
                        m_BtnMove.PositionAxleX(MountName(ticket, A, EV_MOVE), B + 80);         \
                else                                                                            \
                        m_InfoVol.PositionAxleX(MountName(ticket, A, EV_VOLUME), B + 21);       \
                                }
                                                                                
inline void PositionAxlePrice(double price, ulong ticket, eIndicatorTrade it, int FinanceTake, int FinanceStop, int Leverange, bool isBuy)
                        {
                                double ad;
                                int x, y;
                                
                                ChartTimePriceToXY(Terminal.Get_ID(), 0, 0, price, x, y);
                                macroSetAxleY(it);
                                macroSetAxleX(it, m_PositionMinimalAlxeX);
                                if (Leverange == 0) return;
                                if (it == IT_PENDING)
                                {
                                        ad = Terminal.GetAdjustToTrade() / (Leverange * Terminal.GetVolumeMinimal());
                                        ChartTimePriceToXY(Terminal.Get_ID(), 0, 0, price + Terminal.AdjustPrice(FinanceTake * (isBuy ? ad : (-ad))), x, y);
                                        macroSetAxleY(IT_TAKE);
                                        macroSetAxleX(IT_TAKE, m_PositionMinimalAlxeX + 110);
                                        ChartTimePriceToXY(Terminal.Get_ID(), 0, 0, price + Terminal.AdjustPrice(FinanceStop * (isBuy ? (-ad) : ad)), x, y);
                                        macroSetAxleY(IT_STOP);
                                        macroSetAxleX(IT_STOP, m_PositionMinimalAlxeX + 220);
                                }
                        }
#undef macroSetAxleX
#undef macroSetAxleY

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


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


2.0. Как бороться с проблемами

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


2.0.1. Корректируем вещи по мере обновления графика

Это первая проблема, которая у нас появилась, и она вызвана тем, что позиции объектов не обновляются по мере обновления графика. Чтобы понять это, нужно посмотреть на анимацию ниже:

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

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

void DispatchMessage(int id, long lparam, double dparam, string sparam)
{

// ... Код ....
                                
        switch (id)
        {
                case CHARTEVENT_CHART_CHANGE:
                        SetPositionMinimalAxleX();
                        UpdatePosition();
                        break;

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

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


Кажется, всё правильно, не так ли? Но там есть ошибка. Трудно увидеть недостаток, пока мы не протестируем, но действительно в этой системе есть недостаток, который даже после его исправления никуда не уходит.


2.0.2. Советник, не выбирай элементы автоматически, пожалуйста

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

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

inline void CreateIndicatorTrade(ulong ticket, eIndicatorTrade it)
{
        color cor1, cor2, cor3;
        string sz0;
        double infoValue;

// ... Внутренный код ...

        Select(NULL);
}

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

Но то, что мы упомянули было просто поводом для учебы, давайте дальше о действительно важном...


3.0. Обработка событий

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

3.0.1. Просмотр результата позиции

Тот, кто видит код в первый раз, может растеряться и не знать, где смотреть и искать информацию, создавая таким образом другие операции, чтобы сделать что-то такое, что уже делает исходный код. Именно это вызывает обычно много проблем, генерируя дополнительный код, который может создавать ошибки, которых не было в исходном коде, что также выходит за рамки философии повторного использования элементов, программируя только при необходимости. Итак, зная то, как работает MT5 и советник, мы теперь должны выяснить, с какого момента получается результат, который представлен в Chart Trade, поскольку, если результат позиций предъявлен, то именно с этого места мы хотим и должны использовать. Делая это, обратите внимание на фрагмент ниже.

void OnTick()
{
        Chart.DispatchMessage(CHARTEVENT_CHART_CHANGE, 0, OrderView.CheckPosition(), C_Chart_IDE::szMsgIDE[C_Chart_IDE::eRESULT]);
        TimesAndTrade.Update();
}

Далее, мы переходим к выделенной точке. Исходный код можно посмотреть ниже.

inline double CheckPosition(void)
                        {
                                double Res = 0, sl, profit, bid, ask;
                                ulong ticket;
                                
                                bid = SymbolInfoDouble(Terminal.GetSymbol(), SYMBOL_BID);
                                ask = SymbolInfoDouble(Terminal.GetSymbol(), SYMBOL_ASK);
                                for (int i0 = PositionsTotal() - 1; i0 >= 0; i0--) if (PositionGetSymbol(i0) == Terminal.GetSymbol())
                                {
                                        ticket = PositionGetInteger(POSITION_TICKET);
                                        sl = PositionGetDouble(POSITION_SL);
                                        if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
                                        {
                                                if (ask < sl) ClosePosition(ticket);
                                        }else
                                        {
                                                if ((bid > sl) && (sl > 0)) ClosePosition(ticket);
                                        }
                                        Res += profit;
                                }
                                return Res;
                        };

Может быть, вы подумаете: а как же я возьму оттуда данные и применю их к индикатору, если у меня нет возможности узнать, на какой из объектов мне ссылаться? При создании объекты, кажется, были созданы в произвольной форме, без каких-либо критериев... Что сказать? Мне жаль сообщать вам, но это не верно, если вы думаете или представляли себе вещи таким образом. Вам лучше начать изучать немного больше то, как на самом деле работает платформа MetaTrader 5. Это правильно, что я не создавал никакого списка, массива или структуры для ссылки на созданные объекты, но я это сделал специально, потому что я знаю, что это работает. И я покажу вам, что это действительно работает: вам не нужна какая-либо структура для хранения объектов, которые будут в графике, чтобы иметь возможность ссылаться на конкретный объект, то, что В САМОМ ДЕЛЕ нужно правильно смоделировать название объекта, именно это, моделировать название объекта.

Вопрос: как были смоделированы названия объектов?

Следующим образом:

1 - Последовательность заголовка Эта последовательность будет отличать объект, используемый в системе ордеров, от всех остальных.
2 - Ограничивающий символ
Он сообщает о том, что скоро появится какая-то другая информация
3 - Индикатор типа Он отмечает разницу между каждым типом индикатора, индикатор Take отличается от индикатора Stop.
4 - Ограничивающий символ Выполняет ту же задачу, что и пункт 2.
5 - Тикет ордера или позиции Запоминает значение тикета ордера, это связывает индикаторы OCO-ордера и отличает один ордер от другого.
6 - Ограничивающий символ Выполняет ту же задачу, что и пункт 2.
7 - Индикатор событий Это различает объекты внутри одного индикатора

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

inline string MountName(ulong ticket, eIndicatorTrade it, eEventType ev)
                        {
                                return StringFormat("%s%c%c%c%d%c%c", def_NameObjectsTrade, def_SeparatorInfo, (char)it, def_SeparatorInfo, ticket, def_SeparatorInfo, (char)ev);
                        }

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

Теперь этим и займемся. Для этого мы перейдем к классу C_ObjectsTrade и добавим следующий код.

inline void SetResult(ulong ticket, double dVolume, double dResult)
                        {
                                m_InfoVol.SetTextValue(MountName(ticket, IT_RESULT, EV_VOLUME), (dVolume / Terminal.GetVolumeMinimal()), def_ColorVolumeResult);
                                m_EditInfo.SetTextValue(MountName(ticket, IT_RESULT, EV_EDIT), dResult);
                        }

Теперь переходим к классу C_Router и добавим выделенный фрагмент.

inline double CheckPosition(void)
                        {
                                double Res = 0, sl, profit, bid, ask;
                                ulong ticket;
                                
                                bid = SymbolInfoDouble(Terminal.GetSymbol(), SYMBOL_BID);
                                ask = SymbolInfoDouble(Terminal.GetSymbol(), SYMBOL_ASK);
                                for (int i0 = PositionsTotal() - 1; i0 >= 0; i0--) if (PositionGetSymbol(i0) == Terminal.GetSymbol())
                                {
                                        ticket = PositionGetInteger(POSITION_TICKET);
                                        SetResult(ticket, PositionGetDouble(POSITION_VOLUME), profit = PositionGetDouble(POSITION_PROFIT));
                                        sl = PositionGetDouble(POSITION_SL);
                                        if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
                                        {
                                                if (ask < sl) ClosePosition(ticket);
                                        }else
                                        {
                                                if ((bid > sl) && (sl > 0)) ClosePosition(ticket);
                                        }
                                        Res += profit;
                                }
                                return Res;
                        };

Это решает одну из проблем. Но у нас есть и другие нерешенные проблемы.


3.0.2. Указываем объем отложенного ордера

Теперь будем решать проблему объема, указанного в отложенном ордере. Для этого нам нужно создать новую процедуру в классе C_ObjectsTrade, как показано ниже.

inline void SetVolumePendent(ulong ticket, double dVolume)
                        {
                                m_EditInfo.SetTextValue(MountName(ticket, IT_PENDING, EV_EDIT), dVolume / Terminal.GetVolumeMinimal(), def_ColorVolumeEdit);
                        }

Как только это будет сделано, мы используем функцию UpdatePosition, присутствующую в классе C_Router, и обновление произойдет без каких-либо дальнейших проблем.

void UpdatePosition(int iAdjust = -1)
{

// ... Внутренный код ....

        for (int i0 = o; i0 >= 0; i0--) if ((ul = OrderGetTicket(i0)) > 0) if (OrderGetString(ORDER_SYMBOL) == Terminal.GetSymbol())
        {
                price = OrderGetDouble(ORDER_PRICE_OPEN);
                take = OrderGetDouble(ORDER_TP);
                stop = OrderGetDouble(ORDER_SL);
                bTest = CheckLimits(price);
                vol = OrderGetDouble(ORDER_VOLUME_CURRENT);

// ... Внутренный код ...

                CreateIndicatorTrade(ul, price, IT_PENDING);
                SetVolumePendent(ul, vol);
                CreateIndicatorTrade(ul, take, IT_TAKE);
                CreateIndicatorTrade(ul, stop, IT_STOP);
        }
};

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


3.0.3. Событие нажатия кнопки Close индикатора

Единственный безопасный способ удалить ордер или один из его стоп-уровней — это кнопка Close, расположенная в углу каждого из индикаторов. Но здесь у нас некорректно реализовано событие. Давайте это исправим.

В классе C_OrderView должно быть реализовано событие клика, по сути, мы собираемся заменить старую систему выделенным кодом.

void DispatchMessage(int id, long lparam, double dparam, string sparam)
{
        ulong   	ticket;
        double  	price, pp, pt, ps;
        eIndicatorTrade it;
        eEventType 	ev;
                                
        switch (id)
        {

// ... Внутренный код ...
                case CHARTEVENT_OBJECT_CLICK:
                        if (GetInfosOrder(sparam, ticket, price, it, ev))
                        {
                                switch (ev)
                                {
                                        case EV_CLOSE:
                                                if (OrderSelect(ticket)) switch (it)
                                                {
                                                        case IT_PENDING:
                                                                RemoveOrderPendent(ticket);
                                                                break;
                                                        case IT_TAKE:
                                                                ModifyOrderPendent(ticket, OrderGetDouble(ORDER_PRICE_OPEN), 0, OrderGetDouble(ORDER_SL));
                                                                break;
                                                        case IT_STOP:
                                                                ModifyOrderPendent(ticket, OrderGetDouble(ORDER_PRICE_OPEN), OrderGetDouble(ORDER_TP), 0);
                                                                break;
                                                }
                                                if (PositionSelectByTicket(ticket)) switch (it)
                                                {
                                                        case IT_RESULT:
                                                                ClosePosition(ticket);
                                                                break;
                                                        case IT_TAKE:
                                                                ModifyPosition(ticket, 0, PositionGetDouble(POSITION_SL));
                                                                break;
                                                        case IT_STOP:
                                                                ModifyPosition(ticket, PositionGetDouble(POSITION_TP), 0);
                                                                break;
                                                }
                                                break;

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

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

void DispatchMessage(int id, long lparam, double dparam, string sparam)
{
        ulong           ticket;
        double          price, pp, pt, ps;
        eIndicatorTrade it;
        eEventType      ev;
                                
        switch (id)
        {
                case CHART_EVENT_OBJECT_DELETE:
                case CHARTEVENT_CHART_CHANGE:
                        SetPositionMinimalAxleX();
                        UpdatePosition();
                        break;

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

Таким образом, если трейдер удалит что-то не то, советник быстро восстановит удаленный индикатор или элемент.

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




Заключение

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

В приложении можно увидеть систему на текущем этапе разработки.


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

Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (1)
Elkino
Elkino | 30 июл. 2022 в 21:35

При компиляции EA выдаёт ошибки. Надо закомментировать строку № 52

//C_Terminal Terminal;

Нейросети — это просто (Часть 22): Обучение без учителя рекуррентных моделей Нейросети — это просто (Часть 22): Обучение без учителя рекуррентных моделей
Мы продолжаем рассмотрение алгоритмов обучения без учителя. И сейчас я предлагаю обсудить особенности использования автоэнкодеров для обучения рекуррентных моделей.
Разработка торговой системы на основе индикатора Накопления/Распределения - Accumulation/Distribution Разработка торговой системы на основе индикатора Накопления/Распределения - Accumulation/Distribution
Представляю вашему вниманию новую статью из серии, в которой мы учимся создавать торговые системы на основе популярных технических индикаторов. В этой статье мы будем изучать индикатор Накопления/Распределения (Accumulation/Distribution, A/D). Также мы разработаем торговую систему на языке MQL5 для работы в платформе MetaTrader 5, используя несколько простых стратегий.
Индикатор CCI. Три шага трансформации Индикатор CCI. Три шага трансформации
В этой статье мы попробуем внести дополнительные изменения в CCI. Эти изменения коснутся самой логики работы этого индикатора. Вплоть до того, что мы сможем увидеть этот индикатор в главном окне графика.
Разработка торговой системы на основе индикатора OBV Разработка торговой системы на основе индикатора OBV
Это новая статья, продолжающая нашу серию для начинающих MQL5-программистов, в которой мы учимся строить торговые системы с использованием самых популярных индикаторов. На этот раз мы будем изучать индикатор балансового объема On Balance Volume (OBV) — узнаем, как его использовать и как создать торговую систему на его основе.