English 中文 Español Deutsch 日本語 Português
preview
Как построить советник, работающий автоматически (Часть 11): Автоматизация (III)

Как построить советник, работающий автоматически (Часть 11): Автоматизация (III)

MetaTrader 5Трейдинг | 27 апреля 2023, 12:44
663 0
Daniel Jose
Daniel Jose

Введение

В предыдущей статье Как построить советник, работающий автоматически (Часть 10): Автоматизация (II), мы просмотрели способ добавления контроля за расписанием для запуска советника. Хотя вся система советника была построена так, чтобы предпочтение отдавалось автономности, перед переходом к последней фазе, где мы получим 100% автоматический советник, нам необходимо внести некоторые незначительные доработки и изменения в код.

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

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

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

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

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

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

Всё это для того, чтобы дойти к центральной точке, которая представлена на рисунке 01:

Рисунок 01

Рисунок 01 - Система ручного управления

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

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

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

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

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

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


Применение изменений

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

virtual const bool CtrlTimeIsPassed(void) final
                        {
                                datetime dt;
                                MqlDateTime mdt;
                                
                                TimeToStruct(TimeLocal(), mdt);
                                TimeCurrent(mdt);
                                dt = (mdt.hour * 3600) + (mdt.min * 60);
                                return ((m_InfoCtrl[mdt.day_of_week].Init <= dt) && (m_InfoCtrl[mdt.day_of_week].End >= dt));
                        }

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

Рисунок 02

Рисунок 02 - Время, сообщенное сервером в последнем обновлении.

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

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

Следующее изменение коснулось системы ордеров, как показано ниже:

                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);
                                if (_LastError != ERR_SUCCESS) PrintFormat("Order System - Error Number: %d", _LastError);
                                return (_LastError == ERR_SUCCESS ? TradeResult.order : 0);
                        }

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

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

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

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


Подготавливая путь к автоматизации

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

//+------------------------------------------------------------------+
#include "C_ControlOfTime.mqh"
//+------------------------------------------------------------------+
#define def_MAX_LEVERAGE                10
#define def_ORDER_FINISH                false
//+------------------------------------------------------------------+
class C_Manager : public C_ControlOfTime

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

class C_Manager : public C_ControlOfTime
{
        enum eErrUser {ERR_Unknown, ERR_Excommunicate};
        private :
                struct st00
                {
                        double  FinanceStop,
                                FinanceTake;
                        uint    Leverage,
                                MaxLeverage;
                        bool    IsDayTrade,
                                IsOrderFinish;                                          
                }m_InfosManager;

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

                C_Manager(const ulong magic, double FinanceStop, double FinanceTake, uint Leverage, bool IsDayTrade, double Trigger, const bool IsOrderFinish, const uint MaxLeverage)
                        :C_ControlOfTime(magic),
                        m_bAccountHedging(false),
                        m_TicketPending(0),
                        m_Trigger(Trigger)
                        {
                                string szInfo;
                                
                                ResetLastError();
                                ZeroMemory(m_Position);
                                m_InfosManager.IsOrderFinish    = IsOrderFinish;
                                m_InfosManager.MaxLeverage      = MaxLeverage;
                                m_InfosManager.FinanceStop      = FinanceStop;
                                m_InfosManager.FinanceTake      = FinanceTake;
                                m_InfosManager.Leverage         = Leverage;
                                m_InfosManager.IsDayTrade       = IsDayTrade;

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

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);
                                if (def_ORDER_FINISH) if (m_TicketPending > 0) if (OrderSelect(m_TicketPending)) v1 = OrderGetDouble(ORDER_PRICE_OPEN);
                                m_Position.EnableBreakEven = (def_ORDER_FINISH ? m_TicketPending == 0 : m_Position.EnableBreakEven) || (m_Position.IsBuy ? (v1 < v2) : (v1 > v2));
                                if (m_InfosManager.IsOrderFinish) if (m_TicketPending > 0) if (OrderSelect(m_TicketPending)) v1 = OrderGetDouble(ORDER_PRICE_OPEN);
                                m_Position.EnableBreakEven = (m_InfosManager.IsOrderFinish ? m_TicketPending == 0 : m_Position.EnableBreakEven) || (m_Position.IsBuy ? (v1 < v2) : (v1 > v2));
                                m_Position.Gap = FinanceToPoints(m_Trigger, m_Position.Leverage);

                                return m_Position.Leverage - tmp;
                        }
inline void TriggerBreakeven(void)
                        {
                                double price;
                                
                                if (PositionSelectByTicket(m_Position.Ticket))
                                        if (PositionGetDouble(POSITION_PROFIT) >= m_Trigger)
                                        {
                                                price = m_Position.PriceOpen + (GetTerminalInfos().PointPerTick * (m_Position.IsBuy ? 1 : -1));
                                                if (def_ORDER_FINISH)
                                                if (m_InfosManager.IsOrderFinish)
                                                {
                                                        if (m_TicketPending > 0) m_Position.EnableBreakEven = !ModifyPricePoints(m_TicketPending, price, 0, 0);
                                                }else m_Position.EnableBreakEven = !ModifyPricePoints(m_Position.Ticket, m_Position.PriceOpen, price, m_Position.TP);
                                        }
                        }
                void CreateOrder(const ENUM_ORDER_TYPE type, const double Price)
                        {
                                if (!CtrlTimeIsPassed()) return;
                                if ((m_StaticLeverage >= def_MAX_LEVERAGE) || (m_TicketPending > 0) || (m_bAccountHedging && (m_Position.Ticket > 0))) return;
                                if ((m_StaticLeverage >= m_InfosManager.MaxLeverage) || (m_TicketPending > 0) || (m_bAccountHedging && (m_Position.Ticket > 0))) return;
                                m_TicketPending = C_Orders::CreateOrder(type, Price, (def_ORDER_FINISH ? 0 : m_InfosManager.FinanceStop), (def_ORDER_FINISH ? 0 : m_InfosManager.FinanceTake), m_InfosManager.Leverage, m_InfosManager.IsDayTrade);
                                m_TicketPending = C_Orders::CreateOrder(type, Price, (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceStop), (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceTake), m_InfosManager.Leverage, m_InfosManager.IsDayTrade);
                        }
                void ToMarket(const ENUM_ORDER_TYPE type)
                        {
                                ulong tmp;
                                
                                if (!CtrlTimeIsPassed()) return;
                                if ((m_StaticLeverage >= def_MAX_LEVERAGE) || (m_bAccountHedging && (m_Position.Ticket > 0))) return;
                                if ((m_StaticLeverage >= m_InfosManager.MaxLeverage) || (m_bAccountHedging && (m_Position.Ticket > 0))) return;
                                tmp = C_Orders::ToMarket(type, (def_ORDER_FINISH ? 0 : m_InfosManager.FinanceStop), (def_ORDER_FINISH ? 0 : m_InfosManager.FinanceTake), m_InfosManager.Leverage, m_InfosManager.IsDayTrade);
                                tmp = C_Orders::ToMarket(type, (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceStop), (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceTake), m_InfosManager.Leverage, m_InfosManager.IsDayTrade);
                                m_Position.Ticket = (m_bAccountHedging ? tmp : (m_Position.Ticket > 0 ? m_Position.Ticket : tmp));
                        }
                void PendingToPosition(void)
                        {
                                ResetLastError();
                                if ((m_bAccountHedging) && (m_Position.Ticket > 0))
                                {
                                        if (def_ORDER_FINISH)
                                        if (m_InfosManager.IsOrderFinish)
                                        {
// ... Остальной код ...
                void UpdatePosition(const ulong ticket)
                        {
                                int ret;
                                double price;
                                
                                if ((ticket == 0) || (ticket != m_Position.Ticket) || (m_Position.Ticket == 0)) return;
                                if (PositionSelectByTicket(m_Position.Ticket))
                                {
                                        ret = SetInfoPositions();
                                        if (def_ORDER_FINISH)
                                        if (m_InfosManager.IsOrderFinish)
                                        {
                                                price = m_Position.PriceOpen + (FinanceToPoints(m_InfosManager.FinanceStop, m_Position.Leverage) * (m_Position.IsBuy ? -1 : 1));
                                                if (m_TicketPending > 0) if (OrderSelect(m_TicketPending))
                                                {
                                                        price = OrderGetDouble(ORDER_PRICE_OPEN);
                                                        C_Orders::RemoveOrderPendent(m_TicketPending);
                                                }
                                                if (m_Position.Ticket > 0)      m_TicketPending = C_Orders::CreateOrder(m_Position.IsBuy ? ORDER_TYPE_SELL : ORDER_TYPE_BUY, price, 0, 0, m_Position.Leverage, m_InfosManager.IsDayTrade);
                                        }
                                        m_StaticLeverage += (ret > 0 ? ret : 0);
                                }else
                                {
                                        ZeroMemory(m_Position);
                                        if ((def_ORDER_FINISH) && (m_TicketPending > 0))
                                        if ((m_InfosManager.IsOrderFinish) && (m_TicketPending > 0))
                                        {
                                                RemoveOrderPendent(m_TicketPending);
                                                m_TicketPending = 0;
                                        }
                                }
                                ResetLastError();
                        }
inline void TriggerTrailingStop(void)
                        {
                                double price, v1;
                                
                                if ((m_Position.Ticket == 0) || (def_ORDER_FINISH ? m_TicketPending == 0 : m_Position.SL == 0)) return;
                                if ((m_Position.Ticket == 0) || (m_InfosManager.IsOrderFinish ? m_TicketPending == 0 : m_Position.SL == 0)) return;
                                if (m_Position.EnableBreakEven) TriggerBreakeven(); else
                                {
                                        price = SymbolInfoDouble(_Symbol, (GetTerminalInfos().ChartMode == SYMBOL_CHART_MODE_LAST ? SYMBOL_LAST : (m_Position.IsBuy ? SYMBOL_ASK : SYMBOL_BID)));
                                        v1 = m_Position.SL;
                                        if (def_ORDER_FINISH)
                                        if (m_InfosManager.IsOrderFinish)
                                                if (OrderSelect(m_TicketPending)) v1 = OrderGetDouble(ORDER_PRICE_OPEN);
                                        if (v1 > 0) if (MathAbs(price - v1) >= (m_Position.Gap * 2)) 
                                        {
                                                price = v1 + (m_Position.Gap * (m_Position.IsBuy ? 1 : -1));
                                                if (def_ORDER_FINISH)
                                                if (m_InfosManager.IsOrderFinish)
                                                        ModifyPricePoints(m_TicketPending, price, 0, 0);
                                                else    ModifyPricePoints(m_Position.Ticket, m_Position.PriceOpen, price, m_Position.TP);
                                        }
                                }
                        }

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

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

inline void ClosePosition(void)
	{
		if (m_Position.Ticket > 0)
		{
			C_Orders::ClosePosition(m_Position.Ticket);
			ZeroMemory(m_Position.Ticket);
		}                               
	}

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

Рисунок 03

Рисунок 03 - Предупреждения компилятора

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

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

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

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

Чтобы устранить вышеупомянутые предупреждения, у нас есть два варианта. Первый - заменить вызовы функции ClosePosition, которые в настоящее время ссылаются на функцию, присутствующую в классе C_Orders, на новую функцию, добавленную в классе C_Manager. Это лучший вариант, так как мы протестируем вызов, присутствующий в C_Manager. Второй вариант - сообщить компилятору, что вызовы ссылаются на класс C_Orders.

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

                ~C_Manager()
                        {
                                if (_LastError == (ERR_USER_ERROR_FIRST + ERR_Excommunicate))
                                {
                                        if (m_TicketPending > 0) RemoveOrderPendent(m_TicketPending);
                                        if (m_Position.Ticket > 0) ClosePosition(m_Position.Ticket);
                                        ClosePosition();
                                        Print("EA was kicked off the chart for making a serious mistake.");
                                }
                        }

Легче всего было решить это в деструкторе, но есть немного сложный момент, который можно увидеть ниже:

                void PendingToPosition(void)
                        {
                                ResetLastError();
                                if ((m_bAccountHedging) && (m_Position.Ticket > 0))
                                {
                                        if (m_InfosManager.IsOrderFinish)
                                        {
                                                if (PositionSelectByTicket(m_Position.Ticket)) ClosePosition(m_Position.Ticket);
                                                ClosePosition();
                                                if (PositionSelectByTicket(m_TicketPending)) C_Orders::ClosePosition(m_TicketPending); else RemoveOrderPendent(m_TicketPending);
                                                ZeroMemory(m_Position.Ticket);
                                                m_TicketPending = 0;
                                                ResetLastError();
                                        }else   SetUserError(ERR_Unknown);
                                }else m_Position.Ticket = (m_Position.Ticket == 0 ? m_TicketPending : m_Position.Ticket);
                                m_TicketPending = 0;
                                if (_LastError != ERR_SUCCESS) UpdatePosition(m_Position.Ticket);
                                CheckToleranceLevel();
                        }

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

Ниже показываем еще одно изменение, которое нам необходимо внести:

inline void EraseTicketPending(const ulong ticket)
                        {
                                if ((m_TicketPending == ticket) && (m_TicketPending > 0))
                                {
                                        if (PositionSelectByTicket(m_TicketPending)) C_Orders::ClosePosition(m_TicketPending); 
                                        else RemoveOrderPendent(m_TicketPending);
                                        m_TicketPending = 0;
                                }
                                ResetLastError();
                                m_TicketPending = (ticket == m_TicketPending ? 0 : m_TicketPending);
                        }

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

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


Последние штрихи перед заключительным этапом

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

                void UpdatePosition(const ulong ticket)
                        {
                                int ret;
                                double price;
                                
                                if ((ticket == 0) || (ticket != m_Position.Ticket) || (m_Position.Ticket == 0)) return;
                                if (PositionSelectByTicket(m_Position.Ticket))
                                {
                                        ret = SetInfoPositions();
                                        if (m_InfosManager.IsOrderFinish)
                                        {
                                                price = m_Position.PriceOpen + (FinanceToPoints(m_InfosManager.FinanceStop, m_Position.Leverage) * (m_Position.IsBuy ? -1 : 1));
                                                if (m_TicketPending > 0) if (OrderSelect(m_TicketPending))
                                                {
                                                        price = OrderGetDouble(ORDER_PRICE_OPEN);
                                                        C_Orders::RemoveOrderPendent(m_TicketPending);
                                                        EraseTicketPending(m_TicketPending);
                                                }
                                                if (m_Position.Ticket > 0)      m_TicketPending = C_Orders::CreateOrder(m_Position.IsBuy ? ORDER_TYPE_SELL : ORDER_TYPE_BUY, price, 0, 0, m_Position.Leverage, m_InfosManager.IsDayTrade);
                                        }
                                        m_StaticLeverage += (ret > 0 ? ret : 0);
                                }else
                                {
                                        ZeroMemory(m_Position);
                                        if ((m_InfosManager.IsOrderFinish) && (m_TicketPending > 0))
                                        {
                                                RemoveOrderPendent(m_TicketPending);
                                                m_TicketPending = 0;
                                        }
                                        if (m_InfosManager.IsOrderFinish) EraseTicketPending(m_TicketPending);
                                }
                                ResetLastError();
                        }

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

                ~C_Manager()
                        {
                                if (_LastError == (ERR_USER_ERROR_FIRST + ERR_Excommunicate))
                                {
                                        if (m_TicketPending > 0) RemoveOrderPendent(m_TicketPending);
                                        EraseTicketPending(m_TicketPending);
                                        ClosePosition();
                                        Print("EA was kicked off the chart for making a serious mistake.");
                                }
                        }

И, наконец, последний пункт, который также выигрывает от повторного использования кода:

                void PendingToPosition(void)
                        {
                                ResetLastError();
                                if ((m_bAccountHedging) && (m_Position.Ticket > 0))
                                {
                                        if (m_InfosManager.IsOrderFinish)
                                        {
                                                ClosePosition();
                                                EraseTicketPending(m_TicketPending);
                                                if (PositionSelectByTicket(m_TicketPending)) C_Orders::ClosePosition(m_TicketPending); else RemoveOrderPendent(m_TicketPending);
                                                m_TicketPending = 0;
                                                ResetLastError();
                                        }else   SetUserError(ERR_Unknown);
                                }else m_Position.Ticket = (m_Position.Ticket == 0 ? m_TicketPending : m_Position.Ticket);
                                m_TicketPending = 0;
                                if (_LastError != ERR_SUCCESS) UpdatePosition(m_Position.Ticket);
                                CheckToleranceLevel();
                        }

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

Это может показаться незначительным и банальным дефектом, и многие могут считать его малоопасным. В каком-то смысле я согласен с этой идеей, поскольку пятый ордер никогда не будет принят. Однако программирование советника указывало на объем в 10 раз больше, поэтому, когда был принят четвертый ордер, советник выполнил бы объем в 12 раз больше, превысив настроенный максимальный объем в 2 раза. Это произошло потому, что трейдер установил плечо в 3 раза больше Но если бы вы указали плечо, например, в 9 раз? В этом случае трейдер ожидает, что советник выполнит одну сделку.

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

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

Чтобы исправить это, мы просто добавим несколько строк кода, как показано ниже:

//+------------------------------------------------------------------+
                void CreateOrder(const ENUM_ORDER_TYPE type, const double Price)
                        {                               
                                if (!CtrlTimeIsPassed()) return;
                                if ((m_StaticLeverage >= m_InfosManager.MaxLeverage) || (m_TicketPending > 0) || (m_bAccountHedging && (m_Position.Ticket > 0))) return;
                                if (m_StaticLeverage + m_InfosManager.Leverage > m_InfosManager.MaxLeverage)
                                {
                                        Print("Request denied, as it would violate the maximum volume allowed for the EA.");
                                        return;
                                }
                                m_TicketPending = C_Orders::CreateOrder(type, Price, (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceStop), (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceTake), m_InfosManager.Leverage, m_InfosManager.IsDayTrade);
                        }
//+------------------------------------------------------------------+  
                void ToMarket(const ENUM_ORDER_TYPE type)
                        {
                                ulong tmp;
                                
                                if (!CtrlTimeIsPassed()) return;
                                if ((m_StaticLeverage >= m_InfosManager.MaxLeverage) || (m_bAccountHedging && (m_Position.Ticket > 0))) return;
                                if (m_StaticLeverage + m_InfosManager.Leverage > m_InfosManager.MaxLeverage)
                                {
                                        Print("Request denied, as it would violate the maximum volume allowed for the EA.");
                                        return;
                                }
                                tmp = C_Orders::ToMarket(type, (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceStop), (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceTake), m_InfosManager.Leverage, m_InfosManager.IsDayTrade);
                                m_Position.Ticket = (m_bAccountHedging ? tmp : (m_Position.Ticket > 0 ? m_Position.Ticket : tmp));
                        }
//+------------------------------------------------------------------+

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

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

inline bool IsPossible(const bool IsPending)
	{
		if (!CtrlTimeIsPassed()) return false;
		if ((m_StaticLeverage >= m_InfosManager.MaxLeverage) || (m_bAccountHedging && (m_Position.Ticket > 0))) return false;
		if ((IsPending) && (m_TicketPending > 0)) return false;
		if (m_StaticLeverage + m_InfosManager.Leverage > m_InfosManager.MaxLeverage)
		{
			Print("Request denied, as it would violate the maximum volume allowed for the EA.");
			return false;
		}
                                
		return true;
	}

Давайте разберемся, что у нас здесь. Все строки кода, которые повторялись в кодах отправки ордеров, теперь собраны в приведенном выше коде. Однако существует небольшая разница между отправкой отложенного ордера и рыночного ордера, и эта разница определяется этим конкретным пунктом. Мы должны проверить, исходит ли ордер от отложенного ордера или от рыночного ордера, чтобы их отличить друг от друга. Чтобы отличать эти два типа ордеров, мы используем аргумент, который позволяет объединить весь код в один. Если существует какая-либо невозможность отправить ордер, возвращается значение false. Если ордер может быть отправлен, возвращается значение true.

После этого новый код функции можно просмотреть ниже:

//+------------------------------------------------------------------+
                void CreateOrder(const ENUM_ORDER_TYPE type, const double Price)
                        {
                                if (!IsPossible(true)) return;
                                if (!CtrlTimeIsPassed()) return;
                                if ((m_StaticLeverage >= m_InfosManager.MaxLeverage) || (m_TicketPending > 0) || (m_bAccountHedging && (m_Position.Ticket > 0))) return;
                                if (m_StaticLeverage + m_InfosManager.Leverage > m_InfosManager.MaxLeverage)
                                {
                                        Print("Request denied, as it would violate the maximum volume allowed for the EA.");
                                        return;
                                }
                                m_TicketPending = C_Orders::CreateOrder(type, Price, (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceStop), (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceTake), m_InfosManager.Leverage, m_InfosManager.IsDayTrade);
                        }
//+------------------------------------------------------------------+  
                void ToMarket(const ENUM_ORDER_TYPE type)
                        {
                                ulong tmp;
                                
                                if (!IsPossible(false)) return;
                                if (!CtrlTimeIsPassed()) return;
                                if ((m_StaticLeverage >= m_InfosManager.MaxLeverage) || (m_bAccountHedging && (m_Position.Ticket > 0))) return;
                                if (m_StaticLeverage + m_InfosManager.Leverage > m_InfosManager.MaxLeverage)
                                {
                                        Print("Request denied, as it would violate the maximum volume allowed for the EA.");
                                        return;
                                }
                                tmp = C_Orders::ToMarket(type, (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceStop), (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceTake), m_InfosManager.Leverage, m_InfosManager.IsDayTrade);
                                m_Position.Ticket = (m_bAccountHedging ? tmp : (m_Position.Ticket > 0 ? m_Position.Ticket : tmp));
                        }
//+------------------------------------------------------------------+

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

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


Заключительные выводы

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

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


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

Прикрепленные файлы |
Как построить советник, работающий автоматически (Часть 12): Автоматизация (IV) Как построить советник, работающий автоматически (Часть 12): Автоматизация (IV)
Если вы думаете, что автоматизированные системы просты, то наверно вы еще не до конца поняли, что нужно для их создания. В данном материале мы поговорим о проблеме, с которой сталкиваются многие советники: неизбирательное исполнение ордеров, и возможное решение этой проблемы.
Как построить советник, работающий автоматически (Часть 10): Автоматизация (II) Как построить советник, работающий автоматически (Часть 10): Автоматизация (II)
Автоматизация ничего не значит, если вы не можете контролировать расписание его работы. Ни один работник не может быть эффективным при работе 24 часа в сутки. Несмотря на этот факт, многие считают, что автоматизированная система должна работать 24 часа в сутки. Хорошо всегда иметь возможность задавать временной интервал для эксперта. В этой статье мы обсудим, как правильно установить такой временной интервал.
Нейросети — это просто (Часть 39): Go-Explore — иной подход к исследованию Нейросети — это просто (Часть 39): Go-Explore — иной подход к исследованию
Продолжаем тему исследования окружающей среды в моделях обучения с подкреплением. И данной статье мы рассмотрим ещё один алгоритм Go-Explore, который позволяет эффективно исследовать окружающую среду на стадии обучения модели.
Эксперименты с нейросетями (Часть 6): Перцептрон как самодостаточное средство предсказания цены Эксперименты с нейросетями (Часть 6): Перцептрон как самодостаточное средство предсказания цены
Пример использования перцептрона как самодостаточного средства предсказания цены. В статье даются общие понятия, представлен простейший готовый советник и результаты его оптимизации.