在本文中,我提供了另一个从头创建完整功能的交易面板的实例,用于为人工交易外汇的交易者提供帮助。
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; //+------------------------------------------------------------------+ //| EA 初始化函数 | //+------------------------------------------------------------------+ int OnInit() { //--- // 创建交易面板 if(!TradePanel.Create(ChartID(),"Trade Panel",0,20,20,320,420)) { return (INIT_FAILED); } // 运行交易面板 TradePanel.Run(); //--- return(INIT_SUCCEEDED); }
通过这样相对简单的操作,我们就获得了未来面板的模板。
3.2. 声明全部所需对象
现在我们将在我们的模板上使用全部所需的控件,为此需要创建控件中的每个相关元件类的对象,我们将使用标准类 CLabel, CEdit, CButton 和 CBmpButton 类来创建对象,
我们加上所需的包含文件并为 CTradePanel 类创建一个 Creat() 函数:
#include <Controls\Dialog.mqh> #include <Controls\Label.mqh> #include <Controls\Button.mqh>
"Edit.mqh" 和 "BmpButton.mqh" 文件有意没有包含在内,因为它们会在"Dialog.mqh"中被调用。
在为面板上的对象声明变量时,在 CTradePanel 类中需要以下步骤,在此,我们也声明了 Creat(..) 方法来管理所有的元件,请注意: 变量的声明以及 CTradePanel 类的其他操作的声明都在类的 "private" 部分,但是,在类以外调用的函数,例如 Creat(...), 是在 "public" 部分声明的。
class CTradePanel : public CAppDialog { private: CLabel ASK, BID; // 显示买入和卖出价格 CLabel Balance_label; // 显示 "账户余额" 标签 CLabel Balance_value; // 显示账户余额 CLabel Equity_label; // 显示 "账户净值" 标签 CLabel Equity_value; // 显示账户净值 CLabel PIPs; // 显示 "点数" 标签 CLabel Currency; // 显示账户币别 CLabel ShowLevels; // 显示"显示"标签 CLabel StopLoss; // 显示 "止损" 标签 CLabel TakeProfit; // 显示"获利"标签 CLabel Risk; // 显示"风险"标签 CLabel Equity; // 显示 "净值百分比"标签 CLabel Currency2; // 显示账户币别 CLabel Orders; // 显示 "开启的订单" 标签 CLabel Buy_Lots_label; // 显示"买入手数"标签 CLabel Buy_Lots_value; // 显示买入手数值 CLabel Sell_Lots_label; // 显示 "卖出手数" 标签 CLabel Sell_Lots_value; // 显示卖出手数数值 CLabel Buy_profit_label; // 显示 "买入利润" 标签 CLabel Buy_profit_value; // 显示买入利润数值 CLabel Sell_profit_label; // 显示 "卖出利润" 数值 CLabel Sell_profit_value; // 显示卖出利润数值 CEdit Lots; // 显示下一个订单的交易量 CEdit StopLoss_pips; // 显示止损点数 CEdit StopLoss_money; // 显示止损资金数 CEdit TakeProfit_pips; // 显示获利点数 CEdit TakeProfit_money; // 显示获利资金数 CEdit Risk_percent; // 显示风险占净值百分比 CEdit Risk_money; // 显示风险资金数 CBmpButton StopLoss_line; // 选中以显示止损线 CBmpButton TakeProfit_line; // 选中以显示获利线 CBmpButton StopLoss_pips_b; // 选择以点数止损 CBmpButton StopLoss_money_b; // 选择以资金数止损 CBmpButton TakeProfit_pips_b; // 选择以点数获利 CBmpButton TakeProfit_money_b; // 选择以资金数获利 CBmpButton Risk_percent_b; // 选择以净值百分比为风险参数 CBmpButton Risk_money_b; // 选择以账户资金数为风险参数 CBmpButton Increase,Decrease; // 增加和减小按钮 CButton SELL,BUY; // 卖出和买入按钮 CButton CloseSell,CloseBuy,CloseAll; // 关闭按钮 public: CTradePanel(void){}; ~CTradePanel(void){}; //--- 创建函数 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. 创建一组对象的初始化过程
现在是时候实现 Creat(...) 函数的具体功能了,请注意,我们应该在这个函数中初始化所有以上声明的对象。很容易就可以计算出我们已经声明了4类共45个对象,所以,我们可以设置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) { // 所有对象必须使用独立的名称 string name=m_name+"Label"+(string)ObjectsTotal(chart,-1,OBJ_LABEL); //--- 调用创建函数 if(!object.Create(chart,name,subwindow,x,y,0,0)) { return false; } //--- 设置文字 if(!object.Text(text)) { return false; } //--- 把文字在对话框网格中对齐 ObjectSetInteger(chart,object.Name(),OBJPROP_ANCHOR,(align==left ? ANCHOR_LEFT_UPPER : (align==right ? ANCHOR_RIGHT_UPPER : ANCHOR_UPPER))); //--- 把对象加到控件中 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) { // 所有对象必须使用独立的名称 string name=m_name+"Button"+(string)ObjectsTotal(chart,-1,OBJ_BUTTON); //--- 调用创建函数 if(!object.Create(chart,name,subwindow,x,y,x+x_size,y+y_size)) { return false; } //--- 设置文字 if(!object.Text(text)) { return false; } //--- 设置按钮的解锁标志 object.Locking(false); //--- 把按钮标志设为抬起 if(!object.Pressed(false)) { return false; } //--- 把对象加到控件中 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) { // 所有对象必须使用独立的名称 string name=m_name+"Edit"+(string)ObjectsTotal(chart,-1,OBJ_EDIT); //--- 调用创建函数 if(!object.Create(chart,name,subwindow,x,y,x+x_size,y+y_size)) { return false; } //--- 设置文字 if(!object.Text(text)) { return false; } //--- 在编辑框中对齐文字 if(!object.TextAlign(ALIGN_CENTER)) { return false; } //--- 把只读标志设为 false if(!object.ReadOnly(false)) { return false; } //--- 把对象加到控件中 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"
还需要注意的是这个类的所有元件,除了增加和减少手数的按钮,其他都要维护它们的"按下"或者"抬起"的状态。所以,我们会在初始化函数中增加额外的参数。
//+------------------------------------------------------------------+ //| 创建图形(BMP)按钮 | //+------------------------------------------------------------------+ bool CTradePanel::CreateBmpButton(const long chart,const int subwindow,CBmpButton &object,const uint x,const uint y,string BmpON,string BmpOFF,bool lock) { // 所有对象必须使用独立的名称 string name=m_name+"BmpButton"+(string)ObjectsTotal(chart,-1,OBJ_BITMAP_LABEL); //--- 计算坐标 uint y1=(uint)(y-(Y_STEP-CONTROLS_BUTTON_SIZE)/2); uint y2=y1+CONTROLS_BUTTON_SIZE; //--- 调用创建函数 if(!object.Create(m_chart_id,name,m_subwin,x-CONTROLS_BUTTON_SIZE,y1,x,y2)) return(false); //--- 把 BMP 图片复制到按钮状态 if(!object.BmpNames(BmpOFF,BmpON)) return(false); //--- 把对象加到控件中 if(!Add(object)) return(false); //--- 把锁定标志设为 true object.Locking(lock); //--- 成功 return(true); }
在设定创建对象的函数时,这些函数必须在我们类的"private(私有)"部分声明。
private: //--- 创建标签对象 bool CreateLabel(const long chart,const int subwindow,CLabel &object,const string text,const uint x,const uint y,label_align align); //--- 创建按钮 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); //--- 创建编辑框对象 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); //--- 创建图形按钮 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) // 元件间高度的步长 #define Y_WIDTH (int)(ClientAreaHeight()/18) // 元件的高度 #define BORDER (int)(ClientAreaHeight()/24) // 元件距离边框的距离
通过这种方式,我们就能计算第一个控件的坐标以及所有每个控件相对之前控件的坐标了。
另外,通过定义我们面板的最佳大小,我们就能把它们作为传给函数参数的默认值。
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) { // 首先调用父类的创建函数 if(!CAppDialog::Create(chart,name,subwin,x1,y1,x2,y2)) { return false; } // 计算 BID(卖出价) 对象的大小和坐标 // 坐标计算是相对对话框的,而不是在图表上 int l_x_left=BORDER; int l_y=BORDER; int y_width=Y_WIDTH; int y_sptep=Y_STEP; // 创建对象 if(!CreateLabel(chart,subwin,BID,DoubleToString(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits),l_x_left,l_y,left)) { return false; } // 调整对象的字体大小 if(!BID.FontSize(Y_WIDTH)) { return false; } // 为其他对象重复执行相同函数 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. 修改工具的价格
当在MT5终端中修改工具的价格时,在EA交易的 OnTick() 函数中会生成 NewTick 事件,所以,我们应该在处理该事件的这个函数中调用我们类的相关方法,我们将给他起个相似的名字 - OnTick(), 并在"public(公有)"部分声明它,因为它会被外部程序调用。
public: virtual void OnTick(void);
//+------------------------------------------------------------------+ //| EA 订单处理函数 | //+------------------------------------------------------------------+ void OnTick() { //--- TradePanel.OnTick(); }
如果交易价格有变化时,面板上会如何变化呢? 我们首先要做的就是修改我们面板上的买入和卖出价格。
//+------------------------------------------------------------------+ //| New Tick 事件 | //+------------------------------------------------------------------+ void CTradePanel::OnTick(void) { //--- 修改面板上的买入和卖出价格 ASK.Text(DoubleToString(SymbolInfoDouble(_Symbol,SYMBOL_ASK),(int)SymbolInfoInteger(_Symbol,SYMBOL_DIGITS))); BID.Text(DoubleToString(SymbolInfoDouble(_Symbol,SYMBOL_BID),(int)SymbolInfoInteger(_Symbol,SYMBOL_DIGITS)));
然后,如果有持有的仓位,我们就修改面板上的净值。为预防起见,就算没有持有仓位的时候,我也已经在显示净值的部分加了一幅图。这使我们在"紧急情况"下仍能显示实际的资金。通过这种方式,就不需要检查是否有持有的仓位了:我们直接检查账户的当前净值,并在面板上显示数字,如有必要,我们将在面板上显示真实数值。
//--- 检查和修改 (如有必要)净值 if(Equity_value.Text()!=DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY),2)+" "+AccountInfoString(ACCOUNT_CURRENCY)) { Equity_value.Text(DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY),2)+" "+AccountInfoString(ACCOUNT_CURRENCY)); }
显示余额时使用类似的方法,
我想肯定有这样的问题: "为什么在每一时刻都检查余额呢,它只是在进行交易操作时才会改变啊?" 是的,确实如此,晚些时候我们会讨论如何回应交易事件。但是,还是有小的可能性,当我们的面板没有载入时,终端与服务器间还没有连接。我已经把在任何时候都面板上显示真实余额的操作加上了,即使是在紧急状况之下。
当价格变化时,下一步就是基于当前资产检查是否有开启的仓位,如果有的话,我们就检查和修改在买入和卖出栏位中仓位的交易量和当前利润。
//--- 检查和修改 (如有必要) 买入和卖出的手数以及利润. 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)); } }
另外,我们不应忘记检查用于在图表上显示止损和获利水平的复选框的状态,如有必要,我们需要修改线的位置。需要在代码中加入对这些函数的调用,以下提供了进一步的信息。
//--- 如有必要,移动止损或者获利线 if(StopLoss_line.Pressed()) { UpdateSLLines(); } if(TakeProfit_line.Pressed()) { UpdateTPLines(); } return; }
4.2. 把数值输入到可编辑栏位中
在我们的面板上有很多可编辑栏位,所以,我们必然需要接收和处理输入的信息。
在可编辑栏位中输入信息,是图形对象改变的事件,属于 ChartEvent 组,这组事件是由 OnChartEvent 函数处理的。它有四个输入参数: 事件标识符,以及三个事件的特殊参数,分别属于long(长整数型), double(双精度浮点数型)以及 string(字符串)类型。与之前的情况相同,我们将在我们的类中创建事件处理函数并在 OnChartEvent 函数中调用,传入所有事件相关的输入参数。再进一步,我想说的是,这个函数中也应当处理按下交易面板按钮的事件,所以,这个函数就像转发器一样,在分析事件之后,再调用特别的事件处理函数,有关事件的信息将传到父类的函数中,执行父类中指定的处理过程。
public: virtual bool OnEvent(const int id,const long &lparam, const double &dparam, const string &sparam); //+------------------------------------------------------------------+ //| ChartEvent 函数 | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { //--- TradePanel.OnEvent(id, lparam, dparam, sparam); }
在创建这样的转发器时会使用宏替换。
//+------------------------------------------------------------------+ //| 事件的处理 | //+------------------------------------------------------------------+ 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 函数 void LotsEndEdit(void); // 编辑手数 void SLPipsEndEdit(void); // 编辑止损点数 void TPPipsEndEdit(void); // 编辑获利点数 void SLMoneyEndEdit(void); // 编辑止损资金数 void TPMoneyEndEdit(void); // 编辑获利资金数 void RiskPercentEndEdit(void); // 编辑风险百分比 void RiskMoneyEndEdit(void); // 编辑风险资金数
为了从可编辑栏位获取数据并保存,我们在"private"部分添加了额外的变量。
private: //--- 当前数值的变量 double cur_lot; // 下一个订单的手数 int cur_sl_pips; // 止损点数 double cur_sl_money; // 止损资金数 int cur_tp_pips; // 获利点数 double cur_tp_money; // 获利资金数 double cur_risk_percent; // 风险百分比 double cur_risk_money; // 风险资金数
让我们分析一个指定事件作为例子 — 输入所准备交易的交易量。我想提醒您,在栏位中输入信息时,不论其内容如何,都是以文字方式处理的,事实上,当在栏位中输入文字时,会生成很多事件: 鼠标指针掠过对象,按下鼠标按键,开始编辑栏位,点击键盘按键,结束栏位的编辑,等等。我们只对最后的事件感兴趣,那时输入信息的过程已经结束了。所以,对函数的调用应该基于 "ON_END_EDIT" 事件。
我们在事件处理函数中首先要做的是,读取输入的文字并把它转换为双精度浮点数类型的数值,
然后,我们必须把取得的数值"规范化",也就是根据交易品种的条件进行设置(一个订单的最小或者最大交易量,以及交易量改动的步长等)。为了执行这个操作,我们将会写一个独立的函数,因为它在按下增加和减少交易量的按钮时也需要。取得的数值应当被返回给面板,以通知交易者未来交易的交易量。
//+------------------------------------------------------------------+ //| 在编辑后读取手数数值 | //+------------------------------------------------------------------+ void CTradePanel::LotsEndEdit(void) { //--- 读取并规范化手数数值 cur_lot=NormalizeLots(StringToDouble(Lots.Text())); //--- 把手数数值输出到面板 Lots.Text(DoubleToString(cur_lot,2));
除此以外,根据单选框的当前设置,我们还将必须重新计算和修改面板上其他剩余的可编辑栏位。这是必须的,因为当有交易因为止损(假如是以点数指定止损)或者止损水平点数而关闭时,交易量就会有变化,因而在止损后风险水平也将变化。对于获利值也有相同的情形,当然,这些操作都将通过对应的函数来执行。
//--- 检查和修改其它标签的数值 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; // 标志: 根据数值计算风险或根据风险计算数值 RiskByValue=true; return; }
我们将基于 StopLossMoneyByPips 函数探讨组织修改相联系的可编辑栏位函数的原则,因为这更容易从功能上加以理解。
1. 实际上,这个函数在三种情况下会被调用:当修改手数大小时,在止损点数栏位输入数值以及移动止损线时。所以,我们要做的第一件事就是 — 检查将要交易的当前交易量。如果它与交易品种的规格或者市场要求不相符合,在面板上显示的数值就要被修改。
//+------------------------------------------------------------------+ //| 根据订单手数和止损点数修改止损资金 | //+------------------------------------------------------------------+ void CTradePanel::StopLossMoneyByPips(void) { //--- 读取并规范化手数数值 cur_lot=NormalizeLots(StringToDouble(Lots.Text())); //--- 把手数数值输出到面板 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 函数 void SLPipsClick(); // 点击了止损点数 void TPPipsClick(); // 点击了获利点数 void SLMoneyClick(); // 点击了止损资金 void TPMoneyClick(); // 点击了获利资金 void RiskPercentClick(); // 点击了风险百分率 void RiskMoneyClick(); // 点击了风险资金
我们在事件处理函数中加入了宏替换:
//+------------------------------------------------------------------+ //| 事件的处理 | //+------------------------------------------------------------------+ 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)