English 中文 Español Deutsch 日本語 Português 한국어 Français Italiano Türkçe
Создание интерактивного советника для полуавтоматической торговли с заданным риском

Создание интерактивного советника для полуавтоматической торговли с заданным риском

MetaTrader 5Примеры | 4 ноября 2010, 15:49
5 195 2
investeo
investeo

Введение

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

При заданном уровне рискового капитала мне требовалось вычислять текущие параметры уровней Stop Loss а также торговать прямо из советника, автоматически задавая вычисленные уровни Stop Loss и Take Profit.

В данной статье мы представим способ реализации интерактивного советника для полуавтоматической торговли с заданным уровнем соотношения прибыль/риск (Reward/Risk ratio). Величина рискового капитала, отношение прибыль/риск и размер лота могут быть изменены в процессе работы.


1. Технические требования

Технические требования к функциональности советника:
  • возможность установки уровня риска при загрузке и его изменения в процессе работы с пересчетом размера позиции
  • возможность установки отношения прибыль/риск и его изменения в процессе работы
  • возможность вычисления максимального размера лота при заданном риске и уровне Stop Loss.
  • возможность изменения размера лота и динамический пересчет значений рискового капитала и прибыли
  • возможность выполнения торговых операций прямо из советника
  • интерфейс "drag and drop" для установки Stop Loss и вывода ценовых уровней при заданном отношении прибыль/риск.

2. Элементы интерфейса

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

Сначала я собирался использовать объект для объединения объектов на панели, но потом я решил пойти другим путем. Я спроектировал класс, который содержит метки, поля ввода и кнопки, показывая их на фоне картинки. Фоновое изображение было создано при помощи редактора GIMP. Поля ввода, обновляемые красные метки и кнопки созданы при помощи MQL5 (рис 1.)

Я просто разместил объекты на графике, запомнил их положение и создал класс CRRDialog, который содержит все функции показа вычисленных значений, получения параметров с полей CChartObjectEdit и записи состояний кнопок. Цветные прямоугольники для полей прибыли и риска являются объектами класса CChartObjectRectangle, а перемещаемый по графику указатель Stop Loss является объектом класса CChartObjectBitmap.

Рисунок 1. Графический интерфейс советника

Рисунок 1. Графический интерфейс советника


3. Реализация класса CRRDialog 

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

Для фона я использую класс CChartObjectBmpLabel, для полей ввода CChartObjectEdit, для показа текста CChartObjectLabel, для работы с кнопками используются объекты класса CChartObjectButtons.

class CRRDialog
  {
private:

   int               m_baseX;
   int               m_baseY;
   int               m_fontSize;
   
   string            m_font;
   string            m_dialogName;
   string            m_bgFileName;

   double            m_RRRatio;
   double            m_riskPercent;
   double            m_orderLots;
   double            m_SL;
   double            m_TP;
   double            m_maxAllowedLots;
   double            m_maxTicksLoss;
   double            m_orderEquityRisk;
   double            m_orderEquityReward;
   ENUM_ORDER_TYPE   m_orderType;

   CChartObjectBmpLabel m_bgDialog;

   CChartObjectEdit  m_riskRatioEdit;
   CChartObjectEdit  m_riskValueEdit;
   CChartObjectEdit  m_orderLotsEdit;

   CChartObjectLabel m_symbolNameLabel;
   CChartObjectLabel m_tickSizeLabel;
   CChartObjectLabel m_maxEquityLossLabel;
   CChartObjectLabel m_equityLabel;
   CChartObjectLabel m_profitValueLabel;
   CChartObjectLabel m_askLabel;
   CChartObjectLabel m_bidLabel;
   CChartObjectLabel m_tpLabel;
   CChartObjectLabel m_slLabel;
   CChartObjectLabel m_maxAllowedLotsLabel;
   CChartObjectLabel m_maxTicksLossLabel;
   CChartObjectLabel m_orderEquityRiskLabel;
   CChartObjectLabel m_orderEquityRewardLabel;
   CChartObjectLabel m_orderTypeLabel;

   CChartObjectButton m_switchOrderTypeButton;
   CChartObjectButton m_placeOrderButton;
   CChartObjectButton m_quitEAButton;

public:

   void              CRRDialog(); // конструктор класса CRRDialog
   void             ~CRRDialog(); // деструктор класса CRRDialog

   bool              CreateCRRDialog(int topX,int leftY);
   int               DeleteCRRDialog();
   void              Refresh();
   void              SetRRRatio(double RRRatio);
   void              SetRiskPercent(double riskPercent);
   double            GetRiskPercent();
   double            GetRRRRatio();
   void              SetSL(double sl);
   void              SetTP(double tp);
   double            GetSL();
   double            GetTP();
   void              SetMaxAllowedLots(double lots);
   void              SetMaxTicksLoss(double ticks);
   void              SetOrderType(ENUM_ORDER_TYPE);
   void              SwitchOrderType();
   void              ResetButtons();
   ENUM_ORDER_TYPE   GetOrderType();
   void              SetOrderLots(double orderLots);
   double            GetOrderLots();
   void              SetOrderEquityRisk(double equityRisk);
   void              SetOrderEquityReward(double equityReward);
  };

Поскольку методы получения и установки значений переменных простые, я рассмотрю подробнее методы CreateCRRDialog() и Refresh(). Фоновая картинка, надписи, кнопки и поля ввода инициализируются в методе CreateCRRDialog().

Для инициализации меток и полей ввода я использую метод Create() с параметрами координат объекта на графике, методы Font() и FontSize() для установки шрифта и метод Description() для размещения текста на метке.

При инициализации кнопок используется метод Create() с дополнительными параметрами для установки размера кнопки и метод BackColor(), при помощи которого задается цвет фона кнопки.

bool CRRDialog::CreateCRRDialog(int topX,int leftY)
  {
   bool isCreated=false;

   MqlTick current_tick;
   SymbolInfoTick(Symbol(),current_tick);

   m_baseX = topX;
   m_baseY = leftY;

   m_bgDialog.Create(0, m_dialogName, 0, topX, leftY);
   m_bgDialog.BmpFileOn(m_bgFileName);

   m_symbolNameLabel.Create(0, "symbolNameLabel", 0, m_baseX + 120, m_baseY + 40);
   m_symbolNameLabel.Font("Verdana");
   m_symbolNameLabel.FontSize(8);
   m_symbolNameLabel.Description(Symbol());

   m_tickSizeLabel.Create(0, "tickSizeLabel", 0, m_baseX + 120, m_baseY + 57);
   m_tickSizeLabel.Font("Verdana");
   m_tickSizeLabel.FontSize(8);
   m_tickSizeLabel.Description(DoubleToString(SymbolInfoDouble(Symbol(), SYMBOL_TRADE_TICK_SIZE), Digits()));

   m_riskRatioEdit.Create(0, "riskRatioEdit", 0, m_baseX + 120, m_baseY + 72, 35, 15);
   m_riskRatioEdit.Font("Verdana");
   m_riskRatioEdit.FontSize(8);
   m_riskRatioEdit.Description(DoubleToString(m_RRRatio, 2));

   m_riskValueEdit.Create(0, "riskValueEdit", 0, m_baseX + 120, m_baseY + 90, 35, 15);
   m_riskValueEdit.Font("Verdana");
   m_riskValueEdit.FontSize(8);
   m_riskValueEdit.Description(DoubleToString(m_riskPercent, 2));

   m_equityLabel.Create(0, "equityLabel", 0, m_baseX + 120, m_baseY + 107);
   m_equityLabel.Font("Verdana");
   m_equityLabel.FontSize(8);
   m_equityLabel.Description(DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY),2));

   m_maxEquityLossLabel.Create(0, "maxEquityLossLabel", 0, m_baseX + 120, m_baseY + 122);
   m_maxEquityLossLabel.Font("Verdana");
   m_maxEquityLossLabel.FontSize(8);
   m_maxEquityLossLabel.Description(DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY)*m_riskPercent/100.0,2));

   m_askLabel.Create(0, "askLabel", 0, m_baseX + 120, m_baseY + 145);
   m_askLabel.Font("Verdana");
   m_askLabel.FontSize(8);
   m_askLabel.Description("");

   m_bidLabel.Create(0, "bidLabel", 0, m_baseX + 120, m_baseY + 160);
   m_bidLabel.Font("Verdana");
   m_bidLabel.FontSize(8);
   m_bidLabel.Description("");

   m_slLabel.Create(0, "slLabel", 0, m_baseX + 120, m_baseY + 176);
   m_slLabel.Font("Verdana");
   m_slLabel.FontSize(8);
   m_slLabel.Description("");

   m_tpLabel.Create(0, "tpLabel", 0, m_baseX + 120, m_baseY + 191);
   m_tpLabel.Font("Verdana");
   m_tpLabel.FontSize(8);
   m_tpLabel.Description("");

   m_maxAllowedLotsLabel.Create(0, "maxAllowedLotsLabel", 0, m_baseX + 120, m_baseY + 208);
   m_maxAllowedLotsLabel.Font("Verdana");
   m_maxAllowedLotsLabel.FontSize(8);
   m_maxAllowedLotsLabel.Description("");

   m_maxTicksLossLabel.Create(0, "maxTicksLossLabel", 0, m_baseX + 120, m_baseY + 223);
   m_maxTicksLossLabel.Font("Verdana");
   m_maxTicksLossLabel.FontSize(8);
   m_maxTicksLossLabel.Description("");

   m_orderLotsEdit.Create(0, "orderLotsEdit", 0, m_baseX + 120, m_baseY + 238, 35, 15);
   m_orderLotsEdit.Font("Verdana");
   m_orderLotsEdit.FontSize(8);
   m_orderLotsEdit.Description("");

   m_orderEquityRiskLabel.Create(0, "orderEquityRiskLabel", 0, m_baseX + 120, m_baseY + 255);
   m_orderEquityRiskLabel.Font("Verdana");
   m_orderEquityRiskLabel.FontSize(8);
   m_orderEquityRiskLabel.Description("");

   m_orderEquityRewardLabel.Create(0, "orderEquityRewardLabel", 0, m_baseX + 120, m_baseY + 270);
   m_orderEquityRewardLabel.Font("Verdana");
   m_orderEquityRewardLabel.FontSize(8);
   m_orderEquityRewardLabel.Description("");

   m_switchOrderTypeButton.Create(0, "switchOrderTypeButton", 0, m_baseX + 20, m_baseY + 314, 160, 20);
   m_switchOrderTypeButton.Font("Verdana");
   m_switchOrderTypeButton.FontSize(8);
   m_switchOrderTypeButton.BackColor(LightBlue);

   m_placeOrderButton.Create(0, "placeOrderButton", 0, m_baseX + 20, m_baseY + 334, 160, 20);
   m_placeOrderButton.Font("Verdana");
   m_placeOrderButton.FontSize(8);
   m_placeOrderButton.BackColor(LightBlue);
   m_placeOrderButton.Description("Place Market Order");

   m_quitEAButton.Create(0, "quitEAButton", 0, m_baseX + 20, m_baseY + 354, 160, 20);
   m_quitEAButton.Font("Verdana");
   m_quitEAButton.FontSize(8);
   m_quitEAButton.BackColor(LightBlue);
   m_quitEAButton.Description("Quit");

   return isCreated;
  }

Метод Refresh() обновляет все метки и кнопки текущими значениями уровней bid/ask, количеством средств на счете и величиной риска.

void CRRDialog::Refresh()
  {
   MqlTick current_tick;
   SymbolInfoTick(Symbol(),current_tick);

   m_equityLabel.Description(DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY),2));
   m_maxEquityLossLabel.Description(DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY)*
                                         StringToDouble(m_riskValueEdit.Description())/100.0,2));
   m_askLabel.Description(DoubleToString(current_tick.ask, Digits()));
   m_bidLabel.Description(DoubleToString(current_tick.bid, Digits()));
   m_slLabel.Description(DoubleToString(m_SL, Digits()));
   m_tpLabel.Description(DoubleToString(m_TP, Digits()));
   m_maxAllowedLotsLabel.Description(DoubleToString(m_maxAllowedLots,2));
   m_maxTicksLossLabel.Description(DoubleToString(m_maxTicksLoss,0));
   m_orderEquityRiskLabel.Description(DoubleToString(m_orderEquityRisk,2));
   m_orderEquityRewardLabel.Description(DoubleToString(m_orderEquityReward,2));

   if(m_orderType==ORDER_TYPE_BUY) m_switchOrderTypeButton.Description("Order Type: BUY");
   else if(m_orderType==ORDER_TYPE_SELL) m_switchOrderTypeButton.Description("Order Type: SELL");
  }


4. События при работе с графиком

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

Обрабатываются следующие события:

  • перетаскивание указателя S/L (объект SL_arrow класса CChartObjectBitmap) по графику - это позволит удобно управлять уровнями Stop Loss и вычислять Take Profit на основе заданного соотношения прибыль/риск.
  • обработка событий кнопки переключения типа ордера (buy/sell)
  • нажатие кнопки "разместить ордер по рынку" (place market order)
  • редактирования риска, соотношения прибыль/риск и размера лота
  • завершение работы советника после нажатия кнопки "Exit".

Обрабатываемыми являются следующие события: CHARTEVENT_OBJECT_CLICK для выбора объектов и кнопок,  CHARTEVENT_OBJECT_DRAG для перетаскивания указателя S/L и CHARTEVENT_OBJECT_ENDEDIT после обновления трейдером полей ввода.

Первоначальная реализация функции OnChartEvent() занимала несколько страниц кода, потом я решил разделить ее на несколько обработчиков событий, и функция OnChartEvent() стала более читабельной:

void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//--- проверим, является ли событие нажатием на кнопку мыши
   if(id==CHARTEVENT_OBJECT_CLICK)
     {
      string clickedChartObject=sparam;

      if(clickedChartObject==slButtonID)
         SL_arrow.Selected(!SL_arrow.Selected());

      if(clickedChartObject==switchOrderTypeButtonID)
        {
         EA_switchOrderType();
        };

      if(clickedChartObject==placeOrderButtonID)
        {
         EA_placeOrder();
        }

      if(clickedChartObject==quitEAButtonID) ExpertRemove();

      ChartRedraw();
     }

   if(id==CHARTEVENT_OBJECT_DRAG)
     {
      // покупка (BUY) 
      if(visualRRDialog.GetOrderType()==ORDER_TYPE_BUY)
        {
         EA_dragBuyHandle();
        };

      // продажа (SELL)
      if(visualRRDialog.GetOrderType()==ORDER_TYPE_SELL)
        {
         EA_dragSellHandle();
        };
      ChartRedraw();
     }

   if(id==CHARTEVENT_OBJECT_ENDEDIT)
     {
      if((sparam==riskRatioEditID || sparam==riskValueEditID || sparam==orderLotsEditID) && orderPlaced==false)
        {
         EA_editParamsUpdate();
        }
     }
  }

Более подробная реализация обработчиков событий будет рассмотрена позже. 

Стоит отметить один трюк, который я использовал для выбора объекта SL_arrow.  Обычно для выбора объекта на графике нужно дважды кликнуть по нему. Но это можно сделать и по-другому: выбирать объект при помощи обработки однократного клика, вызывая метод Selected() объекта класса CChartObject или его наследников в обработчике события CHARTEVENT_OBJECT_CLICK  функции OnChartEvent():

      if(clickedChartObject==slButtonID)
         SL_arrow.Selected(!SL_arrow.Selected());

Таким образом, текущее значение флага "выбранности" объекта изменяется (в зависимости от предыдущего состояния) при помощи одного клика.

5. Расширенный класс управления капиталом на основе класса CMoneyFixedRisk

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

Для управления капиталом я воспользовался классом CMoneyFixedRiskExt, созданнного на основе класса CMoneyFixedRisk, предоставленным MetaQuotes Software Corp.

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

Я изменил методы CheckOpenLong() и CheckOpenShort() таким образом, что в качестве размера лота возвращается значение 0.0 в случае несоблюдения требований по рискам. Класс расширен четырьмя новыми методами: GetMaxSLPossible(), CalcMaxTicksLoss(), CalcOrderEquityRisk() and CalcOrderEquityReward():

class CMoneyFixedRiskExt : public CExpertMoney
  {
public:
   //---
   virtual double    CheckOpenLong(double price,double sl);
   virtual double    CheckOpenShort(double price,double sl);
   
   double GetMaxSLPossible(double price, ENUM_ORDER_TYPE orderType);
   double CalcMaxTicksLoss();
   double CalcOrderEquityRisk(double price, double sl, double lots);
   double CalcOrderEquityReward(double price, double sl, double lots, double rrratio);
  };

Метод GetMaxSLPossible() вычисляет максимально допустимую цену Stop Loss при заданной величине рискового капитала и минимально допустимым торговым объемом.

Например, если баланс счета равен 10 000 в базовой валюте и допустимый риск равен 2%, то размер рискового капитала равен 200 единицам валюты депозита. Если минимально допустимый торговый объем составляет 0.1 лота, этот метод возвращает уровень цены для ордеров типа ORDER_TYPE_BUY или ORDER_TYPE_SELL при заданной величине риска для позиции объемом 0.1 лота.

Это помогает оценить максимальный уровень Stop Loss, который мы можем себе позволить для минимального торгового объема. Это тот ценовой уровень, который нельзя пересекать для заданного уровня риска.

double CMoneyFixedRiskExt::GetMaxSLPossible(double price, ENUM_ORDER_TYPE orderType)
{
   double maxEquityLoss, tickValLoss, maxTicksLoss;
   double minvol=m_symbol.LotsMin();
   double orderTypeMultiplier;
   
   if(m_symbol==NULL) return(0.0);
   
   switch (orderType)
   {
   case ORDER_TYPE_SELL: orderTypeMultiplier = -1.0; break;
   case ORDER_TYPE_BUY: orderTypeMultiplier = 1.0; break;
   default: orderTypeMultiplier = 0.0;
   }
   
   maxEquityLoss = m_account.Balance()*m_percent/100.0; // максимальный убыток 
   tickValLoss = minvol*m_symbol.TickValueLoss();      // стоимость пункта потерь
   maxTicksLoss = MathFloor(maxEquityLoss/tickValLoss);
 
   return (price - maxTicksLoss*m_symbol.TickSize()*orderTypeMultiplier);
}

Метод CalcMaxTickLoss() возвращает максимальное количество пунктов, которые мы можем себе позволить при заданном риске для минимального торгового объема.

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

double CMoneyFixedRiskExt::CalcMaxTicksLoss()
{
   double maxEquityLoss, tickValLoss, maxTicksLoss;
   double minvol=m_symbol.LotsMin();
   
   if(m_symbol==NULL) return(0.0);
   
   maxEquityLoss = m_account.Balance()*m_percent/100.0; // максимально допустимый убыток 
   tickValLoss = minvol*m_symbol.TickValueLoss();      // величина пункта потерь
   maxTicksLoss = MathFloor(maxEquityLoss/tickValLoss);
   
   return (maxTicksLoss);
}

Метод CalcOrderEquityRisk() возвращает величину рискового капитала для заданной цены, уровня Stop Loss и количества лотов.

Она вычисляется умножением стоимости пункта потерь на количество лотов, а затем умножается на разность (в пунктах) между текущей ценой и ценой уровня Stop Loss:

double CMoneyFixedRiskExt::CalcOrderEquityRisk(double price,double sl, double lots)
{
   double equityRisk;
   
   equityRisk = lots*m_symbol.TickValueLoss()*(MathAbs(price-sl)/m_symbol.TickSize()); 
   
   if (dbg) Print("calcEquityRisk: lots = " + DoubleToString(lots) +
                 " TickValueLoss = " + DoubleToString(m_symbol.TickValueLoss()) +
                 " risk = " + DoubleToString(equityRisk));
   
   return equityRisk;
}

Метод CalcOrderEquityReward() аналогичен методу CalcOrderEquityRisk(), однако вместо TickValueLoss()  он использует TickValueProfit() и умножает результат на отношение прибыль/риск:

double CMoneyFixedRiskExt::CalcOrderEquityReward(double price,double sl, double lots, double rrratio)
{
   double equityReward; 
   equityReward = lots*m_symbol.TickValueProfit()*(MathAbs(price-sl)/m_symbol.TickSize())*rrratio; 
   
   if (dbg) Print("calcEquityReward: lots = " + DoubleToString(lots) + 
                   " TickValueProfit = " + DoubleToString(m_symbol.TickValueProfit()) +
                 " reward = " + DoubleToString(equityReward));
   return equityReward;
}

Этих методов достаточно для вычисления в реальном времени максимальных уровней Stop Loss, рискового капитала и прибыли. Метод CalcMaxTickLoss() используется для корректировки значения в прямоугольнике риска - если трейдер захочет указать величину за пределами допустимого количества пунктов, которые может позволить себе потерять, то в прямоугольнике будет указано максимальное количество пунктов потерь.

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


6. Реализация обработчика событий графика

Обработчик EA_switchOrderType() срабатывает после получения события CHARTEVENT_OBJECT_CLICK от объекта класса m_switchOrderTypeButton.

Он переключает типы ордеров ORDER_TYPE_BUY и ORDER_TYPE_SELL, сбрасывает состояния кнопок, переменных диалога и удаляет с графика объекты прямоугольников риска и прибыли:

void EA_switchOrderType()
  {
   symbolInfo.RefreshRates();

   visualRRDialog.SwitchOrderType();
   visualRRDialog.ResetButtons();
   visualRRDialog.SetSL(0.0);
   visualRRDialog.SetTP(0.0);
   visualRRDialog.SetMaxAllowedLots(0.0);
   visualRRDialog.SetOrderLots(0.0);
   visualRRDialog.SetMaxTicksLoss(0);
   visualRRDialog.SetOrderEquityRisk(0.0);
   visualRRDialog.SetOrderEquityReward(0.0);

   if(visualRRDialog.GetOrderType()==ORDER_TYPE_BUY) SL_arrow.SetDouble(OBJPROP_PRICE,symbolInfo.Ask());
   else if(visualRRDialog.GetOrderType()==ORDER_TYPE_SELL) SL_arrow.SetDouble(OBJPROP_PRICE,symbolInfo.Bid());
   SL_arrow.SetInteger(OBJPROP_TIME,0,TimeCurrent());

   rectReward.Delete();
   rectRisk.Delete();

   visualRRDialog.Refresh();

  }

Обработчик EA_dragBuyHandle() срабатывает после захвата и перетаскивания по графику объекта SL_arrow. Сначала он считывает координаты времени и цены (где был размещен) на графике и устанавливает ценовой уровень как виртуальный Stop Loss нашей сделки.

Это позволяет оценить ширину канала нашего Stop Loss при заданном риске. После вычисления риска и прибыли объекты прямоугольников обновляются на графике.

void EA_dragBuyHandle()
  {
   SL_arrow.GetDouble(OBJPROP_PRICE,0,SL_price);
   SL_arrow.GetInteger(OBJPROP_TIME,0,startTime);

   symbolInfo.RefreshRates();
   currentTime=TimeCurrent();

// BUY
   double allowedLots=MM.CheckOpenLong(symbolInfo.Ask(),SL_price);
   Print("Allowed lots = "+DoubleToString(allowedLots,2));
   double lowestSLAllowed=MM.GetMaxSLPossible(symbolInfo.Ask(),ORDER_TYPE_BUY);

   if(SL_price<lowestSLAllowed)
     {
      SL_price=lowestSLAllowed;
      ObjectSetDouble(0,slButtonID,OBJPROP_PRICE,lowestSLAllowed);
     }

   visualRRDialog.SetSL(SL_price);
   visualRRDialog.SetTP(symbolInfo.Ask()+(symbolInfo.Ask()-SL_price)*visualRRDialog.GetRRRRatio());

   if(visualRRDialog.GetTP()<SL_price)
     {
      visualRRDialog.SetSL(0.0);
      visualRRDialog.SetTP(0.0);
      SL_arrow.SetDouble(OBJPROP_PRICE,symbolInfo.Ask());
      rectReward.Delete();
      rectRisk.Delete();
      return;
     }

   double lotSize=MM.CheckOpenLong(symbolInfo.Ask(),SL_price);

   visualRRDialog.SetMaxAllowedLots(lotSize);
   visualRRDialog.SetOrderLots(lotSize);
   visualRRDialog.SetMaxTicksLoss(MM.CalcMaxTicksLoss());
   visualRRDialog.SetOrderEquityRisk(MM.CalcOrderEquityRisk(symbolInfo.Ask(), SL_price, lotSize));
   visualRRDialog.SetOrderEquityReward(MM.CalcOrderEquityReward(symbolInfo.Ask(), 
                                       SL_price, lotSize, visualRRDialog.GetRRRRatio()));
   visualRRDialog.Refresh();

   rectUpdate(visualRRDialog.GetOrderType());

  }

Функция EA_dragSellHandle() вызывается при настройке ордера на продажу.

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

void EA_dragSellHandle()
  {
   SL_arrow.GetDouble(OBJPROP_PRICE,0,SL_price);
   SL_arrow.GetInteger(OBJPROP_TIME,0,startTime);

   symbolInfo.RefreshRates();
   currentTime=TimeCurrent();

   double allowedLots=MM.CheckOpenShort(symbolInfo.Bid(),SL_price);
   Print("Allowed lots = "+DoubleToString(allowedLots,2));
   double maxSLAllowed=MM.GetMaxSLPossible(symbolInfo.Bid(),ORDER_TYPE_SELL);

   if(SL_price>maxSLAllowed)
     {
      SL_price=maxSLAllowed;
      SL_arrow.SetDouble(OBJPROP_PRICE,0,maxSLAllowed);
     }

   visualRRDialog.SetSL(SL_price);
   visualRRDialog.SetTP(symbolInfo.Bid()-(SL_price-symbolInfo.Bid())*visualRRDialog.GetRRRRatio());

   if(visualRRDialog.GetTP()>SL_price)
     {
      visualRRDialog.SetSL(0.0);
      visualRRDialog.SetTP(0.0);
      SL_arrow.SetDouble(OBJPROP_PRICE,symbolInfo.Bid());
      rectReward.Delete();
      rectRisk.Delete();
      return;
     }

   double lotSize=MM.CheckOpenShort(symbolInfo.Bid(),SL_price);

   visualRRDialog.SetMaxAllowedLots(lotSize);
   visualRRDialog.SetOrderLots(lotSize);
   visualRRDialog.SetMaxTicksLoss(MM.CalcMaxTicksLoss());
   visualRRDialog.SetOrderEquityRisk(MM.CalcOrderEquityRisk(symbolInfo.Bid(), SL_price, lotSize));
   visualRRDialog.SetOrderEquityReward(MM.CalcOrderEquityReward(symbolInfo.Bid(),
                                       SL_price, lotSize, visualRRDialog.GetRRRRatio()));
   visualRRDialog.Refresh();

   rectUpdate(visualRRDialog.GetOrderType());

  }

Функция EA_placeOrder() вызывается после нажатия на объект класса m_placeOrderButton. Он размещает рыночный ордер на покупку или продажу с уровнями Stop Loss и Take Profit и заданным размером лота.

Обратите внимание на то, каким простым является размещение рыночных ордеров при помощи класса CExpertTrade.

bool EA_placeOrder()
  {
   symbolInfo.RefreshRates();
   visualRRDialog.ResetButtons();

   if(visualRRDialog.GetOrderType()==ORDER_TYPE_BUY)
      orderPlaced=trade.Buy(visualRRDialog.GetOrderLots(),symbolInfo.Ask(),
                            visualRRDialog.GetSL(),visualRRDialog.GetTP(),TimeToString(TimeCurrent()));
   else if(visualRRDialog.GetOrderType()==ORDER_TYPE_SELL)
      orderPlaced=trade.Sell(visualRRDialog.GetOrderLots(),symbolInfo.Bid(),
                            visualRRDialog.GetSL(),visualRRDialog.GetTP(),TimeToString(TimeCurrent()));

   return orderPlaced;
  }

Обработчик EA_editParamsUpdate() срабатывает при нажатии клавиши Enter после редактирования одного из следующих полей ввода: riskRatioEdit, riskValueEdit и orderLotsEdit.

Когда это происходит, требуется пересчет допустимого количества лотов, уровней Take Profit, максимального уровня Stop Loss, величины рискового капитала и прибыли:

void EA_editParamsUpdate()
  {
   MM.Percent(visualRRDialog.GetRiskPercent());

   SL_arrow.GetDouble(OBJPROP_PRICE, 0, SL_price);
   SL_arrow.GetInteger(OBJPROP_TIME, 0, startTime);

   symbolInfo.RefreshRates();
   currentTime=TimeCurrent();

   double allowedLots=MM.CheckOpenLong(symbolInfo.Ask(),SL_price);

   double lowestSLAllowed=MM.GetMaxSLPossible(symbolInfo.Ask(),ORDER_TYPE_BUY);
   if(SL_price<lowestSLAllowed)
     {
      SL_price=lowestSLAllowed;
      ObjectSetDouble(0,slButtonID,OBJPROP_PRICE,lowestSLAllowed);
     }

   visualRRDialog.SetSL(SL_price);
   visualRRDialog.SetTP(symbolInfo.Ask()+(symbolInfo.Ask()-SL_price)*visualRRDialog.GetRRRRatio());

   visualRRDialog.SetMaxTicksLoss(MM.CalcMaxTicksLoss());
   visualRRDialog.SetOrderEquityRisk(MM.CalcOrderEquityRisk(symbolInfo.Ask(), 
                                     SL_price, visualRRDialog.GetOrderLots()));
   visualRRDialog.SetOrderEquityReward(MM.CalcOrderEquityReward(symbolInfo.Ask(), SL_price, 
                                       visualRRDialog.GetOrderLots(), visualRRDialog.GetRRRRatio()));
   visualRRDialog.Refresh();
   rectUpdate(visualRRDialog.GetOrderType());

   ChartRedraw();
  }

Функция EA_onTick() вызывается каждый раз при приходе нового тика. Вычисления производятся лишь в том случае, если ордер еще не был размещен, а уровень Stop Loss уже указан путем перетаскивания указателя SL_arrow.

После размещения ордера пересчета величины риска, прибыли и уровней Take Profit не требуется.

void EA_onTick()
  {
   if(SL_price!=0.0 && orderPlaced==false)
     {
      double lotSize=0.0;
      SL_price=visualRRDialog.GetSL();
      symbolInfo.RefreshRates();

      if(visualRRDialog.GetOrderType()==ORDER_TYPE_BUY)
         lotSize=MM.CheckOpenLong(symbolInfo.Ask(),SL_price);
      else if(visualRRDialog.GetOrderType()==ORDER_TYPE_SELL)
         lotSize=MM.CheckOpenShort(symbolInfo.Ask(),SL_price);

      visualRRDialog.SetMaxAllowedLots(lotSize);
      if(visualRRDialog.GetOrderLots()>lotSize) visualRRDialog.SetOrderLots(lotSize);

      visualRRDialog.SetMaxTicksLoss(MM.CalcMaxTicksLoss());

      if(visualRRDialog.GetOrderType()==ORDER_TYPE_BUY)
        {
         visualRRDialog.SetTP(symbolInfo.Ask()+(symbolInfo.Ask()-SL_price)*visualRRDialog.GetRRRRatio());
         visualRRDialog.SetOrderEquityRisk(MM.CalcOrderEquityRisk(symbolInfo.Ask(), 
                                           SL_price, visualRRDialog.GetOrderLots()));
         visualRRDialog.SetOrderEquityReward(MM.CalcOrderEquityReward(symbolInfo.Ask(), SL_price, 
                                             visualRRDialog.GetOrderLots(), visualRRDialog.GetRRRRatio()));
        }
      else if(visualRRDialog.GetOrderType()==ORDER_TYPE_SELL)
        {
         visualRRDialog.SetTP(symbolInfo.Bid()-(SL_price-symbolInfo.Bid())*visualRRDialog.GetRRRRatio());
         visualRRDialog.SetOrderEquityRisk(MM.CalcOrderEquityRisk(
                                           symbolInfo.Bid(), SL_price, visualRRDialog.GetOrderLots()));
         visualRRDialog.SetOrderEquityReward(MM.CalcOrderEquityReward(symbolInfo.Bid(), SL_price, 
                                             visualRRDialog.GetOrderLots(), visualRRDialog.GetRRRRatio()));
        }
      visualRRDialog.Refresh();
      rectUpdate(visualRRDialog.GetOrderType());
     }

   ChartRedraw(0);
  }

Функция rectUpdate() отвечает за перерисовку цветных прямоугольников риска и прибыли. Контрольными точками является время объекта SL_arrow, текущие цены Bid/Ask (в зависимости от типа ордера) и уровни Stop Loss и Take Profit.

Ценовой диапазон между текущей ценой и уровнем Stop Loss изображается светло-розовым прямоугольником. Зеленым цветом окрашивается прямоугольник для диапазона цен между текущей ценой и уровнем Take Profit. Оба прямоугольника очень полезны для анализа влияния соотношения прибыль/риск на уровни Stop Loss и Take Profit, они помогают управлять риском перед совершением сделки.

void rectUpdate(ENUM_ORDER_TYPE orderType)
  {
   symbolInfo.RefreshRates();
   currentTime=TimeCurrent();
   SL_arrow.GetInteger(OBJPROP_TIME,0,startTime);

   if(orderType==ORDER_TYPE_BUY)
     {
      rectReward.Create(0,rewardRectID,0,startTime,symbolInfo.Ask(),currentTime,symbolInfo.Ask()+
                       (symbolInfo.Ask()-visualRRDialog.GetSL())*visualRRDialog.GetRRRRatio());
      rectReward.Color(LightGreen);
      rectReward.Background(true);

      rectRisk.Create(0,riskRectID,0,startTime,visualRRDialog.GetSL(),currentTime,symbolInfo.Ask());
      rectRisk.Color(LightPink);
      rectRisk.Background(true);
     }
   else if(orderType==ORDER_TYPE_SELL)
     {
      rectReward.Create(0,rewardRectID,0,startTime,symbolInfo.Bid(),currentTime,symbolInfo.Bid()-
                        (visualRRDialog.GetSL()-symbolInfo.Bid())*visualRRDialog.GetRRRRatio());
      rectReward.Color(LightGreen);
      rectReward.Background(true);

      rectRisk.Create(0,riskRectID,0,startTime,visualRRDialog.GetSL(),currentTime,symbolInfo.Bid());
      rectRisk.Color(LightPink);
      rectRisk.Background(true);
     }
  }

 

7. Демонстрация

Пожалуйста, посмотрите демонстрацию работы советника. Здесь показан пример размещения ордера на продажу после качелей при открытии в понедельник 01/11/2010.

Рекомендую смотреть это в полноэкранном режиме в качестве 480p. Видео содержит комментарии:

 

Выводы

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

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

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

Прикрепленные файлы |
visualrrea.mq5 (13.84 KB)
crrdialog.mqh (13.95 KB)
visualrrids.mqh (0.8 KB)
images.zip (159.05 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (2)
Izzatilla Ikramov
Izzatilla Ikramov | 9 янв. 2015 в 05:40

Вообще супер! Давно думал о создании помощника-советника для своей ручной торговли. Часть уже реализована. 

Спасибо автору за проделанную работу.  

Alexey Volchanskiy
Alexey Volchanskiy | 9 янв. 2015 в 15:22

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

Автору респект!

Александр Ануфренко: "Знал бы, где упасть - перинку бы подстелил" (ATC 2010) Александр Ануфренко: "Знал бы, где упасть - перинку бы подстелил" (ATC 2010)
Рискованная разработка Александра Ануфренко (Anufrenko321) в течение трех недель не покидала первую тройку Чемпионата. Пережив на прошлой неделе чудовищный стоп-лосс, эксперт потерял около $60 000, но сейчас вновь подбирается к лидирующим позициям. Автор этого интересного эксперта решил рассказать о принципах работы и особенностях своего детища.
Копирование торговли из MetaTrader 5 в MetaTrader 4 Копирование торговли из MetaTrader 5 в MetaTrader 4
Можно ли в MetaTrader 5 торговать на реале уже сегодня? Как организовать такую торговлю? Приводится теория этих вопросов и рабочие коды, при помощи которых реализуется копирование сделок из терминала MetaTrader 5 в MetaTrader 4. Статья будет полезна как разработчикам советников, так и практикующим трейдерам.
Как использовать торговые классы Стандартной библиотеки при написании советника Как использовать торговые классы Стандартной библиотеки при написании советника
В статье рассказывается о том, как использовать основной функционал торговых классов Стандартной библиотеки при написании советников, в которых применяется открытие, закрытие и модификация позиции, проверка свободной маржи перед размещением торговых ордеров, размещение и удаление отложенных ордеров. Показано, как использовать торговые классы для получения свойств ордеров и сделок.
Владимир Цирульник: "Суть моей программы - импровизация!" (ATC 2010) Владимир Цирульник: "Суть моей программы - импровизация!" (ATC 2010)
На счету у Владимира Цирульника один из самых ярких взлетов текущего Чемпионата. К концу третьей торговой недели эксперт Владимира пришел шестым и, похоже, не собирается сдавать позиций. Алгоритм IMEX, который лежит в основе советника, был создан самим разработчиком. Чтобы узнать об этом алгоритме побольше, мы связались с Владимиром.