Как построить советник, работающий автоматически (Часть 03): Новые функции

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

Введение

В предыдущей статье Как построить советник, работающий автоматически (Часть 02): Начинаем писать код мы начали разрабатывать систему ордеров, которой будем пользоваться в автоматическом советнике. Однако мы создали только одну из необходимых функций или процедур.

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

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

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

Раз мы уведомлены, мы можем добавить другие необходимые функции в автоматический советник, что будет покрывать более 90% случаев.


Почему нам необходимы новые процедуры?

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

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

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

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

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

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

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

Всё это объясняется выше только для того, чтобы показать, что существует более одного способа сделать одно и то же, в данном случае - это ЗАКРЫТЬ ПОЗИЦИЮ. Открытие позиции - это самая легкая часть процесса, потому что во время закрытия вам уже придется принимать во внимание следующее:

Все эти моменты необходимо учитывать и соблюдать при создании автоматического советника. Есть и другие моменты, как, например, тот факт, что некоторые программисты добавляют графики, чтобы советник мог реально работать. Что касается этого, то я скажу откровенно и честно. Это совершенно БЕСПОЛЕЗНО и является полной ГЛУПОСТЬЮ. Несмотря на то,что в другой статье показано, как это сделать, я не рекомендую так делать, и сейчас я объясню причину.

Подумайте о следующем: вы не знаете, как торговать на рынке, вы настраиваете советника, чтобы он делал это автоматически, вы устанавливаете для него расписание торговли. Отлично, теперь вы думаете: я могу пойти заняться чем-то другим, попробовать иные занятия... [звучат фанфары неудачи]... ПЛОХО. НИКОГДА, повторяю, НИКОГДА не оставляйте советник, даже автоматический, работать без присмотра. НИКОГДА. Пока он включен, вы или ваше доверенное лицо, должны находиться рядом с ним и наблюдать за его действиями.

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

Реализация необходимых функций 

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

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

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

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

                ulong CreateOrder(const ENUM_ORDER_TYPE type, const double Price, const double FinanceStop, const double FinanceTake, const uint Leverage, const bool IsDayTrade)
                        {
                                double  bid, ask, Desloc;                               
                                
                                Price = AdjustPrice(Price);
                                bid = SymbolInfoDouble(_Symbol, (m_Infos.PlotLast ? SYMBOL_LAST : SYMBOL_BID));
                                ask = (m_Infos.PlotLast ? bid : SymbolInfoDouble(_Symbol, SYMBOL_ASK));
                                CommonData(type, AdjustPrice(Price), FinanceStop, FinanceTake, Leverage, IsDayTrade);
                                m_TradeRequest.action   = TRADE_ACTION_PENDING;
                                m_TradeRequest.type     = (type == ORDER_TYPE_BUY ? (ask >= Price ? ORDER_TYPE_BUY_LIMIT : ORDER_TYPE_BUY_STOP) : 
                                                                                    (bid < Price ? ORDER_TYPE_SELL_LIMIT : ORDER_TYPE_SELL_STOP));                              
                                ZeroMemory(m_TradeRequest);
                                m_TradeRequest.action           = TRADE_ACTION_PENDING;
                                m_TradeRequest.symbol           = _Symbol;
                                m_TradeRequest.volume           = NormalizeDouble(m_Infos.VolMinimal + (m_Infos.VolStep * (Leverage - 1)), m_Infos.nDigits);
                                m_TradeRequest.type             = (type == ORDER_TYPE_BUY ? (ask >= Price ? ORDER_TYPE_BUY_LIMIT : ORDER_TYPE_BUY_STOP) : 
                                                                                            (bid < Price ? ORDER_TYPE_SELL_LIMIT : ORDER_TYPE_SELL_STOP));
                                m_TradeRequest.price            = NormalizeDouble(Price, m_Infos.nDigits);
                                Desloc = FinanceToPoints(FinanceStop, Leverage);
                                m_TradeRequest.sl               = NormalizeDouble(Desloc == 0 ? 0 : Price + (Desloc * (type == ORDER_TYPE_BUY ? -1 : 1)), m_Infos.nDigits);
                                Desloc = FinanceToPoints(FinanceTake, Leverage);
                                m_TradeRequest.tp               = NormalizeDouble(Desloc == 0 ? 0 : Price + (Desloc * (type == ORDER_TYPE_BUY ? 1 : -1)), m_Infos.nDigits);
                                m_TradeRequest.type_time        = (IsDayTrade ? ORDER_TIME_DAY : ORDER_TIME_GTC);
                                m_TradeRequest.type_filling     = ORDER_FILLING_RETURN;
                                m_TradeRequest.deviation        = 1000;
                                m_TradeRequest.comment          = "Order Generated by Experts Advisor.";
                                
                                return (((type == ORDER_TYPE_BUY) || (type == ORDER_TYPE_SELL)) ? ToServer() : 0);
                        };

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

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

                ulong ToMarket(const ENUM_ORDER_TYPE type, const double FinanceStop, const double FinanceTake, const uint Leverage, const bool IsDayTrade)
                        {
                                CommonData(type, SymbolInfoDouble(_Symbol, (type == ORDER_TYPE_BUY ? SYMBOL_ASK : SYMBOL_BID)), FinanceStop, FinanceTake, Leverage, IsDayTrade);
                                m_TradeRequest.action   = TRADE_ACTION_DEAL;
                                m_TradeRequest.type     = type;

                                return (((type == ORDER_TYPE_BUY) || (type == ORDER_TYPE_SELL)) ? ToServer() : 0);
                        };

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

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

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

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

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

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

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

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

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

Чтобы сделать объяснение более простым и понятным, мы будем разбивать всё на части, поэтому будьте внимательны, чтобы не потеряться в объяснении.

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

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

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

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

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

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

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

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

#property copyright "Daniel Jose"
#property description "This one is an automatic Expert Advisor"
#property description "for demonstration. To understand how to"
#property description "develop yours in order to use a particular"
#property description "operational, see the articles where there"
#property description "is an explanation of how to proceed."
#property version   "1.03"
#property link      "https://www.mql5.com/pt/articles/11226"
//+------------------------------------------------------------------+
#include <Generic Auto Trader\C_Orders.mqh>
//+------------------------------------------------------------------+
C_Orders *orders;
//+------------------------------------------------------------------+
input int       user01   = 1;           //Коэффициент плеча
input int       user02   = 100;         //Take Profit ( ФИНАНСОВЫЙ )
input int       user03   = 75;          //Stop Loss ( ФИНАНСОВЫЙ )
input bool      user04   = true;        //Day Trade ?
input double    user05   = 84.00;       //Входная цена...
//+------------------------------------------------------------------+
int OnInit()
{
        orders = new C_Orders(1234456789);
        
        return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
        delete orders;
}
//+------------------------------------------------------------------+
void OnTick()
{
}
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
#define KEY_UP                  38
#define KEY_DOWN                40
#define KEY_NUM_1               97
#define KEY_NUM_2               98
#define KEY_NUM_3               99
#define KEY_NUM_7               103
#define KEY_NUM_8               104
#define KEY_NUM_9               105

        static ulong sticket = 0;
        int key = (int)lparam;
        
        switch (id)
        {
                case CHARTEVENT_KEYDOWN:
                        switch (key)
                        {
                                case KEY_UP:
                                        if (sticket == 0)
                                                sticket = (*orders).CreateOrder(ORDER_TYPE_BUY, user05, user03, user02, user01, user04);
                                        break;
                                case KEY_DOWN:
                                        if (sticket == 0)
                                                sticket = (*orders).CreateOrder(ORDER_TYPE_SELL, user05, user03, user02, user01, user04);
                                        break;
                                case KEY_NUM_1:
                                case KEY_NUM_7:
                                        if (sticket > 0) ModifyStop(key == KEY_NUM_7, sticket);
                                        break;
                                case KEY_NUM_2:
                                case KEY_NUM_8:
                                        if (sticket > 0) ModifyPrice(key == KEY_NUM_8, sticket);
                                        break;
                                case KEY_NUM_3:
                                case KEY_NUM_9:
                                        if (sticket > 0) ModifyTake(key == KEY_NUM_9, sticket);
                                        break;
                        }
                        break;
        }
}
//+------------------------------------------------------------------+
void ModifyPrice(bool IsUp, const ulong ticket)
{
        double p, s, t;
        
        if (!OrderSelect(ticket)) return;
        p = OrderGetDouble(ORDER_PRICE_OPEN) + (SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) * (IsUp ? 1 : -1));
        s = OrderGetDouble(ORDER_SL);
        t = OrderGetDouble(ORDER_TP);
        (*orders).ModifyPricePoints(ticket, p, s, t);
}
//+------------------------------------------------------------------+
void ModifyTake(bool IsUp, const ulong ticket)
{
        double p, s, t;
        
        if (!OrderSelect(ticket)) return;
        p = OrderGetDouble(ORDER_PRICE_OPEN);
        s = OrderGetDouble(ORDER_SL);
        t = OrderGetDouble(ORDER_TP) + (SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) * (IsUp ? 1 : -1));
        (*orders).ModifyPricePoints(ticket, p, s, t);
}
//+------------------------------------------------------------------+
void ModifyStop(bool IsUp, const ulong ticket)
{
        double p, s, t;
        
        if (!OrderSelect(ticket)) return;
        p = OrderGetDouble(ORDER_PRICE_OPEN);
        s = OrderGetDouble(ORDER_SL) + (SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) * (IsUp ? 1 : -1));
        t = OrderGetDouble(ORDER_TP);
        (*orders).ModifyPricePoints(ticket, p, s, t);
}
//+------------------------------------------------------------------+

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

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

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

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



Демонстрация приведенного выше кода.

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

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

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

void ModifyPrice(bool IsUp, const ulong ticket)
{
        double p, s, t;
        
        if (!OrderSelect(ticket)) return;
        p = OrderGetDouble(ORDER_PRICE_OPEN) + (SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) * (IsUp ? 1 : -1));
        s = OrderGetDouble(ORDER_SL) + (SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) * (IsUp ? 1 : -1));
        t = OrderGetDouble(ORDER_TP) + (SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) * (IsUp ? 1 : -1));
        (*orders).ModifyPricePoints(ticket, p, s, t);
}

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

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

                bool ModifyPricePoints(const ulong ticket, const double Price, const double PriceStop, const double PriceTake)
                        {
                                ZeroMemory(m_TradeRequest);
                                m_TradeRequest.symbol   = _Symbol;
                                if (OrderSelect(ticket))
                                {
// ... Код для перемещения ордеров ...
                                }else if (PositionSelectByTicket(ticket))
                                {
                                        m_TradeRequest.action   = TRADE_ACTION_SLTP;
                                        m_TradeRequest.position = ticket;
                                        m_TradeRequest.tp       = NormalizeDouble(AdjustPrice(PriceTake), m_Infos.nDigits);
                                        m_TradeRequest.sl       = NormalizeDouble(AdjustPrice(PriceStop), m_Infos.nDigits);
                                }else return false;
                                ToServer();
                                
                                return (_LastError == ERR_SUCCESS);
                        };

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

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

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

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

                bool ModifyPricePoints(const ulong ticket, const double Price, const double PriceStop, const double PriceTake)
                        {
                                ZeroMemory(m_TradeRequest);
                                m_TradeRequest.symbol   = _Symbol;
                                if (OrderSelect(ticket))
                                {
                                        m_TradeRequest.action = (Price > 0 ? TRADE_ACTION_MODIFY : TRADE_ACTION_REMOVE);
                                        m_TradeRequest.order  = ticket;
                                        if (Price > 0)
                                        {
                                                m_TradeRequest.price      = NormalizeDouble(AdjustPrice(Price), m_Infos.nDigits);
                                                m_TradeRequest.sl         = NormalizeDouble(AdjustPrice(PriceStop), m_Infos.nDigits);
                                                m_TradeRequest.tp         = NormalizeDouble(AdjustPrice(PriceTake), m_Infos.nDigits);
                                                m_TradeRequest.type_time  = (ENUM_ORDER_TYPE_TIME)OrderGetInteger(ORDER_TYPE_TIME) ;
                                                m_TradeRequest.expiration = 0;
                                        }
                                }else if (PositionSelectByTicket(ticket))
                                {
// Часть кода для работы на позициях ...
                                }else return false;
                                ToServer();
                                
                                return (_LastError == ERR_SUCCESS);
                        };

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

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

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

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

                bool ClosePosition(const ulong ticket, const uint partial = 0)
                        {
                                double v1 = partial * m_Infos.VolMinimal, Vol;
                                bool IsBuy;
                                
                                if (!PositionSelectByTicket(ticket)) return false;
                                IsBuy = PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY;
                                Vol = PositionGetDouble(POSITION_VOLUME);
                                ZeroMemory(m_TradeRequest);
                                m_TradeRequest.action    = TRADE_ACTION_DEAL;
                                m_TradeRequest.type      = (IsBuy ? ORDER_TYPE_SELL : ORDER_TYPE_BUY);
                                m_TradeRequest.price     = SymbolInfoDouble(_Symbol, (IsBuy ? SYMBOL_BID : SYMBOL_ASK));
                                m_TradeRequest.position  = ticket;
                                m_TradeRequest.symbol    = _Symbol;
                                m_TradeRequest.volume    = ((v1 == 0) || (v1 > Vol) ? Vol : v1);
                                m_TradeRequest.deviation = 1000;
                                ToServer();
                                
                                return (_LastError == ERR_SUCCESS);
                        };

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

Будь спокоен, дорогой читатель, вы еще не всё поняли, вы выражаете свое предубеждение, когда видите название процедуры. Но давайте копнем немного глубже, проанализируем код и поймем, почему многие мечтают о чем-то большем. Обратите внимание, что здесь, в данной процедуре, есть некоторые вычисления, но зачем вам вычисления? Это делается для того, чтобы обеспечить возможность известных частичных выходов. Давайте разберемся, как это происходит на самом деле. Допустим, что у вас есть открытая позиция с объемом 300. Тогда, если минимальный торгуемый объем составляет 100, вы можете выйти с объемом 100, 200 или 300.

Но для этого вам придется сообщить значение, а оно по умолчанию равно нулю, т.е. оно сообщает функции, что позиция будет закрыта полностью, но это произойдет только тогда, когда вы сохраните его по умолчанию. Но здесь есть одна деталь: НЕТ, и еще раз НЕТ, вы должны сообщать значение объема, вы должны сообщить о делеверидже, который будет сделан, т.е. если у вас объем 300, а минимальный объем 100, это означает, что у вас плечо 3x; а чтобы сделать частичный, то в таком случае вы должны сообщить значение, которое может быть 1 и 2, если сообщается 0, 3 или значение большее, чем ваше кредитное плечо, позиция будет закрыта полностью, об этом сообщается в этой точке здесь.

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

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

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

Чтобы частично исправить советника, нам придется изменить код, отвечающий за обработку событий графика, новый код показан ниже:

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
#define KEY_UP                  38
#define KEY_DOWN                40
#define KEY_NUM_1               97
#define KEY_NUM_2               98
#define KEY_NUM_3               99
#define KEY_NUM_7               103
#define KEY_NUM_8               104
#define KEY_NUM_9               105

        static ulong sticket = 0;
        ulong ul0;
        int key = (int)lparam;
        
        switch (id)
        {
                case CHARTEVENT_KEYDOWN:
                        switch (key)
                        {
                                case KEY_UP:
                                        if (sticket == 0)
                                                sticket = (*orders).CreateOrder(ORDER_TYPE_BUY, user05, user03, user02, user01, user04);
                                        ul0 = (*orders).CreateOrder(ORDER_TYPE_BUY, user05, user03, user02, user01, user04);
                                        sticket = (ul0 > 0 ? ul0 : sticket);
                                        break;
                                case KEY_DOWN:
                                        if (sticket == 0)
                                                sticket = (*orders).CreateOrder(ORDER_TYPE_SELL, user05, user03, user02, user01, user04);
                                        ul0 = (*orders).CreateOrder(ORDER_TYPE_SELL, user05, user03, user02, user01, user04);
                                        sticket = (ul0 > 0 ? ul0 : sticket);
                                        break;
                                case KEY_NUM_1:
                                case KEY_NUM_7:
                                        if (sticket > 0) ModifyStop(key == KEY_NUM_7, sticket);
                                        break;
                                case KEY_NUM_2:
                                case KEY_NUM_8:
                                        if (sticket > 0) ModifyPrice(key == KEY_NUM_8, sticket);
                                        break;
                                case KEY_NUM_3:
                                case KEY_NUM_9:
                                        if (sticket > 0) ModifyTake(key == KEY_NUM_9, sticket);
                                        break;
                        }
                        break;
        }
}

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

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

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

void ModifyTake(bool IsUp, const ulong ticket)
{
        double p = 0, s, t;
        
        if (!OrderSelect(ticket)) return;
        if (OrderSelect(ticket))
        {
                p = OrderGetDouble(ORDER_PRICE_OPEN);
                s = OrderGetDouble(ORDER_SL);
                t = OrderGetDouble(ORDER_TP) + (SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) * (IsUp ? 1 : -1));
        }else if (PositionSelectByTicket(ticket))
        {
                s = PositionGetDouble(POSITION_SL);
                t = PositionGetDouble(POSITION_TP) + (SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) * (IsUp ? 1 : -1));
        }else return;
        (*orders).ModifyPricePoints(ticket, p, s, t);
}

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

void ModifyStop(bool IsUp, const ulong ticket)
{
        double p = 0, s, t;
        
        if (!OrderSelect(ticket)) return;
        if (OrderSelect(ticket))
        {
                p = OrderGetDouble(ORDER_PRICE_OPEN);
                s = OrderGetDouble(ORDER_SL) + (SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) * (IsUp ? 1 : -1));
                t = OrderGetDouble(ORDER_TP);
        }else if (PositionSelectByTicket(ticket))
        {
                s = PositionGetDouble(POSITION_SL) + (SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) * (IsUp ? 1 : -1));
                t = PositionGetDouble(POSITION_TP);
        }else return;
        (*orders).ModifyPricePoints(ticket, p, s, t);
}

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


Заключение

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

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