Как построить советник, работающий автоматически (Часть 05): Ручные триггеры (II)

Daniel Jose | 7 февраля, 2023

Введение

В предыдущей статье Как построить советник, работающий автоматически (Часть 04): Ручные триггеры (I) я продемонстрировал, как при помощи небольшого количества программирования вы можете отправлять рыночные ордера и оставлять ордера в книге заявок, используя для этого комбинацию клавиатуры и мыши.

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

Поскольку многие могут использовать представленные в этом сообществе статьи, чтобы начать учиться программировать, я рассматриваю это как возможность поделиться некоторым своим опытом, приобретенным за годы программирования на C/C++, и показать, как делать некоторые вещи на MQL5, который очень похож на C/C++. Я хочу лишить мистики то представление многих людей о том, что на самом деле означает программирование.

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

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

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


Создание класса C_Terminal

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

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

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

class C_Terminal
{
        private :
        public  :
};

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

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

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

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

class C_Terminal
{
        protected:
//+------------------------------------------------------------------+
                struct stTerminal
                {
                        ENUM_SYMBOL_CHART_MODE ChartMode;
                        int     nDigits;
                        double  VolMinimal,
                                VolStep,
                                PointPerTick,
                                ValuePerPoint,
                                AdjustToTrade;
                };
//+------------------------------------------------------------------+
        private :
        public  :
};

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

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

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

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

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

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

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

Определение в родительском классе Тип наследования Доступ из дочернего класса Доступ путем вызова дочернего класса 
private public: Доступ запрещен Нельзя получить доступ к данным или процедурам базового класса
public: public: Доступ разрешен Разрешен доступ к данным или процедурам базового класса
protected public: Доступ разрешен Нельзя получить доступ к данным или процедурам базового класса
private private Доступ запрещен Нельзя получить доступ к данным или процедурам базового класса
public: private Доступ разрешен Нельзя получить доступ к данным или процедурам базового класса
protected private Доступ разрешен Нельзя получить доступ к данным или процедурам базового класса
private protected Доступ запрещен Нельзя получить доступ к данным или процедурам базового класса
public: protected Доступ разрешен  Нельзя получить доступ к данным или процедурам базового класса
protected protected Доступ разрешен Нельзя получить доступ к данным или процедурам базового класса

Таблица 1) Система наследования на основе определения информации

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

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

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

Итак, продолжим. Если вы обратите внимание, то заметите, что структура, которую можно увидеть в приведенном выше коде такая же, как и в классе C_Orders. Помните об этом, потому что класс C_Order потеряет определение этих данных внутри себя и начнет наследовать эти данные от класса C_Terminal. Но пока давайте останемся внутри класса C_Terminal.

Следующее, что нужно добавить в класс C_Terminal - это функции, общие как для класса C_Mouse, так и для класса C_Orders. Эти функции будут добавлены в защищенную часть класса C_Terminal, таким образом, когда классы C_Mouse и C_Orders наследуются от C_Terminal, то эти функции и процедуры будут следовать таблице 01. Коды, которые вы добавите, можно увидеть ниже:

//+------------------------------------------------------------------+
inline double AdjustPrice(const double value)
                        {
                                return MathRound(value / m_TerminalInfo.PointPerTick) * m_TerminalInfo.PointPerTick;
                        }
//+------------------------------------------------------------------+
inline double FinanceToPoints(const double Finance, const uint Leverage)
                        {
                                double volume = m_TerminalInfo.VolMinimal + (m_TerminalInfo.VolStep * (Leverage - 1));
                                
                                return AdjustPrice(MathAbs(((Finance / volume) / m_TerminalInfo.AdjustToTrade)));
                        };
//+------------------------------------------------------------------+

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

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

        public  :
//+------------------------------------------------------------------+
                C_Terminal()
                        {
                                m_TerminalInfo.nDigits          = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
                                m_TerminalInfo.VolMinimal       = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
                                m_TerminalInfo.VolStep          = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
                                m_TerminalInfo.PointPerTick     = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
                                m_TerminalInfo.ValuePerPoint    = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
                                m_TerminalInfo.AdjustToTrade    = m_TerminalInfo.ValuePerPoint / m_TerminalInfo.PointPerTick;
                                m_TerminalInfo.ChartMode        = (ENUM_SYMBOL_CHART_MODE) SymbolInfoInteger(_Symbol, SYMBOL_CHART_MODE);
                        }
//+------------------------------------------------------------------+

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

Причина в инкапсуляции. Вы не должны позволять коду вне класса получать доступ и тем самым изменять содержимое внутренних переменных класса. Это серьезная ошибка программирования, хотя компилятор не будет жаловаться, вы НИКОГДА не должны допускать этого. Абсолютно все глобальные переменные класса всегда должны быть объявлены внутри приватной части. Таким образом, объявление переменной выглядит так, как показано ниже.

//+------------------------------------------------------------------+
        private :
                stTerminal m_TerminalInfo;
        public  :
//+------------------------------------------------------------------+

Обратите внимание, что глобальная переменная класса определена между частями private и public. Таким образом, она будет недоступна для любого класса, наследуемого от C_Terminal, иными словами, мы гарантируем инкапсуляцию информации и в то же время добавляем наследование в наш код. Это приведет к экспоненциальному увеличению уровня его надежности по мере расширения его полезности.

Но тогда вы можете подумать: как мы будем получать доступ к нужным нам данным в классах выше? Необходимо предоставить некоторый уровень доступа к переменным родительского класса, которым в данном случае является класс C_Terminal! Да, нам это нужно. Но мы не должны реализовать это, делая переменные общедоступными или даже защищенными. Это ошибка программирования. Вы должны добавить какие-то средства, чтобы производные классы могли получить доступ к значениям родительского класса. Но здесь кроется опасность и это важно, ВЫ НИКАКИМ ОБРАЗОМ НЕ ДОЛЖНЫ ДОПУСТИТЬ, ЧТОБЫ ПРОИЗВОДНЫЕ КЛАССЫ МОГЛИ МОДИФИЦИРОВАТЬ ПЕРЕМЕННЫЕ РОДИТЕЛЬСКОГО КЛАССА

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

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

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

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

inline const stTerminal GetTerminalInfos(void) const
                        {
                                return m_TerminalInfo;
                        }

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

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

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

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

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

Пока мы еще не закончили наш базовый класс C_Terminal, мы можем устранить повторяющиеся части в коде, сделав класс C_Mouse того же типа кода, что и класс C_Orders. Но так как изменение в классе C_Mouse намного проще, давайте посмотрим, как он будет выглядеть теперь, когда он наследует класс C_Terminal. Это можно увидеть в следующем коде:

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#include "C_Terminal.mqh"
//+------------------------------------------------------------------+
#define def_MouseName "MOUSE_H"
//+------------------------------------------------------------------+
#define def_BtnLeftClick(A)     ((A & 0x01) == 0x01)
#define def_SHIFT_Press(A)      ((A & 0x04) == 0x04)
#define def_CTRL_Press(A)       ((A & 0x08) == 0x08)
//+------------------------------------------------------------------+
class C_Mouse : private C_Terminal
{
// Внутенний код класса ....
};

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

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

Однако все эти изменения в классе C_Mouse пока временные. Мы внесем еще несколько, чтобы добавить любые линии разграничения, которые нам необходимы при ручном использовании советника. Но пока рано беспокоиться об этом. Давайте посмотрим, как внести гораздо более глубокие изменения в класс C_Orders. Этот класс, однако, заслуживает отдельного рассказа. Теперь перейдем к следующей теме.


Изменение класса C_Orders после наследования класса C_Terminal

Мы инициируем изменения почти так же, как и в классе C_Mouse. Но тут начинаются различия, которые можно увидеть в следующем коде.

#include "C_Terminal.mqh"
//+------------------------------------------------------------------+
class C_Orders : private C_Terminal
{
        private :
//+------------------------------------------------------------------+
                MqlTradeRequest m_TradeRequest;
                ulong           m_MagicNumber;
                struct st00
                {
                        int     nDigits;
                        double  VolMinimal,
                                VolStep,
                                PointPerTick,
                                ValuePerPoint,
                                AdjustToTrade;
                        bool    PlotLast;
                        ulong   MagicNumber;
                }m_Infos;
//+------------------------------------------------------------------+

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

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

                C_Orders(const ulong magic)
                        :C_Terminal(), m_MagicNumber(magic)
                        {
                                m_Infos.MagicNumber     = magic;
                                m_Infos.nDigits         = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
                                m_Infos.VolMinimal      = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
                                m_Infos.VolStep         = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
                                m_Infos.PointPerTick    = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
                                m_Infos.ValuePerPoint   = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
                                m_Infos.AdjustToTrade   = m_Infos.ValuePerPoint / m_Infos.PointPerTick;
                                m_Infos.PlotLast        = (SymbolInfoInteger(_Symbol, SYMBOL_CHART_MODE) == SYMBOL_CHART_MODE_LAST);
                        };

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

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

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

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

inline void CommonData(const ENUM_ORDER_TYPE type, const double Price, const double FinanceStop, const double FinanceTake, const uint Leverage, const bool IsDayTrade)
                        {
                                double Desloc;
                                
                                ZeroMemory(m_TradeRequest);
                                m_TradeRequest.magic            = m_Infos.MagicNumber;
                                m_TradeRequest.magic            = m_MagicNumber;
                                m_TradeRequest.symbol           = _Symbol;
                                m_TradeRequest.volume           = NormalizeDouble(m_Infos.VolMinimal + (m_Infos.VolStep * (Leverage - 1)), m_Infos.nDigits);
                                m_TradeRequest.volume           = NormalizeDouble(GetTerminalInfos().VolMinimal + (GetTerminalInfos().VolStep * (Leverage - 1)), GetTerminalInfos().nDigits);
                                m_TradeRequest.price            = NormalizeDouble(Price, m_Infos.nDigits);
                                m_TradeRequest.price            = NormalizeDouble(Price, GetTerminalInfos().nDigits);
                                Desloc = FinanceToPoints(FinanceStop, Leverage);
                                m_TradeRequest.sl               = NormalizeDouble(Desloc == 0 ? 0 : Price + (Desloc * (type == ORDER_TYPE_BUY ? -1 : 1)), m_Infos.nDigits);
                                m_TradeRequest.sl               = NormalizeDouble(Desloc == 0 ? 0 : Price + (Desloc * (type == ORDER_TYPE_BUY ? -1 : 1)), GetTerminalInfos().nDigits);
                                Desloc = FinanceToPoints(FinanceTake, Leverage);
                                m_TradeRequest.tp               = NormalizeDouble(Desloc == 0 ? 0 : Price + (Desloc * (type == ORDER_TYPE_BUY ? 1 : -1)), m_Infos.nDigits);
                                m_TradeRequest.tp               = NormalizeDouble(Desloc == 0 ? 0 : Price + (Desloc * (type == ORDER_TYPE_BUY ? 1 : -1)), GetTerminalInfos().nDigits);
                                m_TradeRequest.type_time        = (IsDayTrade ? ORDER_TIME_DAY : ORDER_TIME_GTC);
                                m_TradeRequest.stoplimit        = 0;
                                m_TradeRequest.expiration       = 0;
                                m_TradeRequest.type_filling     = ORDER_FILLING_RETURN;
                                m_TradeRequest.deviation        = 1000;
                                m_TradeRequest.comment          = "Order Generated by Experts Advisor.";
                        }

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

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

GetTerminalInfos().nDigits

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

inline const stTerminal GetTerminalInfos(void) const
                        {
                                return m_TerminalInfo;
                        }

Обратите внимание, что функция GetTerminalInfos возвращает структуру, которая показана во фрагменте ниже:

                struct stTerminal
                {
                        ENUM_SYMBOL_CHART_MODE ChartMode;
                        int     nDigits;
                        double  VolMinimal,
                                VolStep,
                                PointPerTick,
                                ValuePerPoint,
                                AdjustToTrade;
                };

Таким образом, для компилятора то, что мы делаем с использованием кода GetTerminalInfos().nDigits, будет эквивалентно утверждению, что GetTerminalInfos() - это не функция, а переменная 😲. Вас это сбило с толку? Что ж, всё становится еще интереснее, поскольку для компилятора код GetTerminalInfos().nDigits будет эквивалентен следующему коду:

stTerminal info;
int value = info.nDigits;

value = 10;
info.nDigits = value;

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

GetTerminalInfos().nDigits = 10;

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

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

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

GetTerminalInfos().nDigits = 10;

Если вы попытаетесь это сделать, то компилятор выдаст ошибку. Потому что для компилятора считается константой GetTerminalInfos().nDigits или любая другая вещь внутри структуры, на которую ссылается GetTerminalInfos(), а значение константы невозможно изменить. Это считается ошибкой.

Теперь вы понимаете, как ссылаться на постоянные данные, используя переменную? То есть для класса C_Terminal структура, на которую ссылается функция GetTerminalInfos() , является переменной, но для любой другой части кода структура будет константой 😁.

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

                ulong CreateOrder(const ENUM_ORDER_TYPE type, const double Price, const double FinanceStop, const double FinanceTake, const uint Leverage, const bool IsDayTrade)
                        {
                                double  bid, ask;
                                
                                bid = SymbolInfoDouble(_Symbol, (m_Infos.PlotLast ? SYMBOL_LAST : SYMBOL_BID));
                                bid = SymbolInfoDouble(_Symbol, (GetTerminalInfos().ChartMode == SYMBOL_CHART_MODE_LAST ? SYMBOL_LAST : SYMBOL_BID));
                                ask = (m_Infos.PlotLast ? bid : SymbolInfoDouble(_Symbol, SYMBOL_ASK));
                                ask = (GetTerminalInfos().ChartMode == SYMBOL_CHART_MODE_LAST ? bid : SymbolInfoDouble(_Symbol, SYMBOL_ASK));
                                CommonData(type, AdjustPrice(Price), FinanceStop, FinanceTake, Leverage, IsDayTrade);
                                m_TradeRequest.action   = TRADE_ACTION_PENDING;
                                m_TradeRequest.type     = (type == ORDER_TYPE_BUY ? (ask >= Price ? ORDER_TYPE_BUY_LIMIT : ORDER_TYPE_BUY_STOP) : 
                                                                                    (bid < Price ? ORDER_TYPE_SELL_LIMIT : ORDER_TYPE_SELL_STOP));                              
                                
                                return (((type == ORDER_TYPE_BUY) || (type == ORDER_TYPE_SELL)) ? ToServer() : 0);
                        };

И последняя функция, которую нужно изменить, показана ниже:

                bool ModifyPricePoints(const ulong ticket, const double Price, const double PriceStop, const double PriceTake)
                        {
                                ZeroMemory(m_TradeRequest);
                                m_TradeRequest.symbol   = _Symbol;
                                if (OrderSelect(ticket))
                                {
                                        m_TradeRequest.action   = (Price > 0 ? TRADE_ACTION_MODIFY : TRADE_ACTION_REMOVE);
                                        m_TradeRequest.order    = ticket;
                                        if (Price > 0)
                                        {
                                                m_TradeRequest.price      = NormalizeDouble(AdjustPrice(Price), m_Infos.nDigits);
                                                m_TradeRequest.sl         = NormalizeDouble(AdjustPrice(PriceStop), m_Infos.nDigits);
                                                m_TradeRequest.tp         = NormalizeDouble(AdjustPrice(PriceTake), m_Infos.nDigits);
                                                m_TradeRequest.price      = NormalizeDouble(AdjustPrice(Price), GetTerminalInfos().nDigits);
                                                m_TradeRequest.sl         = NormalizeDouble(AdjustPrice(PriceStop), GetTerminalInfos().nDigits);
                                                m_TradeRequest.tp         = NormalizeDouble(AdjustPrice(PriceTake), GetTerminalInfos().nDigits);
                                                m_TradeRequest.type_time  = (ENUM_ORDER_TYPE_TIME)OrderGetInteger(ORDER_TYPE_TIME) ;
                                                m_TradeRequest.expiration = 0;
                                        }
                                }else if (PositionSelectByTicket(ticket))
                                {
                                        m_TradeRequest.action   = TRADE_ACTION_SLTP;
                                        m_TradeRequest.position = ticket;
                                        m_TradeRequest.tp       = NormalizeDouble(AdjustPrice(PriceTake), m_Infos.nDigits);
                                        m_TradeRequest.sl       = NormalizeDouble(AdjustPrice(PriceStop), m_Infos.nDigits);
                                        m_TradeRequest.tp       = NormalizeDouble(AdjustPrice(PriceTake), GetTerminalInfos().nDigits);
                                        m_TradeRequest.sl       = NormalizeDouble(AdjustPrice(PriceStop), GetTerminalInfos().nDigits);
                                }else return false;
                                ToServer();
                                
                                return (_LastError == ERR_SUCCESS);
                        };

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

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

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


Создание линии тейк-профит и стоп-лосс

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

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        uint            BtnStatus;
        double  Price;
        static double mem = 0;
        
        (*mouse).DispatchMessage(id, lparam, dparam, sparam);
        (*mouse).GetStatus(Price, BtnStatus);
        if (TerminalInfoInteger(TERMINAL_KEYSTATE_CONTROL))
        {
                if (TerminalInfoInteger(TERMINAL_KEYSTATE_UP))  (*manager).ToMarket(ORDER_TYPE_BUY, user03, user02, user01, user04);
                if (TerminalInfoInteger(TERMINAL_KEYSTATE_DOWN))(*manager).ToMarket(ORDER_TYPE_SELL, user03, user02, user01, user04);
        }
        if (def_SHIFT_Press(BtnStatus) != def_CTRL_Press(BtnStatus))
        {
// This point ...
                if (def_BtnLeftClick(BtnStatus) && (mem == 0)) (*manager).CreateOrder(def_SHIFT_Press(BtnStatus) ? ORDER_TYPE_BUY : ORDER_TYPE_SELL, mem = Price, user03, user02, user01, user04);
        }else mem = 0;
}

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

Лучшая альтернатива этому (и я думаю, что все со мной согласятся) - это разместить код в классе C_Mouse. Поэтому, когда мы убираем мышь, то и линии также исчезают. Это именно то, что мы будем делать. Давайте перейдем к классу C_Mouse, чтобы создать линии, которые будут представлять тейк-профит и стоп-лосс.

Но я сделаю что-то немного отличающееся от того, что я представлял раньше. Я буду добавлять строки не в событие OnChartEvent, а в обработчик события внутри класса C_Mouse. Таким образом всё получится лучше, несмотря на то, что придется внести некоторые другие изменения в код советника, но мы оставим это на потом. Итак, теперь перейдем к заголовочному файлу C_Mouse.mqh и реализуем всё необходимое.

Первое, что мы сделаем - добавим несколько новых определений, как показано ниже:

#define def_PrefixNameObject    "MOUSE_"
#define def_MouseLineName       def_PrefixNameObject + "H"
#define def_MouseLineTake       def_PrefixNameObject + "T"
#define def_MouseLineStop       def_PrefixNameObject + "S"
#define def_MouseName           "MOUSE_H"

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

                void CreateLineH(void)
                void CreateLineH(const string szName, const color cor)
                        {
                                ObjectCreate(m_Infos.Id, def_MouseName, OBJ_HLINE, 0, 0, 0);
                                ObjectSetString(m_Infos.Id, def_MouseName, OBJPROP_TOOLTIP, "\n");
                                ObjectSetInteger(m_Infos.Id, def_MouseName, OBJPROP_BACK, false);
                                ObjectSetInteger(m_Infos.Id, def_MouseName, OBJPROP_COLOR, m_Infos.Cor);
                                ObjectCreate(m_Infos.Id, szName, OBJ_HLINE, 0, 0, 0);
                                ObjectSetString(m_Infos.Id, szName, OBJPROP_TOOLTIP, "\n");
                                ObjectSetInteger(m_Infos.Id, szName, OBJPROP_BACK, false);
                                ObjectSetInteger(m_Infos.Id, szName, OBJPROP_COLOR, cor);
                        }

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

                C_Mouse(const color corPrice, const color corTake, const color corStop, const double FinanceStop, const double FinanceTake, const uint Leverage)
                        {
                                m_Infos.Id        = ChartID();
                                m_Infos.CorPrice  = corPrice;
                                m_Infos.CorTake   = corTake;
                                m_Infos.CorStop   = corStop;
                                m_Infos.PointsTake= FinanceToPoints(FinanceTake, Leverage);
                                m_Infos.PointsStop= FinanceToPoints(FinanceStop, Leverage);
                                ChartSetInteger(m_Infos.Id, CHART_EVENT_MOUSE_MOVE, true);
                                ChartSetInteger(m_Infos.Id, CHART_EVENT_OBJECT_DELETE, true);
                                CreateLineH(def_MouseLineName, m_Infos.CorPrice);
                        }

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

                ~C_Mouse()
                        {
                                ChartSetInteger(m_Infos.Id, CHART_EVENT_OBJECT_DELETE, false);
                                ObjectsDeleteAll(m_Infos.Id, def_PrefixNameObject);
                                ObjectDelete(m_Infos.Id, def_MouseName);
                        }

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

                void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
                        {
                                int w;
                                datetime dt;
                                static bool bView = false;
                                
                                switch (id)
                                        {
                                                case CHARTEVENT_OBJECT_DELETE:
                                                        if (sparam == def_MouseName) CreateLineH();
                                                        if (sparam == def_MouseLineName) CreateLineH(def_MouseLineName, m_Infos.CorPrice);
                                                        break;
                                                case CHARTEVENT_MOUSE_MOVE:
                                                        ChartXYToTimePrice(m_Infos.Id, (int)lparam, (int)dparam, w, dt, m_Infos.Price);
                                                        ObjectMove(m_Infos.Id, def_MouseName, 0, 0, m_Infos.Price = AdjustPrice(m_Infos.Price));
                                                        ObjectMove(m_Infos.Id, def_MouseLineName, 0, 0, m_Infos.Price = AdjustPrice(m_Infos.Price));
                                                        m_Infos.BtnStatus = (uint)sparam;
                                                        if (def_CTRL_Press(m_Infos.BtnStatus) != def_SHIFT_Press(m_Infos.BtnStatus))
                                                        {
								if (!bView)
								{
									if (m_Infos.PointsTake > 0) CreateLineH(def_MouseLineTake, m_Infos.CorTake);
									if (m_Infos.PointsStop > 0) CreateLineH(def_MouseLineStop, m_Infos.CorStop);
									bView = true;
								}
								if (m_Infos.PointsTake > 0) ObjectMove(m_Infos.Id, def_MouseLineTake, 0, 0, m_Infos.Price + (m_Infos.PointsTake * (def_SHIFT_Press(m_Infos.BtnStatus) ? 1 : -1)));
								if (m_Infos.PointsStop > 0) ObjectMove(m_Infos.Id, def_MouseLineStop, 0, 0, m_Infos.Price + (m_Infos.PointsStop * (def_SHIFT_Press(m_Infos.BtnStatus) ? -1 : 1)));
                                                        }else if (bView)
                                                        {
                                                                ObjectsDeleteAll(m_Infos.Id, def_PrefixNameObject);
                                                                bView = false;
                                                        }
                                                        ChartRedraw();
                                                        break;
                                        }
                        }

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

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

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


Заключение

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