Создаем помощника в ручной торговле
Введение
В этой статье я представлю очередной пример создания с нуля полноценной торговой панели, которая поможет в работе на Forex приверженцам ручного трейдинга.
1. Определяем необходимый функционал для торговой панели
Вначале нам нужно определить для себя, какой конечный результат мы хотим получить. Нам предстоит решить, какого функционала мы ждем от нашей панели и какой дизайн будет для нас удобен. В данной статье я предлагаю своё видение торговой панели, но с удовольствием приму ваши предложения. Надеюсь, в новых моих статьях они будут подробно рассмотрены.
Итак, наша панель безусловно должна включать следующие элементы.
- Кнопки покупки и продажи.
- Кнопки закрытия всех позиций по символу и счету или в отдельном направлении (ордера на покупку или продажу).
- Возможность указания уровней стоп-лосса и тейк-профита как в пунктах, так и в валюте депозита (при вводе одного параметра другой должен автоматически корректироваться).
- Панель должна автоматически рассчитывать уровни стоп-лосса и тейк-профита по задаваемым вручную параметрам (п.2) и отображать их на графике.
- У трейдера должна быть возможность перемещения уровней стоп-лосса и/или тейк-профита на графике. При этом все изменения должны отражаться на панели с изменением соответствующих значений.
- Панель должна рассчитывать предстоящий объём сделки по заданным параметрам риска (в валюте депозита или в процентах от текущего баланса).
- Трейдер должен иметь возможность самостоятельно задать объём сделки. При этом зависящие от него соответствующие параметры должны пересчитываться автоматически.
- Панель должна запоминать, какие параметры внесены трейдером, а какие — автоматически рассчитаны. Это необходимо, чтобы при последующих пересчетах параметры, внесённые трейдером, по возможности не изменялись.
- Панель должна сохранять все внесённые параметры, чтобы при перезагрузках не вносить их повторно.
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(); }
//+------------------------------------------------------------------+ //| 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. Изменение состояния чекбоксов.
На следующем этапе построим обработчик событий реагирования на нажатие чекбоксов. На нашей панели есть два чекбокса для включения и выключения отображения уровней стоп-лосса и тейк-профита на графике.
Что же должно происходить при изменении состояния чекбокса? В принципе, основной функцией данного события должно быть отображение линий на графике. Эту задачу можно решить двумя способами:
- при каждом нажатии создавать или удалять линии;
- создать один раз линии на графике вместе со всеми объектами панели, и при изменении состоянии чек-боксов отображать их или скрывать.
Я предпочел второй вариант. С этой целью подключим еще одну библиотеку:
#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; }
Заключение
Уважаемые читатели, коллеги и друзья!
Я искренне надеюсь, что вы дочитали мою статью до конца, и мне бы хотелось, чтобы она была вам полезна.
Здесь я попытался доступным языком рассказать об опыте создания торговых панелей и дать вам готовый инструмент для работы на рынке.
Прошу вас, присылайте свои идеи и пожелания, что бы вы еще хотели увидеть на нашей торговой панели. В свою очередь, я обещаю воплотить в жизнь наиболее интересные идеи и рассказать об этом в будущих статьях.- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Добрый день!
Как можно ограничить перемещение панели за границы графика?
Например, если панель размещена у правого края графика, то при включении панели навигатора, панель "выезжает" за видимую область. Пример на скриншотах:
В блоке перемещения панели, контроль реализован. Вы не сможете переместить панель за график. Проблема в том, что контроль работает только при перемещении. В Вашем примере, панель оказывается за графиком при изменении размера графика. Поэтому, нужно добавить отработку события изменения размеров графика.
Спасибо за статью и за пример.
У меня ваша панель при каком то наборе индикаторов - становится прозрачной почему то.
Подскажите пожалуйста, а что надо сделать чтобы данная панель могла открывать сделки в тестере стратегий?
Сейчас это невозможно из-за того что данная панель опирается на систему событий ChartEvents, а данная система не поддерживается внутри Тестера Стратегий. Недавно я внес открытое предложение команде MQ провести этой осенью се резную работу по улучшению функционала Тестера, там как раз это пункт 1.
Ссылка: https://www.mql5.com/ru/forum/431283
Так что поддерживайте тему необходимости модернизации Тестера Стратеги доп. постами, и в новых релизах все будет работать.