Как построить советник, работающий автоматически (Часть 02): Начинаем писать код

Daniel Jose | 26 января, 2023

Введение

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

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

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


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

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

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


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

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

Рисунок 1

Рисунок 01 - Добавляем включаемый файл

Рисунок 02

Рисунок 02 - Вот так создается нужный нам файл


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

#property copyright "Daniel Jose"
#property link      ""
//+------------------------------------------------------------------+
//| defines                                                          |
//+------------------------------------------------------------------+
// #define MacrosHello   "Hello, world!"
// #define MacrosYear    2010
//+------------------------------------------------------------------+
//| DLL imports                                                      |
//+------------------------------------------------------------------+
// #import "user32.dll"
//   int      SendMessageA(int hWnd,int Msg,int wParam,int lParam);
// #import "my_expert.dll"
//   int      ExpertRecalculate(int wParam,int lParam);
// #import
//+------------------------------------------------------------------+
//| EX5 imports                                                      |
//+------------------------------------------------------------------+
// #import "stdlib.ex5"
//   string ErrorDescription(int error_code);
// #import
//+------------------------------------------------------------------+

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

Чтобы сделать наш код безопасным и надежным, мы используем инструмент, который MQL5 принес из C++: классы. Если вы не знаете, что такое классы, я вам рекомендую ознакомиться с этой темой. Вам не обязательно почитать непосредственно документацию C++ о классах; на самом деле, можете начать с классов в документации MQL5, что обеспечит вас хорошей отправной точки. Кроме того, содержание документации по MQL5 легче понять, чем всю ту путаницу, которую C++ может вызвать у людей, никогда не слышавших о классах.

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

А теперь, давайте двигаться дальше. Первое, что мы сделаем, — сгенерируем следующие строки:

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
class C_Orders
{
        private :
        public  :
};
//+------------------------------------------------------------------+

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

Одним словом: всё, что находится между объявлением слова private и словом public, может быть доступно исключительно внутри класса. Здесь вы сможете использовать глобальные переменные, к которым нельзя получить доступ вне кода класса. Может быть доступно всё, что объявлено после слова public в любом месте кода, независимо от того, является ли оно частью класса или нет. Любой человек может получить доступ к тому, что там находится.

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

#property copyright "Daniel Jose"
#property version   "1.00"
#property link      "https://www.mql5.com/pt/articles/11223"
//+------------------------------------------------------------------+
#include <Generic Auto Trader\C_Orders.mqh>
//+------------------------------------------------------------------+
int OnInit()
{
        return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
}
//+------------------------------------------------------------------+
void OnTick()
{
}
//+------------------------------------------------------------------+

На этом этапе мы включаем наш класс в советник с помощью директивы компиляции Include. Когда мы используем эту директиву, компилятор понимает, что с этого момента заголовочный файл C_Orders.mqh, находящийся в каталоге include папки Generic Auto Trader, должен быть включен в систему и потом скомпилирован. Есть несколько хитростей по этому поводу, но я не буду вдаваться в подробности, потому что попытки разобраться в этом оставит менее опытных людей в полной растерянности, но происходит именно то, что я описал.


Определяем первые функции класса C_Orders

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

Поэтому и вам, только начинающим свою карьеру, следует поступать таким же образом, т.е., перед тем, как добавить какие-либо строки кода, вы должны продумать все нужные моменты. Давайте поразмышляем: что нам действительно нужно иметь в классе C_Orders, чтобы проделать как можно меньше работы и максимально использовать возможности платформы MetaTrader 5?

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

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

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

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

class C_Orders
{
        private :
//+------------------------------------------------------------------+
                MqlTradeRequest m_TradeRequest;
                struct st00
                {
                        int     nDigits;
                        double  VolMinimal,
                                VolStep,
                                PointPerTick,
                                ValuePerPoint,
                                AdjustToTrade;
                        bool    PlotLast;
                }m_Infos;

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

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

                C_Orders()
                        {
                                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_Orders() { }

Посмотрите на синтаксис. Имя конструктора и деструктора совпадают с именем класса. Однако объявлению деструктора предшествует тильда (~). Кроме того, важно помнить, что как конструктор, так и деструктор не возвращают значения какого-либо типа. Попытка сделать это считается ошибкой и сделает компилирование кода невозможным.

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

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

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

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

        m_Infos.PlotLast = (SymbolInfoInteger(_Symbol, SYMBOL_CHART_MODE) == SYMBOL_CHART_MODE_LAST);

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

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

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

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

Эта проблема возникает при попытке использовать на рынке ФОРЕКС советника, созданного для ФОНДОВОГО рынка. Но верно и обратное: если система графического представления имеет вид LAST, но советник был создан и построен для работы на рынке форекс, где система графиков имеет тип BID, то советник может не отправить ордера в нужный момент, так как цена LAST может меняться, в то время как BID и ASK остаются статичными.

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

По этой причине советник проверяет, на каком виде рынка или графической системы он работает, чтобы его можно было использовать или переносить с рынка ФОРЕКСА на ФОНДОВЫЙ рынок или наоборот без необходимости модификации или перекомпиляции.

Мы уже видели, как важны мелкие нюансы. Простая деталь может поставить под угрозу всё. Теперь посмотрим, как отправить отложенный ордер на торговый сервер.


Отправляем отложенный ордер на сервер

После изучения документации по работе торговой системы на языке MQL5, мы обнаружили функцию OrderSend. Важно помнить, что не обязательно использовать данную функцию напрямую, поскольку можно отправлять ордера на сервер с помощью стандартной библиотеки MetaTrader 5, через класс CTrade. Однако здесь я хочу показать работу этой функции «за кулисами».

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

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

                ulong ToServer(void)
                        {
                                MqlTradeCheckResult TradeCheck;
                                MqlTradeResult      TradeResult;
                                bool bTmp;
                                
                                ResetLastError();
                                ZeroMemory(TradeCheck);
                                ZeroMemory(TradeResult);
                                bTmp = OrderCheck(m_TradeRequest, TradeCheck);
                                if (_LastError == ERR_SUCCESS) bTmp = OrderSend(m_TradeRequest, TradeResult);
                                if (_LastError != ERR_SUCCESS) MessageBox(StringFormat("Error Number: %d", GetLastError()), "Order System", MB_OK);
                
                                return (_LastError == ERR_SUCCESS ? TradeResult.order : 0);
                        }

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

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

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

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

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

inline double AdjustPrice(const double value)
                        {
                                return MathRound(value / m_Infos.PointPerTick) * m_Infos.PointPerTick;
                        }

Этот простой расчет скорректирует цену до соответствующего значения, независимо от введенного значения, и сервер примет указанную цену.

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

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

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

Но прежде чем создавать функцию, надо немного подумать. Нам нужно ответить на несколько вопросов: