English 中文 Español Deutsch 日本語 Português
preview
Как построить советник, работающий автоматически (Часть 06): Виды счетов (I)

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

MetaTrader 5Трейдинг | 16 февраля 2023, 15:05
1 833 0
Daniel Jose
Daniel Jose

Введение

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

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


Рождение класса C_Manager

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

Давайте теперь посмотрим, как начнет строится данный класс. Ниже показываем его начальный код:

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#include "C_Orders.mqh"
//+------------------------------------------------------------------+
class C_Manager : private C_Orders
{
        private :
                struct st00
                {
                        double  FinanceStop,
                                FinanceTake;
                        uint    Leverage;
                        bool    IsDayTrade;
                }m_InfosManager;
        public  :
//+------------------------------------------------------------------+
                C_Manager(const ulong magic, double FinanceStop, double FinanceTake, uint Leverage, bool IsDayTrade)
                        :C_Orders(magic)
                        {
                                m_InfosManager.FinanceStop = FinanceStop;
                                m_InfosManager.FinanceTake = FinanceTake;
                                m_InfosManager.Leverage    = Leverage;
                                m_InfosManager.IsDayTrade  = IsDayTrade;
                        }
//+------------------------------------------------------------------+
                ~C_Manager() { }
//+------------------------------------------------------------------+
                void CreateOrder(const ENUM_ORDER_TYPE type, const double Price)
                        {
                                C_Orders::CreateOrder(type, Price, m_InfosManager.FinanceStop, m_InfosManager.FinanceTake, m_InfosManager.Leverage, m_InfosManager.IsDayTrade);
                        }
//+------------------------------------------------------------------+  
                void ToMarket(const ENUM_ORDER_TYPE type)
                        {
                                C_Orders::ToMarket(type, m_InfosManager.FinanceStop, m_InfosManager.FinanceTake, m_InfosManager.Leverage, m_InfosManager.IsDayTrade);
                        }
//+------------------------------------------------------------------+
};

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

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

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

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

int OnInit()
{
        manager = new C_Orders(def_MAGIC_NUMBER);
        manager = new C_Manager(def_MAGIC_NUMBER, user03, user02, user01, user04);
        mouse = new C_Mouse(user05, user06, user07, user03, user02, user01);

        return INIT_SUCCEEDED;
}

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

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 (TerminalInfoInteger(TERMINAL_KEYSTATE_UP))  (*manager).ToMarket(ORDER_TYPE_BUY);
                if (TerminalInfoInteger(TERMINAL_KEYSTATE_DOWN))(*manager).ToMarket(ORDER_TYPE_SELL);
        }
        if ((def_SHIFT_Press(BtnStatus) != def_CTRL_Press(BtnStatus)) && def_BtnLeftClick(BtnStatus))
        {
                if (mem == 0) (*manager).CreateOrder((def_SHIFT_Press(BtnStatus) ? ORDER_TYPE_BUY : ORDER_TYPE_SELL), mem = Price, user03, user02, user01, user04);
                if (mem == 0) (*manager).CreateOrder((def_SHIFT_Press(BtnStatus) ? ORDER_TYPE_BUY : ORDER_TYPE_SELL), mem = Price);
        }else mem = 0;
}

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


Счет NETTING, счет EXCHANGE или счет HEDGING... вот в чем вопрос.

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

В общем, для целей этой статьи я упомяну только два типа счетов: NETTING и HEDGING. Причина проста: счет NETTING работает для советника так же, как и счет EXCHANGE, поэтому я не буду говорить о трех моделях, а только о двух.

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

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

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

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

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

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

Таким образом, в классе C_Manager появится следующий код:

class C_Manager : private C_Orders
{
        private :
                struct st00
                {
                        double  FinanceStop,
                                FinanceTake;
                        uint    Leverage;
                        bool    IsDayTrade;
                }m_InfosManager;
//---
                struct st01
                {
                        ulong   Ticket;
                        double  SL,
                                TP,
                                PriceOpen,
                                Gap;
                        bool    EnableBreakEven,
                                IsBuy;
                        int     Leverage;
                }m_Position;
                ulong           m_TicketPending;
                bool            m_bAccountHedging;
		double		m_Trigger;

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

Именно так мы начинаем нормализовать вещи; после этого мы можем внести изменения в конструктор класса, как показано ниже:

                C_Manager(const ulong magic, double FinanceStop, double FinanceTake, uint Leverage, bool IsDayTrade, double Trigger)
                        :C_Orders(magic),
                        m_bAccountHedging(false),
                        m_TicketPending(0),
                        m_Trigger(Trigger)
                        {
                                string szInfo;
                                
                                ZeroMemory(m_Position);
                                m_InfosManager.FinanceStop = FinanceStop;
                                m_InfosManager.FinanceTake = FinanceTake;
                                m_InfosManager.Leverage    = Leverage;
                                m_InfosManager.IsDayTrade  = IsDayTrade;
                                switch ((ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE))
                                {
                                        case ACCOUNT_MARGIN_MODE_RETAIL_HEDGING:
                                                m_bAccountHedging = true;
                                                szInfo = "HEDGING";
                                                break;
                                        case ACCOUNT_MARGIN_MODE_RETAIL_NETTING:
                                                szInfo = "NETTING";
                                                break;
                                        case ACCOUNT_MARGIN_MODE_EXCHANGE:
                                                szInfo = "EXCHANGE";
                                                break;
                                }
                                Print("Detected Account ", szInfo);
                        }

Я покажу код постепенно, для тех, у кого нет опыта или знаний в программировании. Надеюсь, я не скучно выражаюсь, поскольку хочу, чтобы все могли понять то, что я делаю. Давайте перейдем к объяснениям. Эти строки «говорят» компилятору, что мы хотим, чтобы эти переменные были инициализированы до того, как начнет выполняться код конструктора. Можно подумать об этом так: При создании переменной компилятор обычно присваивает ей значение, равное нулю.

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

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

class C_Manager : private C_Orders
{
        enum eErrUser {ERR_Unknown};
        private :

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

};

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

inline void LoadOrderValid(void)
                        {
                                ulong value;
                                
                                for (int c0 = OrdersTotal() - 1; (c0 >= 0) && (_LastError == ERR_SUCCESS); c0--)
                                {
                                        if ((value = OrderGetTicket(c0)) == 0) continue;
                                        if (OrderGetString(ORDER_SYMBOL) != _Symbol) continue;
                                        if (OrderGetInteger(ORDER_MAGIC) != GetMagicNumber()) continue;
                                        if (m_TicketPending > 0) SetUserError(ERR_Unknown); else m_TicketPending = value;
                                }
                        }

Давайте разберемся, как работает этот код и почему он имеет такой необычный вид. Здесь мы используем цикл для чтения всех отложенных ордеров в стакане заявок; функция OrdersTotal вернет значение больше нуля, если ордера существуют, но индекс всегда будет начинаться с нуля. Это пришло из C / C++. Но у нас есть два условия для завершения цикла: первое - значение переменной c0 меньше нуля, а второе - чтобы _LastError отличался от ERR_SUCESS, что указывает на то, что в советнике произошел какой-то сбой.

Таким образом, мы входим в цикл и захватываем первый ордер, индекс которого обозначен переменной c0, OrderGetTicket вернет значение тикета ордера или ноль. Если он равен нулю, мы вернемся в цикл, но теперь мы вычитаем одну единицу из переменной c0.

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

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

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

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

inline void LoadPositionValid(void)
                        {
                                ulong value;
                                
                                for (int c0 = PositionsTotal() - 1; (c0 >= 0) && (_LastError == ERR_SUCCESS); c0--)
                                {
                                        if ((value = PositionGetTicket(c0)) == 0) continue;
                                        if (PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
                                        if (PositionGetInteger(POSITION_MAGIC) != GetMagicNumber()) continue;
                                        if (m_Position.Ticket > 0) SetUserError(ERR_Unknown); else
					{
						m_Position.Ticket = value;
						SetInfoPositions();
					}
                                }
                        }

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

inline int SetInfoPositions(void)
                        {
                                double v1, v2;
                                int tmp = m_Position.Leverage;
                                
                                m_Position.Leverage = (int)(PositionGetDouble(POSITION_VOLUME) / GetTerminalInfos().VolMinimal);
                                m_Position.IsBuy = ((ENUM_POSITION_TYPE) PositionGetInteger(POSITION_TYPE)) == POSITION_TYPE_BUY;
                                m_Position.TP = PositionGetDouble(POSITION_TP);
                                v1 = m_Position.SL = PositionGetDouble(POSITION_SL);
                                v2 = m_Position.PriceOpen = PositionGetDouble(POSITION_PRICE_OPEN);
                                m_Position.EnableBreakEven = (m_Position.IsBuy ? (v1 < v2) : (v1 > v2));
                                m_Position.Gap = FinanceToPoints(m_Trigger, m_Position.Leverage);

                                return m_Position.Leverage - tmp;
                        }

Этот код очень интересен, если речь идет о работе с последними данными позиции, но будьте внимательны, перед его вызовом необходимо обновить данные о позиции одним из следующих вызовов: PositionGetTicket, PositionSelect, PositionGetSymbol и PositionSelectByTicket. В общем, мы здесь всё инициализируем или настраиваем по необходимости. То, что мы поместили этот код отдельно, объясняется тем, что мы будем использовать его в других местах для обновления данных о позиции, когда это будет необходимо.

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

                C_Manager(const ulong magic, double FinanceStop, double FinanceTake, uint Leverage, bool IsDayTrade, double Trigger)
                        :C_Orders(magic),
                        m_bAccountHedging(false),
                        m_TicketPending(0),
                        m_Trigger(Trigger)
                        {
                                string szInfo;
                                
                                ResetLastError();
                                ZeroMemory(m_Position);
                                m_InfosManager.FinanceStop = FinanceStop;
                                m_InfosManager.FinanceTake = FinanceTake;
                                m_InfosManager.Leverage    = Leverage;
                                m_InfosManager.IsDayTrade  = IsDayTrade;
                                switch ((ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE))
                                {
                                        case ACCOUNT_MARGIN_MODE_RETAIL_HEDGING:
                                                m_bAccountHedging = true;
                                                szInfo = "HEDGING";
                                                break;
                                        case ACCOUNT_MARGIN_MODE_RETAIL_NETTING:
                                                szInfo = "NETTING";
                                                break;
                                        case ACCOUNT_MARGIN_MODE_EXCHANGE:
                                                szInfo = "EXCHANGE";
                                                break;
                                }
                                Print("Detected Account ", szInfo);
                                LoadPositionValid();
                                LoadOrderValid();
                                if (_LastError == ERR_SUCCESS)
                                {
                                        szInfo = "Successful upload...";
                                        szInfo += StringFormat("%s", (m_Position.Ticket > 0 ? "\nTicket Position: " + (string)m_Position.Ticket : ""));
                                        szInfo += StringFormat("%s", (m_TicketPending > 0 ? "\nTicket Order: " + (string)m_TicketPending : ""));
                                        Print(szInfo);
                                }
                        }

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

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

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


Существует одна проблема в счете HEDGING для автоматического советника

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

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

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

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

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

class C_Orders : protected C_Terminal
{
        protected:
//+------------------------------------------------------------------+
inline const ulong GetMagicNumber(void) const { return m_MagicNumber; }
//+------------------------------------------------------------------+
                void RemoveOrderPendent(const ulong ticket)
                        {
                                ZeroMemory(m_TradeRequest);
                                m_TradeRequest.action   = TRADE_ACTION_REMOVE;
                                m_TradeRequest.order    = ticket;
                                ToServer();
                        };

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

}

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

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

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

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

inline void LoadOrderValid(void)
                        {
                                ulong value;
                                
                                for (int c0 = OrdersTotal() - 1; (c0 >= 0) && (_LastError == ERR_SUCCESS); c0--)
                                {
                                        if ((value = OrderGetTicket(c0)) == 0) continue;
                                        if (OrderGetString(ORDER_SYMBOL) != _Symbol) continue;
                                        if (OrderGetInteger(ORDER_MAGIC) != GetMagicNumber()) continue;
                                        if ((m_bAccountHedging) && (m_Position.Ticket > 0))
                                        {
                                                RemoveOrderPendent(value);
                                                continue;
                                        }
                                        if (m_TicketPending > 0) SetUserError(ERR_Unknown); else m_TicketPending = value;
                                }
                        }

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

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

inline void LoadPositionValid(void)
                        {
                                ulong value;
                                
                                for (int c0 = PositionsTotal() - 1; (c0 >= 0) && (_LastError == ERR_SUCCESS); c0--)
                                {
                                        if ((value = PositionGetTicket(c0)) == 0) continue;
                                        if (PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
                                        if (PositionGetInteger(POSITION_MAGIC) != GetMagicNumber()) continue;
                                        if ((m_bAccountHedging) && (m_TicketPending > 0))
                                        {
                                                ClosePosition(value);
                                                continue;
                                        }
                                        if (m_Position.Ticket > 0) SetUserError(ERR_Unknown); else
					{
						m_Position.Ticket = value;
						SetInfoPositions();
					}
                                }
                        }

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

                C_Manager(const ulong magic, double FinanceStop, double FinanceTake, uint Leverage, bool IsDayTrade, double Trigger)
                        :C_Orders(magic),
                        m_bAccountHedging(false),
                        m_TicketPending(0),
                        m_Trigger(Trigger)
                        {
                                string szInfo;
                                
                                ResetLastError();
                                ZeroMemory(m_Position);
                                m_InfosManager.FinanceStop = FinanceStop;
                                m_InfosManager.FinanceTake = FinanceTake;
                                m_InfosManager.Leverage    = Leverage;
                                m_InfosManager.IsDayTrade  = IsDayTrade;
                                switch ((ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE))
                                {
                                        case ACCOUNT_MARGIN_MODE_RETAIL_HEDGING:
                                                m_bAccountHedging = true;
                                                szInfo = "HEDGING";
                                                break;
                                        case ACCOUNT_MARGIN_MODE_RETAIL_NETTING:
                                                szInfo = "NETTING";
                                                break;
                                        case ACCOUNT_MARGIN_MODE_EXCHANGE:
                                                szInfo = "EXCHANGE";
                                                break;
                                }
                                Print("Detected Account ", szInfo);
                                LoadOrderValid();
                                LoadPositionValid();
                                if (_LastError == ERR_SUCCESS)
                                {
                                        szInfo = "Successful upload...";
                                        szInfo += StringFormat("%s", (m_Position.Ticket > 0 ? "\nTicket Position: " + (string)m_Position.Ticket + "\n" : ""));
                                        szInfo += StringFormat("%s", (m_TicketPending > 0 ? "\nTicket Order: " + (string)m_TicketPending : ""));
                                        Print(szInfo);
                                }
                        }

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

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

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

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


Заключение

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

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


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

Как построить советник, работающий автоматически (Часть 07): Виды счетов (II) Как построить советник, работающий автоматически (Часть 07): Виды счетов (II)
Сегодня посмотрим, как создать советник, просто и безопасно работающий в автоматическом режиме. Трейдеру всегда необходимо быть в курсе того, что делает автоматический советник, чтобы, если он «сойдет с рельсов», как можно быстрее удалить его с графика, прекратить таким образом его работу, и взять ситуацию под свой контроль.
Алан Эндрюс и его приемы анализа временных рядов Алан Эндрюс и его приемы анализа временных рядов
Алан Эндрюс — один из известнейших "просветителей" современного мира в области трейдинга. Его "вилы" включены практически во все современные программы анализа котировок. Но большинство трейдеров не используют и пятой части тех возможностей, что заложены в этом инструменте. А оригинальный курс Эндрюса включает описание не только вил (хотя они всё же главные), но и некоторых других полезных прямых. Эта статья даёт представление о тех изумительных техниках анализа графиков, которым учил Эндрюс в своем оригинальном курсе. Осторожно, много картинок.
Возможности Мастера MQL5, которые вам нужно знать (Часть 5): Цепи Маркова Возможности Мастера MQL5, которые вам нужно знать (Часть 5): Цепи Маркова
Цепи Маркова — это мощный математический инструмент, который можно использовать для моделирования и прогнозирования данных временных рядов в различных областях, включая финансы. При моделировании и прогнозировании финансовых временных рядов цепи Маркова часто используются для моделирования эволюции финансовых активов с течением времени, таких как цены акций или обменные курсы. Одними из основных преимуществ моделей цепей Маркова являются их простота и удобство использования.
Популяционные алгоритмы оптимизации: Алгоритм гравитационного поиска (Gravitational Search Algorithm - GSA) Популяционные алгоритмы оптимизации: Алгоритм гравитационного поиска (Gravitational Search Algorithm - GSA)
GSA — популяционный алгоритм оптимизации, инспирированный неживой природой. Высокая достоверность моделирования взаимодействия физических тел, благодаря закону гравитации Ньютона в алгоритме, позволяет наблюдать феерический танец планетарных систем и галактических скоплений, который завораживает своим представлением на анимации. Сегодня рассмотрим один из самых интересных и оригинальных алгоритмов оптимизации. Симулятор движения космических объектов прилагается.