Пишем кроссплатформенный помощник для выставления StopLoss и TakeProfit в соответствии со своими рисками

Roman Klymenko | 21 июня, 2019

Введение

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

Какое будет N, вы выбираете сами. Но чтобы соблюсти это правило, следует правильно высчитать лот, которым нужно войти в сделку.

Те, кто в своих мастер-классах советуют так делать, обычно показывают Excel-файл, в котором у них уже забиты нужные формулы расчета лота для каждого символа. А им остается «просто» ввести размер своего стоп-лосса, чтобы получить нужный объем лота.

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

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

Постановка целей

Итак, с первой задачей мы определились.

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

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

С точки зрения подсчета статистики лучше всего всегда использовать одинаковое соотношение размера стоп-лосса к тейк-профиту. Например, всегда входить в сделки с соотношением тейк-профита к стоп-лоссу 3 к 1, 4 к 1 и т. д. Какое именно соотношение выбрать, вы решаете самостоятельно, исходя из показателей своей торговли.

Но остается вопрос, как же нам выставлять уровень тейка, не тратя на это драгоценные минуты своего времени?  Опять пытаться использовать Excel? Или надеяться, что метод «на глаз» вполне подойдет?

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

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

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

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

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

Например, вот так выглядит окно графика с диалоговым окном при ширине в 640 пикселей:

Версия советника с диалоговым окном

Рис.1.Версия советника с диалоговым окном

Как можно заметить, окно даже полностью не помещается на экране.

С целью решения данной проблемы со временем появилось 2 версии данного советника.

В первой данная проблема была решена сокрытием окна настроек по умолчанию, и его выводом при нажатии кнопки Настройки. Этот советник до сих пор можно приобрести в Маркете для MetaTrader 5.

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

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

Версия советника без диалогового окна

Рис.2. Версия советника без диалогового окна

Входящие параметры

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

Входящие параметры советника

Рис.3. Входящие параметры советника

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

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

Параметр "Размер стоп-лосса в $ или %" как раз и определяет ту сумму, которую вы готовы потерять при наступлении стоп лосса.

Размер стоп-лосса в центах (клавиши 7 и 8). Также к заданию стоп-лосса можно отнести еще один параметр — "Размер стоп лосса в центах (клавиши 7 и 8)".

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

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

Не входить в сделку, если при мин. лоте риск больше заданного. Поскольку размер лота, с которым нужно войти в сделку, высчитывается автоматически исходя из параметра " Размер стоп-лосса в $ или %", может возникнуть такая ситуация, что даже при минимально возможном у вашего брокера лоте риск на сделку будет больше заданного вами.

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

По умолчанию советник не даст вам войти в сделку, если риск будет превышен.

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

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

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

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

Остальные параметры. Также вы можете:

Функция открытия позиции

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

Что это такое,  я уже не раз рассказывал в своих статьях. Например, в статье Создаем кроссплатформенный советник-сеточник (гридер).

Если вкратце, то код условной компиляции выглядит так:

#ifdef __MQL5__ 
   //код на MQL5
#else 
   //код на MQL4
#endif 

На протяжении всей статьи возможностями условной компиляции мы воспользуемся всего 3 раза, два из которых относятся к функции открытия позиции. Весь остальной код будет одинаково работать и в MetaTrader 4, и в MetaTrader 5.

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

// возможные типы ордеров для функции открытия позиций
enum TypeOfPos
  {
   MY_BUY,
   MY_SELL,
   MY_BUYSTOP,
   MY_BUYLIMIT,
   MY_SELLSTOP,
   MY_SELLLIMIT,
   MY_BUYSLTP,
   MY_SELLSLTP,
  }; 

//Выбор типа исполнения сделки для MT5
#ifdef __MQL5__ 
   enum TypeOfFilling //Тип исполнения сделки
     {
      FOK,//ORDER_FILLING_FOK
      RETURN,// ORDER_FILLING_RETURN
      IOC,//ORDER_FILLING_IOC
     }; 
   input TypeOfFilling  useORDER_FILLING_RETURN=FOK; //Режим исполнения ордера
#endif 


/*
функция открытия позиции или выставления лимитного ордера
*/
bool pdxSendOrder(TypeOfPos mytype, double price, double sl, double tp, double volume, ulong position=0, string comment="", string sym="", datetime expiration=0){
      if( !StringLen(sym) ){
         sym=_Symbol;
      }
      int curDigits=(int) SymbolInfoInteger(sym, SYMBOL_DIGITS);
      if(sl>0){
         sl=NormalizeDouble(sl,curDigits);
      }
      if(tp>0){
         tp=NormalizeDouble(tp,curDigits);
      }
      if(price>0){
         price=NormalizeDouble(price,curDigits);
      }else{
         #ifdef __MQL5__ 
         #else
            MqlTick latest_price;
            SymbolInfoTick(sym,latest_price);
            if( mytype == MY_SELL ){
               price=latest_price.ask;
            }else if( mytype == MY_BUY ){
               price=latest_price.bid;
            }
         #endif 
      }
   #ifdef __MQL5__ 
      ENUM_TRADE_REQUEST_ACTIONS action=TRADE_ACTION_DEAL;
      ENUM_ORDER_TYPE type=ORDER_TYPE_BUY;
      switch(mytype){
         case MY_BUY:
            action=TRADE_ACTION_DEAL;
            type=ORDER_TYPE_BUY;
            break;
         case MY_BUYSLTP:
            action=TRADE_ACTION_SLTP;
            type=ORDER_TYPE_BUY;
            break;
         case MY_BUYSTOP:
            action=TRADE_ACTION_PENDING;
            type=ORDER_TYPE_BUY_STOP;
            break;
         case MY_BUYLIMIT:
            action=TRADE_ACTION_PENDING;
            type=ORDER_TYPE_BUY_LIMIT;
            break;
         case MY_SELL:
            action=TRADE_ACTION_DEAL;
            type=ORDER_TYPE_SELL;
            break;
         case MY_SELLSLTP:
            action=TRADE_ACTION_SLTP;
            type=ORDER_TYPE_SELL;
            break;
         case MY_SELLSTOP:
            action=TRADE_ACTION_PENDING;
            type=ORDER_TYPE_SELL_STOP;
            break;
         case MY_SELLLIMIT:
            action=TRADE_ACTION_PENDING;
            type=ORDER_TYPE_SELL_LIMIT;
            break;
      }
      
      MqlTradeRequest mrequest;
      MqlTradeResult mresult;
      ZeroMemory(mrequest);
      
      mrequest.action = action;
      mrequest.sl = sl;
      mrequest.tp = tp;
      mrequest.symbol = sym;
      if(expiration>0){
         mrequest.type_time = ORDER_TIME_SPECIFIED_DAY;
         mrequest.expiration = expiration;
      }
      if(position>0){
         mrequest.position = position;
      }
      if(StringLen(comment)){
         mrequest.comment=comment;
      }
      if(action!=TRADE_ACTION_SLTP){
         if(price>0){
            mrequest.price = price;
         }
         if(volume>0){
            mrequest.volume = volume;
         }
         mrequest.type = type;
         mrequest.magic = EA_Magic;
         switch(useORDER_FILLING_RETURN){
            case FOK:
               mrequest.type_filling = ORDER_FILLING_FOK;
               break;
            case RETURN:
               mrequest.type_filling = ORDER_FILLING_RETURN;
               break;
            case IOC:
               mrequest.type_filling = ORDER_FILLING_IOC;
               break;
         }
         mrequest.deviation=100;
      }
      if(OrderSend(mrequest,mresult)){
         if(mresult.retcode==10009 || mresult.retcode==10008){
            if(action!=TRADE_ACTION_SLTP){
               switch(type){
                  case ORDER_TYPE_BUY:
//                     Alert("Order Buy #:",mresult.order," sl",sl," tp",tp," p",price," !!");
                     break;
                  case ORDER_TYPE_SELL:
//                     Alert("Order Sell #:",mresult.order," sl",sl," tp",tp," p",price," !!");
                     break;
               }
            }else{
//               Alert("Order Modify SL #:",mresult.order," sl",sl," tp",tp," !!");
            }
            return true;
         }else{
            msgErr(GetLastError(), mresult.retcode);
         }
      }
   #else 
      int type=OP_BUY;
      switch(mytype){
         case MY_BUY:
            type=OP_BUY;
            break;
         case MY_BUYSTOP:
            type=OP_BUYSTOP;
            break;
         case MY_BUYLIMIT:
            type=OP_BUYLIMIT;
            break;
         case MY_SELL:
            type=OP_SELL;
            break;
         case MY_SELLSTOP:
            type=OP_SELLSTOP;
            break;
         case MY_SELLLIMIT:
            type=OP_SELLLIMIT;
            break;
      }
      
      if(OrderSend(sym, type, volume, price, 100, sl, tp, comment, EA_Magic, expiration)<0){
            msgErr(GetLastError());
      }else{
         switch(type){
            case OP_BUY:
               Alert("Order Buy sl",sl," tp",tp," p",price," !!");
               break;
            case OP_SELL:
               Alert("Order Sell sl",sl," tp",tp," p",price," !!");
               break;
            }
            return true;
      }
   
   #endif 
   return false;
}

В MetaTrader 5 при открытии позиции необходимо выбрать тип ее исполнения. Поэтому для MetaTrader 5 мы добавляем еще один входящий параметр: " Режим исполнения ордера".

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

Локализация советника

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

Программируем интерфейс советника

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

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

И начнем мы с интерфейса нашего советника.

При запуске советника будут созданы следующие элементы интерфейса:

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

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

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

Если же вы хотите войти в рынок лимитным ордером, тогда дополнительно нужно нажать на кнопку "Показать линию цены открытия (0)", и переместить появившуюся зеленую линию на цену, по которой вы хотите открыть лимитный ордер. Направление и тип лимитного ордера советник определит автоматически на основе расположения линии стоп лосса и линии тейк-профита (стоп лосс выше цены открытия или ниже).

В принципе, кнопку "Показать линию цены открытия (0)" можно и не нажимать. После того, как вы переместите красную линию стоп-лосса на нужную цену, данная кнопка будет нажата автоматически, и появится зеленая линия цены открытия. Если переместить её, то будет открыт лимитный ордер. Если же не трогать, то откроется позиция по рынку.

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

Работа с комментарием к графику. Для работы с комментарием к графику используется стандартная функция Comment. Поэтому нам осталось только подготовить ту строку, которую функция Comment выведет на график. Для этого мы создадим собственную функцию getmespread:

/*
   Выводим информацию о спреде и времени закрытия сессии в комментарий к графику
*/
void getmespread(){
   string msg="";
   
   //получаем спред в валюте инструмента
   curSpread=lastme.ask-lastme.bid;
   
   // если рынок не закрыт, выводим информацию о спреде
   if( !isClosed ){
      if(curSpread>0){
         StringAdd(msg, langs.Label1_spread+": "+(string) DoubleToString(curSpread, (int) SymbolInfoInteger(_Symbol, SYMBOL_DIGITS))+" "+currencyS+" ("+DoubleToString(curSpread/curPoint, 0)+langs.lbl_point+")");
         StringAdd(msg, "; "+DoubleToString(((curSpread)/lastme.bid)*100, 3)+"%");
      }else{
         StringAdd(msg, langs.Label1_spread+": "+langs.lblNo);
      }
      StringAdd(msg, "; ");
   }
   
   // выводим время закрытия рынка, если ранее мы смогли его определить
   if(StringLen(time_info)){
      StringAdd(msg, "   "+time_info);
   }
      
   Comment(msg);
}

Вызывать функцию getmespread будем как при инициализации советника (OnInit), так и при каждом новом тике (OnTick).

В функции getmespread мы используем пять глобальных переменных нашего советника: lastme, isClosed, time_info, currencyS, curPoint.

В переменной lastme хранится информация об Ask и Bid последней полученной цены. Ее содержимое обновляется в функциях OnInit и OnTick командой:

SymbolInfoTick(_Symbol,lastme);

Остальные переменные инициализируются в функции OnInit. isClosed и time_info инициализируются следующим образом:

  isClosed=false;
  // получаем текущую дату
  TimeToStruct(TimeCurrent(), curDay);
  // получаем время торгов по инструменту на сегодня
  if(SymbolInfoSessionTrade(_Symbol, (ENUM_DAY_OF_WEEK) curDay.day_of_week, 0, dfrom, dto)){
      time_info="";
      TimeToStruct(dto, curEndTime);
      TimeToStruct(dfrom, curStartTime);
         
         isEndTime=true;
         string tmpmsg="";
         tmp_val=curEndTime.hour;
         if(tmp_val<10){
            StringAdd(tmpmsg, "0");
         }
         StringAdd(tmpmsg, (string) tmp_val+":");
         tmp_val=curEndTime.min;
         if(tmp_val<10){
            StringAdd(tmpmsg, "0");
         }
         StringAdd(tmpmsg, (string) tmp_val);
         if(curEndTime.hour==curDay.hour){
            if(tmp_val>curDay.min){
            }else{
               isClosed=true;
            }
         }else{
            if(curEndTime.hour==0){
            }else{
               if( curEndTime.hour>1 && (curDay.hour>curEndTime.hour || curDay.hour==0)){
                  StringAdd(time_info, " ("+langs.lbl_close+")");
                  isClosed=true;
               }else if(curDay.hour<curStartTime.hour ){
                  StringAdd(time_info, " ("+langs.lbl_close+")");
                  isEndTime=false;
                  isClosed=true;
               }else if(curDay.hour==curStartTime.hour && curDay.min<curStartTime.min ){
                  StringAdd(time_info, " ("+langs.lbl_close+")");
                  isEndTime=false;
                  isClosed=true;
               }
            }
         }

         if(isEndTime){
            StringAdd(time_info, langs.lblshow_TIME+": "+tmpmsg+time_info);
         }else{
            StringAdd(time_info, langs.lblshow_TIME2+": "+tmpmsg+time_info);
         }
  }

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

currencyS=SymbolInfoString(_Symbol, SYMBOL_CURRENCY_PROFIT);

Размер пункта по инструменту мы будем хранить в переменной curPoint:

curPoint=SymbolInfoDouble(_Symbol, SYMBOL_POINT);

Линия стоп-лосса. Непосредственно при запуске советника мы видим только одну линию — красную линию выставления стоп лосса.

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

  // если на графике есть линии стоп лосса и цены открытия, тогда
  // помещаем в переменные цены, на которых они находятся
  if(ObjectFind(0, exprefix+"_stop")>=0){
      draw_stop=ObjectGetDouble(0, exprefix+"_stop", OBJPROP_PRICE);
      if(ObjectFind(0, exprefix+"_open")>=0){
         draw_open=ObjectGetDouble(0, exprefix+"_open", OBJPROP_PRICE);
      }
  // иначе создаем весь графический интерфейс советника
  }else{
      draw_open=lastme.bid;
      draw_stop=draw_open-(SymbolInfoInteger(_Symbol, SYMBOL_SPREAD)*curPoint);
      ObjectCreate(0, exprefix+"_stop", OBJ_HLINE, 0, 0, draw_stop);
      ObjectSetInteger(0,exprefix+"_stop",OBJPROP_SELECTABLE,1);
      ObjectSetInteger(0,exprefix+"_stop",OBJPROP_SELECTED,1); 
      ObjectSetInteger(0,exprefix+"_stop",OBJPROP_STYLE,STYLE_DASHDOTDOT); 
      ObjectSetInteger(0,exprefix+"_stop",OBJPROP_ANCHOR,ANCHOR_TOP);

      // другие элементы интерфейса
  }

Может ли вообще возникнуть такая ситуация, когда на графике уже есть элементы интерфейса запускаемого советника?

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

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

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

void OnChartEvent(const int id,         // event ID   
                  const long& lparam,   // event parameter of the long type 
                  const double& dparam, // event parameter of the double type 
                  const string& sparam) // event parameter of the string type 
  { 
   switch(id){
      case CHARTEVENT_OBJECT_DRAG:
         if(sparam==exprefix+"_stop"){
            setstopbyline();
            showOpenLine();
            ObjectSetInteger(0,exprefix+"_openbtn",OBJPROP_STATE, true);
         }
         break;
   }
}

После перемещения красной линии будет запущена функция setstopbyline, которая "запоминает" уровень стоп лосса для будущего ордера:

/*
"запоминает" уровень стоп лосса для будущего ордера
*/
void setstopbyline(){
   // получаем цену, на которой находится линия стоп лосса
   double curprice=ObjectGetDouble(0, exprefix+"_stop", OBJPROP_PRICE);
   // если цена отличается от той, на которую линия стоп лосса была помещена при инициализации советника, то
   if(  curprice>0 && curprice != draw_stop ){
      double tmp_double=SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
      if( tmp_double>0 && tmp_double!=1 ){
         if(tmp_double<1){
            resval=DoubleToString(curprice/tmp_double, 8);
            if( StringFind(resval, ".00000000")>0 ){}else{
               curprice=MathFloor(curprice)+MathFloor((curprice-MathFloor(curprice))/tmp_double)*tmp_double;
            }
         }else{
            if( MathMod(curprice,tmp_double) ){
               curprice= MathFloor(curprice/tmp_double)*tmp_double;
            }
         }
      }
      draw_stop=STOPLOSS_PRICE=curprice;
                  
      updatebuttontext();
      ChartRedraw(0);
   }
}

Помимо функции setstopbyline перемещение красной линии приводит к отображению на графике линии цены открытия (функция showOpenLine) и изменению состояния кнопки " Показать линию цены открытия (0)".

Кнопка и линия цены открытия. Кнопка "Показать линию цены открытия (0)" также создается при инициализации советника:

      if(ObjectFind(0, exprefix+"_openbtn")<0){
         ObjectCreate(0, exprefix+"_openbtn", OBJ_BUTTON, 0, 0, 0);
         ObjectSetInteger(0,exprefix+"_openbtn",OBJPROP_XDISTANCE,0); 
         ObjectSetInteger(0,exprefix+"_openbtn",OBJPROP_YDISTANCE,33); 
         ObjectSetString(0,exprefix+"_openbtn",OBJPROP_TEXT, langs.btnShowOpenLine); 
         ObjectSetInteger(0,exprefix+"_openbtn",OBJPROP_XSIZE,333); 
         ObjectSetInteger(0,exprefix+"_openbtn",OBJPROP_FONTSIZE, 8);
         ObjectSetInteger(0,exprefix+"_openbtn",OBJPROP_YSIZE,25); 
      }

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

      case CHARTEVENT_OBJECT_CLICK:
         if (sparam==exprefix+"_openbtn"){
            updateOpenLine();
         }
         break;

Функция updateOpenLine, которая вызывается при клике на кнопку "Показать линию цены открытия (0)", является небольшой оберткой для вызова основной функции showOpenLine. Та, в свою очередь, просто выводит на график цену открытия:

void showOpenLine(){
   if(ObjectFind(0, exprefix+"_open")<0){
      draw_open=lastme.bid;
      ObjectCreate(0, exprefix+"_open", OBJ_HLINE, 0, 0, draw_open);
      ObjectSetInteger(0,exprefix+"_open",OBJPROP_SELECTABLE,1);
      ObjectSetInteger(0,exprefix+"_open",OBJPROP_SELECTED,1); 
      ObjectSetInteger(0,exprefix+"_open",OBJPROP_STYLE,STYLE_DASHDOTDOT); 
      ObjectSetInteger(0,exprefix+"_open",OBJPROP_ANCHOR,ANCHOR_TOP); 
      ObjectSetInteger(0,exprefix+"_open",OBJPROP_COLOR,clrGreen);
   }
}

Нам остается только переписать обработчик события CHARTEVENT_OBJECT_DRAG, чтобы он реагировал на перемещение как линии стоп-лосса, так и линии цены открытия:

      case CHARTEVENT_OBJECT_DRAG:
         if(sparam==exprefix+"_stop"){
            setstopbyline();
            showOpenLine();
            ObjectSetInteger(0,exprefix+"_openbtn",OBJPROP_STATE, true);
         }else if(sparam==exprefix+"_open"){
               curprice=ObjectGetDouble(0, exprefix+"_open", OBJPROP_PRICE);
               if( curprice>0 && curprice != draw_open ){
                  double tmp_double=SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
                  if( tmp_double>0 && tmp_double!=1 ){
                     if(tmp_double<1){
                        resval=DoubleToString(curprice/tmp_double, 8);
                        if( StringFind(resval, ".00000000")>0 ){}else{
                           curprice=MathFloor(curprice)+MathFloor((curprice-MathFloor(curprice))/tmp_double)*tmp_double;
                        }
                     }else{
                        if( MathMod(curprice,tmp_double) ){
                           curprice= MathFloor(curprice/tmp_double)*tmp_double;
                        }
                     }
                  }
                  draw_open=open=OPEN_PRICE=curprice;
                  
                  updatebuttontext();
                  ObjectSetString(0,exprefix+"Edit3",OBJPROP_TEXT,0, (string) NormalizeDouble(draw_open, _Digits));
                  ChartRedraw(0);
               }
         }
         break;

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

Линии стоп лосса, цены открытия и цены тейк-профита

Рис.4. Линии стоп лосса, цены открытия и цены тейк-профита

Кнопка открытия позиции. Кнопка открытия позиции выводится точно таким же образом, как и кнопка "Показать линию цены открытия (0)".

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

      case CHARTEVENT_OBJECT_CLICK:
         if (sparam==exprefix+"_send"){
            startPosition();
         }else if (sparam==exprefix+"_openbtn"){
            updateOpenLine();
         }
         break;

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

Чтобы выполнить какие-либо команды при завершении работы советника, достаточно написать их внутри стандартной функции OnDeinit:

void OnDeinit(const int reason)
  {

     if(reason!=REASON_CHARTCHANGE){
        ObjectsDeleteAll(0, exprefix);
        Comment("");
     }
      
  }

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

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

Реализуем клавиши быстрого управления советником

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

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

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

void OnChartEvent(const int id,         // event ID   
                  const long& lparam,   // event parameter of the long type 
                  const double& dparam, // event parameter of the double type 
                  const string& sparam) // event parameter of the string type 
  { 
   string text="";
   double curprice=0;
   switch(id){
      case CHARTEVENT_OBJECT_CLICK:
         // нажатие кнопок на графике
         break;
      case CHARTEVENT_OBJECT_DRAG:
         // перемещение линий
         break;
      case CHARTEVENT_KEYDOWN:
         switch((int) sparam){
            // завершить работу советника без выставления ордера
            case 45: //x
               closeNotSave();
               break;
            // выставить ордер и завершить работу советника
            case 31: //s
               startPosition();
               break;
            // установить стоп лосс минимально возможным для открытия Long позиции
            case 22: //u
               setMinStopBuy();
               break;
            // установить стоп лосс минимально возможным для открытия Sell позиции
            case 38: //l
               setMinStopSell();
               break;
            // отменить заданную цену открытия
            case 44: //z
               setZero();
               ChartRedraw();
               break;
            // установить стоп лосс в 0.2% от текущей цены для открытия Long позиции
            case 3: //2
               set02StopBuy();
               break;
            // установить стоп лосс в 0.2% от текущей цены для открытия Short позиции
            case 4: //3
               set02StopSell();
               break;
            // установить стоп лосс в 7 центов от текущей цены (параметр CENT_STOP)
            // для открытия Long позиции
            case 8: //7
               set7StopBuy();
               break;
            // установить стоп лосс в 7 центов от текущей цены (параметр CENT_STOP)
            // для открытия Short позиции
            case 9: //8
               set7StopSell();
               break;
         }
         break;
   }
}

Таким образом, если вы задаете фиксированный стоп, равный минимально возможному, 0.2% от цены или в центах от цены, тогда вам даже мышкой не нужно пользоваться. Запустили советник, нажали клавишу " 2", чтобы установить стоп-лосс, равным 0.2% от цены в Long, нажали клавишу "S", и позиция открылась.

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

Назначение горячих клавиш советникам

Рис.5. Назначение горячих клавиш советникам

Подсчет нужного объема сделки

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

Самым интересным из перечисленного является механизм подсчета объема сделки.

Сначала нам нужно подсчитать, сколько мы потеряем в случае стоп-лосса на минимально возможных объемах. Реализация данного функционала в MQL5 отличается от MQL4.

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

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

В итоге для подсчета убытков получилась такая функция:

double getMyProfit(double fPrice, double fSL, double fLot, bool forLong=true){
   double fProfit=0;
   
   fPrice=NormalizeDouble(fPrice,_Digits);
   fSL=NormalizeDouble(fSL,_Digits);
   #ifdef __MQL5__ 
      if( forLong ){
         if(OrderCalcProfit(ORDER_TYPE_BUY, _Symbol, fLot, fPrice, fSL, fProfit)){};
      }else{
         if(OrderCalcProfit(ORDER_TYPE_SELL, _Symbol, fLot, fPrice, fSL, fProfit)){};
      }
   #else
      if( forLong ){
         fProfit=(fPrice-fSL)*fLot* (1 / MarketInfo(_Symbol, MODE_POINT)) * MarketInfo(_Symbol, MODE_TICKVALUE);
      }else{
         fProfit=(fSL-fPrice)*fLot* (1 / MarketInfo(_Symbol, MODE_POINT)) * MarketInfo(_Symbol, MODE_TICKVALUE);
      }
   #endif 
   if( fProfit!=0 ){
      fProfit=MathAbs(fProfit);
   }
   
   return fProfit;
}

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

      profit=getMyProfit(open, STOPLOSS_PRICE, lot);
      if( profit!=0 ){
         // если на минимальном объеме размер убытков меньше ваших рисков,
         // тогда подсчитываем нужный объем сделки
         if( profit<stopin_value ){
            // получаем нужный объем сделки
            lot*=(stopin_value/profit);
            // корректируем объем, если он не соответствует минимально возможному шагу
            // по инструменту
            if( SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP)==0.01 ){
               lot=(floor(lot*100))/100;
            }else if( SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP)==0.1 ){
               lot=(floor(lot*10))/10;
            }else{
               lot=floor(lot);
            }
         // если на минимальном объеме размер убытков больше ваших рисков,
         // тогда отменяем открытие позиции, если это указано в настройках советника
         }else if( profit>stopin_value && EXIT_IF_MORE ){
            Alert(langs.wrnEXIT_IF_MORE1+": "+(string) lot+" "+langs.wrnEXIT_IF_MORE2+": "+(string) profit+" "+AccountInfoString(ACCOUNT_CURRENCY)+" ("+(string) stopin_value+" "+AccountInfoString(ACCOUNT_CURRENCY)+")!");
            return;
         }
      }

Ограничения на вход

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

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

   if(SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN)==0){
      Alert(langs.wrnMinVolume);
      ExpertRemove();
   }

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

   if(SymbolInfoInteger(_Symbol, SYMBOL_TRADE_MODE)==SYMBOL_TRADE_MODE_DISABLED || SymbolInfoInteger(_Symbol, SYMBOL_TRADE_MODE)==SYMBOL_TRADE_MODE_CLOSEONLY ){
      Alert(langs.wrnOnlyClose);
      ExpertRemove();
   }

Непосредственно при открытии позиции выполняется проверка корректности заданной цены открытия и стоп-лосса. Например, если минимальный шаг цены 0.25, а у вас стоп-лосс установлен на цену 23.29, то брокер просто не примет ваш ордер. Вообще, в подобных случаях советник автоматически приводит цену к правильному значению (и цена стоп лосса будет не 23.29, а 23.25 или 23.5). И вы просто не сможете задать «неправильную» цену. Но на всякий случай выполняется и дополнительная проверка:

   if( SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE)>0 && SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE)!=1 ){
      resval=DoubleToString(STOPLOSS_PRICE/SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE), 8);
      if( StringFind(resval, ".00000000")>0 ){}else{
         Alert(langs.wrnSYMBOL_TRADE_TICK_SIZE+" "+(string) SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE)+"! "+langs.wrnSYMBOL_TRADE_TICK_SIZE_end);
         return;
      }
   }

Заключение

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

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

Любые доработки советника разрешены и приветствуются.

Если же программирование — это не ваше. Но вам очень нужно реализовать какой-то функционал, которого нет в рамках данного советника, пишите в личку. Однако скорее всего это будет не бесплатно =)

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

Возможно, среди данного функционала будет то, что вам нужно.