English 中文 Español Deutsch 日本語 Português
Создаем помощника в ручной торговле

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

MetaTrader 5Примеры | 8 июня 2016, 10:02
12 099 44
Dmitriy Gizlyk
Dmitriy Gizlyk

Введение

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

1. Определяем необходимый функционал для торговой панели

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

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

  1. Кнопки покупки и продажи.
  2. Кнопки закрытия всех позиций по символу и счету или в отдельном направлении (ордера на покупку или продажу).
  3. Возможность указания уровней стоп-лосса и тейк-профита как в пунктах, так и в валюте депозита (при вводе одного параметра другой должен автоматически корректироваться).
  4. Панель должна автоматически рассчитывать уровни стоп-лосса и тейк-профита по задаваемым вручную параметрам (п.2) и отображать их на графике.
  5. У трейдера должна быть возможность перемещения уровней стоп-лосса и/или тейк-профита на графике. При этом все изменения должны отражаться на панели с изменением соответствующих значений.
  6. Панель должна рассчитывать предстоящий объём сделки по заданным параметрам риска (в валюте депозита или в процентах от текущего баланса).
  7. Трейдер должен иметь возможность самостоятельно задать объём сделки. При этом зависящие от него соответствующие параметры должны пересчитываться автоматически.
  8. Панель должна запоминать, какие параметры внесены трейдером, а какие — автоматически рассчитаны. Это необходимо, чтобы при последующих пересчетах параметры, внесённые трейдером, по возможности не изменялись.
  9. Панель должна сохранять все внесённые параметры, чтобы при перезагрузках не вносить их повторно.

2. Создаем графический макет панели

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

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

Вот мой вариант.

Дизайн

3. Построение макета панели на языке MQL5

3.1.  Заготовка

Теперь, когда у нас есть представление о конечной цели, воплотим ее в коде MQL5. Для этого по возможности максимально будем использовать стандартные библиотеки, которые могут облегчить нашу работу. В MQL5 существует класс CAppDialog, который является базовым для построения диалоговых окон. На базе этого класса мы и построим нашу панель. 
Для этого создадим дубликат класса и проинициализируем его в функции OnInit().

#include <Controls\Dialog.mqh>

class CTradePanel : public CAppDialog
  {
public:
                     CTradePanel(void){};
                    ~CTradePanel(void){};
  };

CTradePanel TradePanel;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   // Create Trade Panel
   if(!TradePanel.Create(ChartID(),"Trade Panel",0,20,20,320,420))
     {
      return (INIT_FAILED);
     }
   // Run Trade Panel
   TradePanel.Run();
//---
   return(INIT_SUCCEEDED);
  }

Результат этой несложной манипуляции — заготовка нашей будущей панели.

Заготовка

3.2. Объявление необходимых объектов

Теперь нанесем на нашу заготовку необходимые элементы управления. Для этого создадим объекты соответствующих классов для каждого элемента управления. Создавать объекты мы будем с помощью стандартных классов CLabel, CEdit, CButton и CBmpButton.

Добавляем необходимые включаемые файлы и создаем функцию Create() для класса CTradePanel:

#include <Controls\Dialog.mqh>
#include <Controls\Label.mqh>
#include <Controls\Button.mqh>

Файлы "Edit.mqh" и "BmpButton.mqh" я не включил намеренно, так как они уже вызываются из "Dialog.mqh".

Следующий шаг — для каждого объекта на панели в классе CTradePanel объявляем переменные соответствующего типа, и там же объявляем процедуру Create(..), в которой и расставим все элементы по своим местам. Обратите внимание: объявление переменных и другие действия внутри класса CTradePanel мы объявляем в блоке "private". Функции же, доступные для вызова из-за пределов класса, такие как Create(...), объявляются в блоке "public".

class CTradePanel : public CAppDialog
  {
private:

   CLabel            ASK, BID;                        // Display Ask and Bid prices
   CLabel            Balance_label;                   // Display label "Account Balance"
   CLabel            Balance_value;                   // Display Account balance
   CLabel            Equity_label;                    // Display label "Account Equity"
   CLabel            Equity_value;                    // Display Account Equity
   CLabel            PIPs;                            // Display label "Pips"
   CLabel            Currency;                        // Display Account currency
   CLabel            ShowLevels;                      // Display label "Show"
   CLabel            StopLoss;                        // Display label "Stop Loss"
   CLabel            TakeProfit;                      // Display label "TakeProfit"
   CLabel            Risk;                            // Display label "Risk"
   CLabel            Equity;                          // Display label "% to Equity"
   CLabel            Currency2;                       // Display Account currency
   CLabel            Orders;                          // Display label "Opened Orders"
   CLabel            Buy_Lots_label;                  // Display label "Buy Lots"
   CLabel            Buy_Lots_value;                  // Display Buy Lots value 
   CLabel            Sell_Lots_label;                 // Display label "Sell Lots"
   CLabel            Sell_Lots_value;                 // Display Sell Lots value 
   CLabel            Buy_profit_label;                // Display label "Buy Profit"
   CLabel            Buy_profit_value;                // Display Buy Profit value 
   CLabel            Sell_profit_label;               // Display label "Sell Profit"
   CLabel            Sell_profit_value;               // Display Sell profit value 
   CEdit             Lots;                            // Display volume of next order
   CEdit             StopLoss_pips;                   // Display Stop loss in pips
   CEdit             StopLoss_money;                  // Display Stop loss in account currency
   CEdit             TakeProfit_pips;                 // Display Take profit in pips
   CEdit             TakeProfit_money;                // Display Take profit in account currency
   CEdit             Risk_percent;                    // Display Risk percent to equity
   CEdit             Risk_money;                      // Display Risk in account currency
   CBmpButton        StopLoss_line;                   // Check to display StopLoss Line
   CBmpButton        TakeProfit_line;                 // Check to display TakeProfit Line
   CBmpButton        StopLoss_pips_b;                 // Select Stop loss in pips
   CBmpButton        StopLoss_money_b;                // Select Stop loss in account currency
   CBmpButton        TakeProfit_pips_b;               // Select Take profit in pips
   CBmpButton        TakeProfit_money_b;              // Select Take profit in account currency
   CBmpButton        Risk_percent_b;                  // Select Risk percent to equity
   CBmpButton        Risk_money_b;                    // Select Risk in account currency
   CBmpButton        Increase,Decrease;               // Increase and Decrease buttons
   CButton           SELL,BUY;                        // Sell and Buy Buttons
   CButton           CloseSell,CloseBuy,CloseAll;     // Close buttons
   
public:
                     CTradePanel(void){};
                    ~CTradePanel(void){};
  //--- Create function
   virtual bool      Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2);
   
  };

3.3. Создание процедур инициализации для групп объектов

Настало время прописать тело функции Create(...). Обратите внимание, что в этой функции мы должны проинициализировать все объявленные выше объекты. Легко посчитать, что выше мы объявили 45 объектов 4-х типов. А значит, целесообразно прописать 4 процедуры инициализации объектов, по одной для каждого типа.  Функции инициализации классов объявляем в блоке "private".

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

Класс CLabel

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

  • имя объекта;
  • отображаемый текст;
  • координаты элемента;
  • выравнивание объекта относительно точки привязки.

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

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

string name=m_name+"Label"+(string)ObjectsTotal(chart,-1,OBJ_LABEL);

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

  enum label_align
     {
      left=-1,
      right=1,
      center=0
     };

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

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

  • Создадим объект при помощи функции Create(...) родительского класса.
  • Затем поместим в объект необходимый текст.
  • Выровняем объект относительно точки привязки.
  • Добавим объект в "контейнер" диалогового окна.
bool CTradePanel::CreateLabel(const long chart,const int subwindow,CLabel &object,const string text,const uint x,const uint y,label_align align)
  {
   // All objects must have separate name
   string name=m_name+"Label"+(string)ObjectsTotal(chart,-1,OBJ_LABEL);
   //--- Call Create function
   if(!object.Create(chart,name,subwindow,x,y,0,0))
     {
      return false;
     }
   //--- Adjust text
   if(!object.Text(text))
     {
      return false;
     }
   //--- Align text to Dialog box's grid
   ObjectSetInteger(chart,object.Name(),OBJPROP_ANCHOR,(align==left ? ANCHOR_LEFT_UPPER : (align==right ? ANCHOR_RIGHT_UPPER : ANCHOR_UPPER)));
   //--- Add object to controls
   if(!Add(object))
     {
      return false;
     }
   return true;
  }

Класс CButton

Класс CButton предназначен для создания кнопок прямоугольной формы с надписью. Это наши стандартные кнопки открытия и закрытия ордеров.

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

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

bool CTradePanel::CreateButton(const long chart,const int subwindow,CButton &object,const string text,const uint x,const uint y,const uint x_size,const uint y_size)
  {
   // All objects must have separate name
   string name=m_name+"Button"+(string)ObjectsTotal(chart,-1,OBJ_BUTTON);
   //--- Call Create function
   if(!object.Create(chart,name,subwindow,x,y,x+x_size,y+y_size))
     {
      return false;
     }
   //--- Adjust text
   if(!object.Text(text))
     {
      return false;
     }
   //--- set button flag to unlock
   object.Locking(false);
   //--- set button flag to unpressed
   if(!object.Pressed(false))
     {
      return false;
     }
   //--- Add object to controls
   if(!Add(object))
     {
      return false;
     }
   return true;
  }

Класс CEdit

Класс CEdit предназначен для создания объектов ввода данных. К таким объектам на нашей панели относятся ячейки ввода объема сделки, уровней стоп-лосса и тейк-профита (в пунктах и в валюте депозита) и уровня риска.

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

Объекты класса CEdit, в отличие от кнопок, не имеют состояния "Нажата" / "Отжата", но в то же время этим классом предусмотрено создание объектов, недоступных для редактирования пользователем во время работы программы. В нашем случае все объекты должны быть доступны для редактирования пользователем. Укажем это в нашей функции инициализации. 

bool CTradePanel::CreateEdit(const long chart,const int subwindow,CEdit &object,const string text,const uint x,const uint y,const uint x_size,const uint y_size)
  {
   // All objects must have separate name
   string name=m_name+"Edit"+(string)ObjectsTotal(chart,-1,OBJ_EDIT);
   //--- Call Create function
   if(!object.Create(chart,name,subwindow,x,y,x+x_size,y+y_size))
     {
      return false;
     }
   //--- Adjust text
   if(!object.Text(text))
     {
      return false;
     }
   //--- Align text in Edit box
   if(!object.TextAlign(ALIGN_CENTER))
     {
      return false;
     }
   //--- set Read only flag to false
   if(!object.ReadOnly(false))
     {
      return false;
     }
   //--- Add object to controls
   if(!Add(object))
     {
      return false;
     }
   return true;
  }

Класс CBmpButton

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

  • радио-кнопки выбора, в чем будут выражаться стоп-лосс, тейк-профит и риск: в денежном формате или в пунктах (или процентах для риска);
  • чекбоксы для фиксации того, отображать или нет уровни стоп-лосса и тейк-профита на графике;
  • кнопки увеличения и уменьшения объема сделки.

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

#resource "\\Include\\Controls\\res\\RadioButtonOn.bmp"
#resource "\\Include\\Controls\\res\\RadioButtonOff.bmp"
#resource "\\Include\\Controls\\res\\CheckBoxOn.bmp"
#resource "\\Include\\Controls\\res\\CheckBoxOff.bmp"
#resource "\\Include\\Controls\\res\\SpinInc.bmp"
#resource "\\Include\\Controls\\res\\SpinDec.bmp"

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

//+------------------------------------------------------------------+
//| Create BMP Button                                                |
//+------------------------------------------------------------------+
bool CTradePanel::CreateBmpButton(const long chart,const int subwindow,CBmpButton &object,const uint x,const uint y,string BmpON,string BmpOFF,bool lock)
  {
   // All objects must have separate name
   string name=m_name+"BmpButton"+(string)ObjectsTotal(chart,-1,OBJ_BITMAP_LABEL);
   //--- Calculate coordinates
   uint y1=(uint)(y-(Y_STEP-CONTROLS_BUTTON_SIZE)/2);
   uint y2=y1+CONTROLS_BUTTON_SIZE;
   //--- Call Create function
   if(!object.Create(m_chart_id,name,m_subwin,x-CONTROLS_BUTTON_SIZE,y1,x,y2))
      return(false);
   //--- Assign BMP pictures to button status
   if(!object.BmpNames(BmpOFF,BmpON))
      return(false);
   //--- Add object to controls
   if(!Add(object))
      return(false);
   //--- set Lock flag to true
   object.Locking(lock);
//--- succeeded
   return(true);
  }

Прописав функции создания объектов, в обязательном порядке объявим эти функции в блоке "private" нашего класса.

private:

   //--- Create Label object
   bool              CreateLabel(const long chart,const int subwindow,CLabel &object,const string text,const uint x,const uint y,label_align align);
   //--- Create Button
   bool              CreateButton(const long chart,const int subwindow,CButton &object,const string text,const uint x,const uint y,const uint x_size,const uint y_size);
   //--- Create Edit object
   bool              CreateEdit(const long chart,const int subwindow,CEdit &object,const string text,const uint x,const uint y,const uint x_size,const uint y_size);
   //--- Create BMP Button
   bool              CreateBmpButton(const long chart,const int subwindow,CBmpButton &object,const uint x,const uint y,string BmpON,string BmpOFF,bool lock);

3.4. Расставим все элементы по своим местам

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

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

  • расстояние от границы окна до первого элемента управления;
  • расстояние между элементами управления по высоте;
  • высота элемента управления.
   #define  Y_STEP   (int)(ClientAreaHeight()/18/4)      // height step between elements
   #define  Y_WIDTH  (int)(ClientAreaHeight()/18)        // height of element
   #define  BORDER   (int)(ClientAreaHeight()/24)        // distance between border and elements

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

bool CTradePanel::Create(const long chart,const string name,const int subwin=0,const int x1=20,const int y1=20,const int x2=320,const int y2=420)
  {
      // At first call create function of parents class
   if(!CAppDialog::Create(chart,name,subwin,x1,y1,x2,y2))
     {
      return false;
     }
   // Calculate coordinates and size of BID object
   // Coordinates are calculated in dialog box, not in chart
   int l_x_left=BORDER;
   int l_y=BORDER;
   int y_width=Y_WIDTH;
   int y_sptep=Y_STEP;
   // Create object
   if(!CreateLabel(chart,subwin,BID,DoubleToString(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits),l_x_left,l_y,left))
     {
      return false;
     }
   // Adjust font size for object
   if(!BID.FontSize(Y_WIDTH))
     {
      return false;
     }
   // Repeat same functions for other objects
   int l_x_right=ClientAreaWidth()-20;
   if(!CreateLabel(chart,subwin,ASK,DoubleToString(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits),l_x_right,l_y,right))
     {
      return false;
     }
   if(!ASK.FontSize(Y_WIDTH))
     {
      return false;
     }
   l_y+=2*Y_WIDTH;
...................
  }

С полным кодом функции можно ознакомиться в прилагаемом примере.

Результатом наших трудов стала нижеследующая панель.

Панель

Но пока это только макет — красивая картинка на графике. На следующем этапе мы "вдохнём в неё жизнь".

4. "Оживление картинки"

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

4.1. Изменение цены инструмента

При изменении цены инструмента терминалом МТ5 генерируется событие NewTick, которое запускает функцию OnTick() советника. Следовательно, из этой функции мы должны вызвать соответствующую функцию нашего класса, которая будет обрабатывать данное событие. Дадим ей аналогичное имя OnTick() и объявим её в блоке "public", т.к. она будет вызываться из внешней программы.

public:

   virtual void      OnTick(void);
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   TradePanel.OnTick();
  }
Какие же изменения происходят на панели при смене стоимости инструмента? Первое, что мы должны сделать, — это изменить значения Ask и Bid на нашей панели.
//+------------------------------------------------------------------+
//| Event "New Tick"                                                  |
//+------------------------------------------------------------------+
void CTradePanel::OnTick(void)
  {
   //--- Change Ask and Bid prices on panel
   ASK.Text(DoubleToString(SymbolInfoDouble(_Symbol,SYMBOL_ASK),(int)SymbolInfoInteger(_Symbol,SYMBOL_DIGITS)));
   BID.Text(DoubleToString(SymbolInfoDouble(_Symbol,SYMBOL_BID),(int)SymbolInfoInteger(_Symbol,SYMBOL_DIGITS)));

Затем, в случае наличия открытых на счете позиций, изменяем значение суммы средств (Equity) на панели. В качестве перестраховки я добавил проверку соответствия отображаемой на панели суммы фактическим средствам на счете, даже в момент отсутствия открытых позиций. Это позволит отображать реальную сумму средств после "внештатных ситуаций". Таким образом, нам нет необходимости проверять наличие открытых позиций: мы сразу проверяем соответствие текущей суммы средств по счету и отображаемой на панели. В случае необходимости выводим на панель истинное значение.

//--- Сheck and change (if necessary) equity
   if(Equity_value.Text()!=DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY),2)+" "+AccountInfoString(ACCOUNT_CURRENCY))
     {
      Equity_value.Text(DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY),2)+" "+AccountInfoString(ACCOUNT_CURRENCY));
     }
   

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

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

//--- Check and change (if necessary) Buy and Sell lots and profit value.
   if(PositionSelect(_Symbol))
     {
      switch((ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE))
        {
         case POSITION_TYPE_BUY:
           Buy_profit_value.Text(DoubleToString(PositionGetDouble(POSITION_PROFIT),2)+" "+AccountInfoString(ACCOUNT_CURRENCY));
           if(Buy_Lots_value.Text()!=DoubleToString(PositionGetDouble(POSITION_VOLUME),2))
              {
               Buy_Lots_value.Text(DoubleToString(PositionGetDouble(POSITION_VOLUME),2));
              }
           if(Sell_profit_value.Text()!=DoubleToString(0,2)+" "+AccountInfoString(ACCOUNT_CURRENCY))
              {
               Sell_profit_value.Text(DoubleToString(0,2)+" "+AccountInfoString(ACCOUNT_CURRENCY));
              }
           if(Sell_Lots_value.Text()!=DoubleToString(0,2))
              {
               Sell_Lots_value.Text(DoubleToString(0,2));
              }
           break;
         case POSITION_TYPE_SELL:
           Sell_profit_value.Text(DoubleToString(PositionGetDouble(POSITION_PROFIT),2)+" "+AccountInfoString(ACCOUNT_CURRENCY));
           if(Sell_Lots_value.Text()!=DoubleToString(PositionGetDouble(POSITION_VOLUME),2))
              {
               Sell_Lots_value.Text(DoubleToString(PositionGetDouble(POSITION_VOLUME),2));
              }
           if(Buy_profit_value.Text()!=DoubleToString(0,2)+" "+AccountInfoString(ACCOUNT_CURRENCY))
              {
               Buy_profit_value.Text(DoubleToString(0,2)+" "+AccountInfoString(ACCOUNT_CURRENCY));
              }
           if(Buy_Lots_value.Text()!=DoubleToString(0,2))
              {
               Buy_Lots_value.Text(DoubleToString(0,2));
              }
           break;
        }
     }
   else
     {
      if(Buy_Lots_value.Text()!=DoubleToString(0,2))
        {
         Buy_Lots_value.Text(DoubleToString(0,2));
        }
      if(Sell_Lots_value.Text()!=DoubleToString(0,2))
        {
         Sell_Lots_value.Text(DoubleToString(0,2));
        }
      if(Buy_profit_value.Text()!=DoubleToString(0,2)+" "+AccountInfoString(ACCOUNT_CURRENCY))
        {
         Buy_profit_value.Text(DoubleToString(0,2)+" "+AccountInfoString(ACCOUNT_CURRENCY));
        }
      if(Sell_profit_value.Text()!=DoubleToString(0,2)+" "+AccountInfoString(ACCOUNT_CURRENCY))
        {
         Sell_profit_value.Text(DoubleToString(0,2)+" "+AccountInfoString(ACCOUNT_CURRENCY));
        }
     }

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

   //--- Move SL and TP lines if necessary
   if(StopLoss_line.Pressed())
     {
      UpdateSLLines();
     }
   if(TakeProfit_line.Pressed())
     {
      UpdateTPLines();
     }
   return;
  }

4.2. Ввод значений в редактируемые поля.

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

Ввод информации в редактируемые поля — событие изменения графического объекта, которое относится к группе событий ChartEvent. События этой группы обрабатываются функцией OnChartEvent. У нее 4 входных параметра: идентификатор события и 3 параметра, характеризующих событие, относящихся к типам long, double и string. Как и в предыдущем случае, мы создадим обработчик событий в нашем классе и будем вызывать его из функции OnChartEvent с передачей всех входных параметров, характеризующих событие. Забегая немного вперёд, хочется сказать, что этой функцией также будут обрабатываться и события нажатия на кнопки торговой панели. Поэтому данная функция будет диспетчером, который, проанализиров событие, будет вызывать функцию обработки конкретного события. Затем информацию о событии передадим функции родительского класса для выполнения процедур, прописанных в родительском классе.

public:

   virtual bool      OnEvent(const int id,const long &lparam, const double &dparam, const string &sparam);
//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//---
   TradePanel.OnEvent(id, lparam, dparam, sparam);
  }

Для построения такого диспетчера воспользуемся макроподстановками.

//+------------------------------------------------------------------+
//| Event Handling                                                   |
//+------------------------------------------------------------------+
EVENT_MAP_BEGIN(CTradePanel)
   ON_EVENT(ON_END_EDIT,Lots,LotsEndEdit)
   ON_EVENT(ON_END_EDIT,StopLoss_pips,SLPipsEndEdit)
   ON_EVENT(ON_END_EDIT,TakeProfit_pips,TPPipsEndEdit)
   ON_EVENT(ON_END_EDIT,StopLoss_money,SLMoneyEndEdit)
   ON_EVENT(ON_END_EDIT,TakeProfit_money,TPMoneyEndEdit)
   ON_EVENT(ON_END_EDIT,Risk_percent,RiskPercentEndEdit)
   ON_EVENT(ON_END_EDIT,Risk_money,RiskMoneyEndEdit)
EVENT_MAP_END(CAppDialog)

Соответственно, все функции обработки событий мы должны объявить в блоке "private" нашего класса

private:

   //--- On Event functions
   void              LotsEndEdit(void);                              // Edit Lot size
   void              SLPipsEndEdit(void);                            // Edit Stop Loss in pips
   void              TPPipsEndEdit(void);                            // Edit Take Profit in pips
   void              SLMoneyEndEdit(void);                           // Edit Stop Loss in money
   void              TPMoneyEndEdit(void);                           // Edit Take Profit in money
   void              RiskPercentEndEdit(void);                       // Edit Risk in percent
   void              RiskMoneyEndEdit(void);                         // Edit Risk in money

 Для хранения данных, полученных из редактируемых полей, введем дополнительные переменные в блоке "private"

private:

   //--- variables of current values
   double            cur_lot;                         // Lot of next order
   int               cur_sl_pips;                     // Stop Loss in pips
   double            cur_sl_money;                    // Stop Loss in money
   int               cur_tp_pips;                     // Take Profit in pips
   double            cur_tp_money;                    // Take Profit in money
   double            cur_risk_percent;                // Risk in percent
   double            cur_risk_money;                  // Risk in money

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

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

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

//+------------------------------------------------------------------+
//| Read lots value after edit                                       |
//+------------------------------------------------------------------+
void CTradePanel::LotsEndEdit(void)
  {
   //--- Read and normalize lot value
   cur_lot=NormalizeLots(StringToDouble(Lots.Text()));
   //--- Output lot value to panel
   Lots.Text(DoubleToString(cur_lot,2));

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

   //--- Check and modify value of other labels 
   if(StopLoss_money_b.Pressed())
     {
      StopLossPipsByMoney();
     }
   if(TakeProfit_money_b.Pressed())
     {
      TakeProfitPipsByMoney();
     }
   if(StopLoss_pips_b.Pressed())
     {
      StopLossMoneyByPips();
     }
   if(TakeProfit_pips_b.Pressed())
     {
      TakeProfitMoneyByPips();
     }

Когда мы строим инструмент для ежедневной работы пользователя, мы всегда должны помнить о понятии "юзабилити" (удобство использования). И здесь мы должны вспомнить о пункте 8 описанного вначале функционала нашей торговой панели: "Панель должна запоминать, какие параметры внесены трейдером, а какие — автоматически рассчитаны. Это необходимо, чтобы при последующих пересчетах параметры, внесённые трейдером, по возможности не изменялись." Иными словами, в дальнейшем, при изменении стоп-лосса в пунктах, мы должны вспомнить, что трейдер изменял последним — объем сделки или уровень риска. При необходимости, может потребоваться оставить неизменными последние введенные данные.
С этой целью введем в блок "private" переменную RiskByValue, и в функции обработки этого события присвоим ей значение true.

private:
   bool              RiskByValue;                     // Flag: Risk by Value or Value by Risk
   RiskByValue=true;
   return;
  }

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

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

//+------------------------------------------------------------------+
//|  Modify SL money by Order lot and SL pips                        |
//+------------------------------------------------------------------+
void CTradePanel::StopLossMoneyByPips(void)
  {
   //--- Read and normalize lot value
   cur_lot=NormalizeLots(StringToDouble(Lots.Text()));
   //--- Output lot value to panel
   Lots.Text(DoubleToString(cur_lot,2));

2. Вторая составляющая для расчёта денежного значения возможного риска — сумма изменения средств при изменении цены инструмента на один тик при открытой позиции в 1 лот. Для этого получим стоимость одного тика и минимальный размер изменения цены инструмента:

   double tick_value=SymbolInfoDouble(_Symbol,SYMBOL_TRADE_TICK_VALUE);
   double tick_size=SymbolInfoDouble(_Symbol,SYMBOL_TRADE_TICK_SIZE);

3. По полученным данным рассчитаем возможные убытки, и полученное значение выведем на панель в соответствующее поле.

   cur_sl_money=NormalizeDouble(tick_value*cur_lot*(tick_size/_Point)*cur_sl_pips,2);
   StopLoss_money.Text(DoubleToString(cur_sl_money,2));

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

   cur_risk_money=cur_sl_money;    Risk_money.Text(DoubleToString(cur_risk_money,2));    cur_risk_percent=NormalizeDouble(cur_risk_money/AccountInfoDouble(ACCOUNT_BALANCE)*100,2);    Risk_percent.Text(DoubleToString(cur_risk_percent,2));
return;
}

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

Аналогично прописываются функции для корректировки значений тейк-профита.

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

4.3. Обработка событий нажатия радио-кнопок.

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

Что касается технической стороны, то нажатие кнопки относится к группе событий ChartEvent. Следовательно, обработка будет производиться так же, как и редактирование поля. Объявим функции обработки событий в блоке "private":

private:

   //--- On Event functions
   void              SLPipsClick();                                  // Click Stop Loss in pips
   void              TPPipsClick();                                  // Click Take Profit in pips
   void              SLMoneyClick();                                 // Click Stop Loss in money
   void              TPMoneyClick();                                 // Click Take Profit in money
   void              RiskPercentClick();                             // Click Risk in percent
   void              RiskMoneyClick();                               // Click Risk in money

Дополним макроподстановки обработчика событий:

//+------------------------------------------------------------------+
//| Event Handling                                                   |
//+------------------------------------------------------------------+
EVENT_MAP_BEGIN(CTradePanel)
   ON_EVENT(ON_END_EDIT,Lots,LotsEndEdit)
   ON_EVENT(ON_END_EDIT,StopLoss_pips,SLPipsEndEdit)
   ON_EVENT(ON_END_EDIT,TakeProfit_pips,TPPipsEndEdit)
   ON_EVENT(ON_END_EDIT,StopLoss_money,SLMoneyEndEdit)
   ON_EVENT(ON_END_EDIT,TakeProfit_money,TPMoneyEndEdit)
   ON_EVENT(ON_END_EDIT,Risk_percent,RiskPercentEndEdit)
   ON_EVENT(ON_END_EDIT,Risk_money,RiskMoneyEndEdit)
   ON_EVENT(ON_CLICK,StopLoss_pips_b,SLPipsClick)
   ON_EVENT(ON_CLICK,TakeProfit_pips_b,TPPipsClick)
   ON_EVENT(ON_CLICK,StopLoss_money_b,SLMoneyClick)
   ON_EVENT(ON_CLICK,TakeProfit_money_b,TPMoneyClick)
   ON_EVENT(ON_CLICK,Risk_percent_b,RiskPercentClick)
   ON_EVENT(ON_CLICK,Risk_money_b,RiskMoneyClick)
EVENT_MAP_END(CAppDialog)

Сама функция обработки события будет иметь следующий вид:

//+------------------------------------------------------------------+
//| Click Stop Loss in pips                                          |
//+------------------------------------------------------------------+
void CTradePanel::SLPipsClick(void)
  {
   StopLoss_pips_b.Pressed(cur_sl_pips>0);
   StopLoss_money_b.Pressed(false);
   Risk_money_b.Pressed(false);
   Risk_percent_b.Pressed(false);
   return;
  }

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

4.4. Нажатие кнопок изменения объема сделки.

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

Так же, как и ранее, объявим наши функции в блоке private:

private:
................
   //--- On Event functions
................
   void              IncreaseLotClick();                             // Click Increase Lot
   void              DecreaseLotClick();                             // Click Decrease Lot

Дополним функцию обработки прерываний макроподстановками:

//+------------------------------------------------------------------+
//| Event Handling                                                   |
//+------------------------------------------------------------------+
EVENT_MAP_BEGIN(CTradePanel)
   ON_EVENT(ON_END_EDIT,Lots,LotsEndEdit)
   ON_EVENT(ON_END_EDIT,StopLoss_pips,SLPipsEndEdit)
   ON_EVENT(ON_END_EDIT,TakeProfit_pips,TPPipsEndEdit)
   ON_EVENT(ON_END_EDIT,StopLoss_money,SLMoneyEndEdit)
   ON_EVENT(ON_END_EDIT,TakeProfit_money,TPMoneyEndEdit)
   ON_EVENT(ON_END_EDIT,Risk_percent,RiskPercentEndEdit)
   ON_EVENT(ON_END_EDIT,Risk_money,RiskMoneyEndEdit)
   ON_EVENT(ON_CLICK,StopLoss_pips_b,SLPipsClick)
   ON_EVENT(ON_CLICK,TakeProfit_pips_b,TPPipsClick)
   ON_EVENT(ON_CLICK,StopLoss_money_b,SLMoneyClick)
   ON_EVENT(ON_CLICK,TakeProfit_money_b,TPMoneyClick)
   ON_EVENT(ON_CLICK,Risk_percent_b,RiskPercentClick)
   ON_EVENT(ON_CLICK,Risk_money_b,RiskMoneyClick)
   ON_EVENT(ON_CLICK,Increase,IncreaseLotClick)
   ON_EVENT(ON_CLICK,Decrease,DecreaseLotClick)
EVENT_MAP_END(CAppDialog)

Рассмотрим функцию обработки события:

//+------------------------------------------------------------------+
//|  Increase Lot Click                                              |
//+------------------------------------------------------------------+
void CTradePanel::IncreaseLotClick(void)
  {
   //--- Read and normalize lot value
   cur_lot=NormalizeLots(StringToDouble(Lots.Text())+SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP));
   //--- Output lot value to panel
   Lots.Text(DoubleToString(cur_lot,2));
   //--- Call end edit lot function
   LotsEndEdit();
   return;
  }

Вначале мы считываем текущее значение объема сделки и увеличиваем его на шаг из спецификации инструмента. Затем сразу же приводим полученное значение в соответствие со спецификацией инструмента функцией NormalizeLots, которую мы уже встречали ранее.

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

Аналогично строим и функцию уменьшения лота.

4.5. Изменение состояния чекбоксов.

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

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

  1. при каждом нажатии создавать или удалять линии;
  2. создать один раз линии на графике вместе со всеми объектами панели, и при изменении состоянии чек-боксов отображать их или скрывать.

Я предпочел второй вариант. С этой целью подключим еще одну библиотеку:

#include <ChartObjects\ChartObjectsLines.mqh>

Затем в блоке private объявим объекты горизонтальных линий и объявим функцию их инициализации:

private:
.................
   CChartObjectHLine BuySL, SellSL, BuyTP, SellTP;    // Stop Loss and Take Profit Lines
   
   //--- Create Horizontal line
   bool              CreateHLine(long chart, int subwindow,CChartObjectHLine &object,color clr, string comment);

Пропишем процедуру инициализации горизонтальных линий. Вначале создадим линию на графике.

//+------------------------------------------------------------------+
//| Create horizontal line                                           |
//+------------------------------------------------------------------+
bool CTradePanel::CreateHLine(long chart, int subwindow,CChartObjectHLine &object,color clr, string comment)
  {
   // All objects must have separate name
   string name="HLine"+(string)ObjectsTotal(chart,-1,OBJ_HLINE);
   //--- Create horizontal line
   if(!object.Create(chart,name,subwindow,0))
      return false;

Затем зададим цвет, тип линии и добавим комментарий, отображаемый при наведении на объект.

   //--- Set color of line
   if(!object.Color(clr))
      return false;
   //--- Set dash style to line
   if(!object.Style(STYLE_DASH))
      return false;
   //--- Add comment to line
   if(!object.Tooltip(comment))
      return false;

Скроем линию с графика и сделаем фоновым отображение линии.

   //--- Hide line 
   if(!object.Timeframes(OBJ_NO_PERIODS))
      return false;
   //--- Move line to background
   if(!object.Background(true))
      return false;

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

   if(!object.Selectable(true))
      return false;
   return true;
  }

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

//+------------------------------------------------------------------+
//| Create Trade Panel function                                       |
//+------------------------------------------------------------------+
bool CTradePanel::Create(const long chart,const string name,const int subwin=0,const int x1=20,const int y1=20,const int x2=320,const int y2=420)
  {
...................
...................
   //--- Create horizontal lines of SL & TP
   if(!CreateHLine(chart,subwin,BuySL,SL_Line_color,"Buy Stop Loss"))
     {
      return false;
     }
   if(!CreateHLine(chart,subwin,SellSL,SL_Line_color,"Sell Stop Loss"))
     {
      return false;
     }
   if(!CreateHLine(chart,subwin,BuyTP,TP_Line_color,"Buy Take Profit"))
     {
      return false;
     }
   if(!CreateHLine(chart,subwin,SellTP,TP_Line_color,"Sell Take Profit"))
     {
      return false;
     }
    return true;
  }

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

private:
...............
   void              StopLossLineClick();                            // Click StopLoss Line 
   void              TakeProfitLineClick();                          // Click TakeProfit Line

Добавим вызов функции в обработчик событий:

//+------------------------------------------------------------------+
//| Event Handling                                                   |
//+------------------------------------------------------------------+
EVENT_MAP_BEGIN(CTradePanel)
   ON_EVENT(ON_END_EDIT,Lots,LotsEndEdit)
   ON_EVENT(ON_END_EDIT,StopLoss_pips,SLPipsEndEdit)
   ON_EVENT(ON_END_EDIT,TakeProfit_pips,TPPipsEndEdit)
   ON_EVENT(ON_END_EDIT,StopLoss_money,SLMoneyEndEdit)
   ON_EVENT(ON_END_EDIT,TakeProfit_money,TPMoneyEndEdit)
   ON_EVENT(ON_END_EDIT,Risk_percent,RiskPercentEndEdit)
   ON_EVENT(ON_END_EDIT,Risk_money,RiskMoneyEndEdit)
   ON_EVENT(ON_CLICK,StopLoss_pips_b,SLPipsClick)
   ON_EVENT(ON_CLICK,TakeProfit_pips_b,TPPipsClick)
   ON_EVENT(ON_CLICK,StopLoss_money_b,SLMoneyClick)
   ON_EVENT(ON_CLICK,TakeProfit_money_b,TPMoneyClick)
   ON_EVENT(ON_CLICK,Risk_percent_b,RiskPercentClick)
   ON_EVENT(ON_CLICK,Risk_money_b,RiskMoneyClick)
   ON_EVENT(ON_CLICK,Increase,IncreaseLotClick)
   ON_EVENT(ON_CLICK,Decrease,DecreaseLotClick)
   ON_EVENT(ON_CLICK,StopLoss_line,StopLossLineClick)
   ON_EVENT(ON_CLICK,TakeProfit_line,TakeProfitLineClick)
EVENT_MAP_END(CAppDialog)

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

//+------------------------------------------------------------------+
//| Show and Hide Stop Loss Lines                                    |
//+------------------------------------------------------------------+
void CTradePanel::StopLossLineClick()
  {
   if(StopLoss_line.Pressed()) // Button pressed
     {
      if(BuySL.Price(0)<=0)
        {
         UpdateSLLines();
        }
      BuySL.Timeframes(OBJ_ALL_PERIODS);
      SellSL.Timeframes(OBJ_ALL_PERIODS);
     }

Если чекбокс не нажат, то линии скрываются.

   else                         // Button unpressed
     {
      BuySL.Timeframes(OBJ_NO_PERIODS);
      SellSL.Timeframes(OBJ_NO_PERIODS);
     }
   ChartRedraw();
   return;
  }

В конце функции вызываем перерисовывание графика.

4.6. Торговые операции

Теперь, когда описаны функции обработки событий для основных элементов управления на панели, приступим к обработке событий нажатия на кнопки торговых операций. Для проведения торговых операций на счете мы также воспользуемся стандартной библиотекой MQL5 "Trade.mqh", в которой описан класс торговых операций CTrade.

#include <Trade\Trade.mqh>

Объявим класс торговых операций в блоке private:

private:
................
   CTrade            Trade;                           // Class of trade operations

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

 

//+------------------------------------------------------------------+
//| Class initialization function                                    |
//+------------------------------------------------------------------+
CTradePanel::CTradePanel(void)
  {
   Trade.SetExpertMagicNumber(0);
   Trade.SetDeviationInPoints(5);
   int fill=(int)SymbolInfoInteger(_Symbol,SYMBOL_FILLING_MODE);
   Trade.SetTypeFilling((ENUM_ORDER_TYPE_FILLING)(fill==0 ? 2 : fill-1));
   return;
  }

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

После проведения подготовительной работы пропишем функции обработки событий нажатия на кнопки. Вначале, как и ранее, объявим функции в блоке private:

private:
.....................
   void              BuyClick();                                     // Click BUY button
   void              SellClick();                                    // Click SELL button
   void              CloseBuyClick();                                // Click CLOSE BUY button
   void              CloseSellClick();                               // Click CLOSE SELL button
   void              CloseClick();                                   // Click CLOSE ALL button

Затем дополним диспетчер обработки событий новыми функциями:

//+------------------------------------------------------------------+
//| Event Handling                                                   |
//+------------------------------------------------------------------+
EVENT_MAP_BEGIN(CTradePanel)
...................
   ON_EVENT(ON_CLICK,BUY,BuyClick)
   ON_EVENT(ON_CLICK,SELL,SellClick)
   ON_EVENT(ON_CLICK,CloseBuy,CloseBuyClick)
   ON_EVENT(ON_CLICK,CloseSell,CloseSellClick)
   ON_EVENT(ON_CLICK,CloseAll,CloseClick)
EVENT_MAP_END(CAppDialog)

И, конечно же, пропишем непосредственно функции обработки событий. Рассмотрим, к примеру, функцию покупки. Какие же действия должна выполнить наша программа при нажатии на кнопку "BUY"?

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

void CTradePanel::BuyClick(void)
  {
   cur_lot=NormalizeLots(StringToDouble(Lots.Text()));
   Lots.Text(DoubleToString(cur_lot,2));

Следующим шагом получим рыночную цену инструмента и рассчитаем ценовые уровни стоп-лосса и тейк-профита в соответствии с заданными на панели параметрами:

   double price=SymbolInfoDouble(_Symbol,SYMBOL_ASK);
   double SL=(cur_sl_pips>0 ? NormalizeDouble(price-cur_sl_pips*_Point,_Digits) : 0);
   double TP=(cur_tp_pips>0 ? NormalizeDouble(price+cur_tp_pips*_Point,_Digits) : 0);

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

   if(!Trade.Buy(NormalizeLots(cur_lot),_Symbol,price,SL,TP,"Trade Panel"))
      MessageBox("Error of open BUY ORDER "+Trade.ResultComment(),"Trade Panel Error",MB_ICONERROR|MB_OK);;
   return;
  }

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

5. "Ручное" перемещение уровней стоп-лосса и тейк-профита.

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

Для себя я решил не перегружать программу кодом обработки движений мыши на графике и воспользовался стандартной функцией терминала перемещения объектов. С этой целью мы оставили возможность пользователю выделять и передвигать горизонтальные линии. Программой же мы будем обрабатывать событие "CHARTEVENT_OBJECT_DRAG"

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

public:
................
   virtual bool      DragLine(string name);
Вызов этой функции будем производить из функции OnChartEvent основной программы, при наступлении события с передачей наименования объекта.
//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//---
   if(id==CHARTEVENT_OBJECT_DRAG)
     {
      if(TradePanel.DragLine(sparam))
        {
         ChartRedraw();
        }
     }
...........

В самой функции обработке события мы должны:

  • определить, какая именно линия была перемещена (стоп-лосса или тейк-профита);
  • высчитать значение показателя в пунктах;
  • вывести полученное значение в соответствующую ячейку панели;
  • пересчитать значение всех связанных показателей на панели;
  • в случае необходимости изменить значение радио-кнопок.

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

//+------------------------------------------------------------------+
//| Function of moving horizontal lines                              |
//+------------------------------------------------------------------+
bool CTradePanel::DragLine(string name)
  {
   if(name==BuySL.Name())
     {
      StopLoss_pips.Text(DoubleToString(MathAbs(BuySL.Price(0)-SymbolInfoDouble(_Symbol,SYMBOL_ASK))/_Point,0));
      SLPipsEndEdit();
      BuySL.Selected(false);
      return true;
     }
   if(name==SellSL.Name())
     {
      StopLoss_pips.Text(DoubleToString(MathAbs(SellSL.Price(0)-SymbolInfoDouble(_Symbol,SYMBOL_BID))/_Point,0));
      SLPipsEndEdit();
      SellSL.Selected(false);
      return true;
     }
   if(name==BuyTP.Name())
     {
      TakeProfit_pips.Text(DoubleToString(MathAbs(BuyTP.Price(0)-SymbolInfoDouble(_Symbol,SYMBOL_ASK))/_Point,0));
      TPPipsEndEdit();
      BuyTP.Selected(false);
      return true;
     }
   if(name==SellTP.Name())
     {
      TakeProfit_pips.Text(DoubleToString(MathAbs(SellTP.Price(0)-SymbolInfoDouble(_Symbol,SYMBOL_BID))/_Point,0));
      TPPipsEndEdit();
      SellTP.Selected(false);
      return true;
     }
   return false;
  }

6. Сохранение текущих параметров при перезапуске

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

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

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

Не буду далеко вдаваться в наследование классов, но скажу, что начиная с самого прародителя класса CObject, все наследующие классы имеют функции Save и Load. А наш класс CTradePanel унаследовал от своего родительского класса вызов функции сохранения всех включенных объектов при деинициализации класса. Однако здесь нас ждет неприятный сюрприз — классы CEdit и CBmpButton унаследовали "пустые" функции:

   //--- methods for working with files
   virtual bool      Save(const int file_handle)                         { return(true);   }
   virtual bool      Load(const int file_handle)                         { return(true);   }
Следовательно, нам нужно переписать эти функции для объектов, данные которых мы хотим сохранить. С этой целью создадим два новых класса — CEdit_new и CBmpButton_new, которые будут наследниками классов CEdit и CBmpButton, соответственно. В них пропишем функции сохранения и чтения данных.
class CEdit_new : public CEdit
  {
public:
                     CEdit_new(void){};
                    ~CEdit_new(void){};
   virtual bool      Save(const int file_handle)
     {
      if(file_handle==INVALID_HANDLE)
        {
         return false;
        }
      string text=Text();
      FileWriteInteger(file_handle,StringLen(text));
      return(FileWriteString(file_handle,text)>0); 
     }
   virtual bool      Load(const int file_handle)
     {
      if(file_handle==INVALID_HANDLE)
        {
         return false;
        }
      int size=FileReadInteger(file_handle);
      string text=FileReadString(file_handle,size);
      return(Text(text));
     }
   
  };

class CBmpButton_new : public CBmpButton
  {
public:
                     CBmpButton_new(void){};
                    ~CBmpButton_new(void){};
   virtual bool      Save(const int file_handle)
    {
     if(file_handle==INVALID_HANDLE)
        {
         return false;
        }
      return(FileWriteInteger(file_handle,Pressed()));
     }
   virtual bool      Load(const int file_handle)
     {
      if(file_handle==INVALID_HANDLE)
        {
         return false;
        }
      return(Pressed((bool)FileReadInteger(file_handle)));
     }
  };

И, конечно же, поменяем типы сохраняемых объектов на новые.

   CEdit_new         Lots;                            // Display volume of next order
   CEdit_new         StopLoss_pips;                   // Display Stop loss in pips
   CEdit_new         StopLoss_money;                  // Display Stop loss in account currency
   CEdit_new         TakeProfit_pips;                 // Display Take profit in pips
   CEdit_new         TakeProfit_money;                // Display Take profit in account currency
   CEdit_new         Risk_percent;                    // Display Risk percent to equity
   CEdit_new         Risk_money;                      // Display Risk in account currency
   CBmpButton_new    StopLoss_line;                   // Check to display StopLoss Line
   CBmpButton_new    TakeProfit_line;                 // Check to display TakeProfit Line
   CBmpButton_new    StopLoss_pips_b;                 // Select Stop loss in pips
   CBmpButton_new    StopLoss_money_b;                // Select Stop loss in account currency
   CBmpButton_new    TakeProfit_pips_b;               // Select Take profit in pips
   CBmpButton_new    TakeProfit_money_b;              // Select Take profit in account currency
   CBmpButton_new    Risk_percent_b;                  // Select Risk percent to equity
   CBmpButton_new    Risk_money_b;                    // Select Risk in account currency

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

public:
.................
   virtual bool      Run(void);

Сначала прочитаем сохраненные данные:

//+------------------------------------------------------------------+
//| Run of Trade Panel                                               |
//+------------------------------------------------------------------+
bool CTradePanel::Run(void)
  {
   IniFileLoad();

Затем обновим переменные значений:

   cur_lot=StringToDouble(Lots.Text());
   cur_sl_pips=(int)StringToInteger(StopLoss_pips.Text());     // Stop Loss in pips
   cur_sl_money=StringToDouble(StopLoss_money.Text());         // Stop Loss in money
   cur_tp_pips=(int)StringToInteger(TakeProfit_pips.Text());   // Take Profit in pips
   cur_tp_money=StringToDouble(TakeProfit_money.Text());       // Take Profit in money
   cur_risk_percent=StringToDouble(Risk_percent.Text());       // Risk in percent
   cur_risk_money=StringToDouble(Risk_money.Text());           // Risk in money
   RiskByValue=true;
И, наконец, вызовем функции обработки нажатий на чекбоксы, которые актуализируют состояния уровней стоп-лоссов и тейк-профитов:
   StopLossLineClick();
   TakeProfitLineClick();
   return(CAppDialog::Run());
  }

7. "Генеральная уборка"

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

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

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   TradePanel.Destroy(reason);
   return;
  }

Эту функцию мы должны объявить в блоке public нашего класса:

public:
.............
   virtual void      Destroy(const int reason);
В теле этой функции мы удалим горизонтальные линии с графика и вызовем функцию деинициализации родительского класса, которая сохранит всю необходимую информацию и удалит объекты торговой панели с графика.
//+------------------------------------------------------------------+
//| Application deinitialization function                            |
//+------------------------------------------------------------------+
void CTradePanel::Destroy(const int reason)
  {
   BuySL.Delete();
   SellSL.Delete();
   BuyTP.Delete();
   SellTP.Delete();
   CAppDialog::Destroy(reason);
   return;
  }

Заключение

Уважаемые читатели, коллеги и друзья!

Я искренне надеюсь, что вы дочитали мою статью до конца, и мне бы хотелось, чтобы она была вам полезна.

Здесь я попытался доступным языком рассказать об опыте создания торговых панелей и дать вам готовый инструмент для работы на рынке.

Прошу вас, присылайте свои идеи и пожелания, что бы вы еще хотели увидеть на нашей торговой панели. В свою очередь, я обещаю воплотить в жизнь наиболее интересные идеи и рассказать об этом в будущих статьях.
Прикрепленные файлы |
tradepanel.ex5 (319.32 KB)
tradepanel.mq5 (56.85 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (44)
Dmitriy Gizlyk
Dmitriy Gizlyk | 8 янв. 2020 в 10:59
Dmitriy Tyunin:

Добрый день!

Как можно ограничить перемещение панели за границы графика?

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


   

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

Jezuz
Jezuz | 2 мар. 2020 в 07:04
Здравствуйте. Подскажите, можно ли как-то сделать, чтобы вновь созданные объекты не перекрывали панель? Например линии, прямоугольники и т.д. всё это накладывается поверх панели.. Каждый раз сворачивать и разворачивать панель неудобно.
dd
dd | 10 нояб. 2020 в 13:45

Спасибо за статью и за пример.

У меня ваша панель при каком то наборе индикаторов - становится прозрачной почему то.

Sergei Poliukhov
Sergei Poliukhov | 17 июл. 2022 в 22:08
Подскажите пожалуйста, а что надо сделать чтобы данная панель могла открывать сделки в тестере стратегий?
Gladiator WXT
Gladiator WXT | 26 авг. 2022 в 16:18
Sergei Poliukhov #:
Подскажите пожалуйста, а что надо сделать чтобы данная панель могла открывать сделки в тестере стратегий?

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

Ссылка: https://www.mql5.com/ru/forum/431283


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

Графические интерфейсы VII: Элементы "Таблицы" (Глава 1) Графические интерфейсы VII: Элементы "Таблицы" (Глава 1)
В седьмой части серии статей о графических интерфейсах в терминалах MetaTrader будут представлены три типа таблиц: таблица из текстовых меток, таблица из полей ввода и нарисованная таблица. Ещё один важный и часто используемый элемент управления — вкладки, с помощью которых можно скрывать и делать видимыми группы других элементов управления, что позволяет пользователю делать компактные графические интерфейсы в своих MQL-приложениях.
Графические интерфейсы VI: Элементы "Слайдер" и "Двухсторонний слайдер" (Глава 2) Графические интерфейсы VI: Элементы "Слайдер" и "Двухсторонний слайдер" (Глава 2)
В предыдущей статье разрабатываемая библиотека была пополнена сразу четырьмя довольно часто используемыми в графических интерфейсах элементами управления: «чекбокс», «поле ввода», «поле ввода с чекбоксом» и «комбобокс с чекбоксом». Вторая глава шестой части серии будет посвящена таким элементам управления, как слайдер и двухсторонний слайдер.
Как копировать сигналы с помощью советника по своим правилам? Как копировать сигналы с помощью советника по своим правилам?
При подписке на сигналы может возникнуть такая ситуация: у Вашего торгового счёта кредитное плечо 1:100, провайдер имеет кредитное плечо 1:500 и торгует минимальным лотом, а Ваши торговые балансы практически равны — при этом коэффициент копирования будет от 10% до 15%. Эта статья расскажет, как в таком случае увеличить коэффициент копирования.
Регулярные выражения для трейдеров Регулярные выражения для трейдеров
Регулярные выражения (англ. regular expressions) — специальный язык для обработки текстов по заданному правилу, которое также называют шаблоном или маской регулярного выражения. В этой статье мы покажем, как обработать торговый отчет с помощью библиотеки RegularExpressions для MQL5, а также продемонстрируем результаты оптимизации с ее использованием.