创建一个人工交易助手

Dmitriy Gizlyk | 25 七月, 2016


简介

在本文中,我提供了另一个从头创建完整功能的交易面板的实例,用于为人工交易外汇的交易者提供帮助。

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;
//+------------------------------------------------------------------+
//| 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)

处理事件的函数看起来如下:

//+------------------------------------------------------------------+
//| 点击了止损点数                           |
//+------------------------------------------------------------------+
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 函数
................
   void              IncreaseLotClick();                             // 点击增加手数
   void              DecreaseLotClick();                             // 点击减少手数


我们使用宏替换来增加处理函数:

//+------------------------------------------------------------------+
//| 事件的处理                              |
//+------------------------------------------------------------------+
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)

让我们查看事件处理函数:

//+------------------------------------------------------------------+
//|  点击增加手数                           |
//+------------------------------------------------------------------+
void CTradePanel::IncreaseLotClick(void)
  {
   //--- 读取并规范化手数数值
   cur_lot=NormalizeLots(StringToDouble(Lots.Text())+SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP));
   //--- 把手数数值输出到面板
   Lots.Text(DoubleToString(cur_lot,2));
   //--- 调用结束编辑手数函数
   LotsEndEdit();
   return;
  }

首先我们读取交易手数的当前数值,并使用交易品种规格中的步长增加手数,然后,我们直接把获取的数值根据交易品种的规格通过我们已经用过的NormalizeLots函数进行处理,

另外,我们调用在输入窗口中处理手数变化的函数,因为我们之前已经在这个函数中实现了所有所需的过程。

减小手数的函数实现起来是一样的。

4.5. 修改复选框的状态。

在以下的步骤中,我们将创建用于回应按下复选框的事件处理函数,我们的面板上有两个复选框,用于切换止损和获利在图表上显示的开关,

当复选框的状态改变时应该发生什么呢?实际上,这个事件的主要功能就是显示图表线,有两种方法来解决这个问题:

  1. 在您按下按钮时,每次都创建和删除线;
  2. 在面板上只创建一次线以及所有其他对象,当复选框状态改变时显示或隐藏它们。

我选择的是第二个选项。为此,要多连接一个库:

#include <ChartObjects\ChartObjectsLines.mqh>

然后,我们在私有部分声明水平线的对象以及它们的初始化方法:

private:
.................
   CChartObjectHLine BuySL, SellSL, BuyTP, SellTP;    // 止损与获利线
   
   //--- 创建水平线
   bool              CreateHLine(long chart, int subwindow,CChartObjectHLine &object,color clr, string comment);

让我们实现初始化水平线的过程,首先,我们在图表上创建一条线。

//+------------------------------------------------------------------+
//| 创建水平线                              |
//+------------------------------------------------------------------+
bool CTradePanel::CreateHLine(long chart, int subwindow,CChartObjectHLine &object,color clr, string comment)
  {
   // 所有对象必须使用独立的名称
   string name="HLine"+(string)ObjectsTotal(chart,-1,OBJ_HLINE);
   //--- 创建水平线
   if(!object.Create(chart,name,subwindow,0))
      return false;

然后,我们设置颜色,线的类型,并添加注释,当鼠标掠过对象时会显示。

   //--- 设置线的颜色
   if(!object.Color(clr))
      return false;
   //--- 设置点划线
   if(!object.Style(STYLE_DASH))
      return false;
   //--- 给线加上注释
   if(!object.Tooltip(comment))
      return false;

我们把线从图表上隐藏并把线移到背景上。

   //--- 隐藏线 
   if(!object.Timeframes(OBJ_NO_PERIODS))
      return false;
   //--- 把线移动到背景上
   if(!object.Background(true))
      return false;


因为面板的一个选项就是是否允许交易者在图表上移动止损和获利线,我们将启用突出显示的线:

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

现在,我们在创建我们交易面板的函数中加上线的初始化。

//+------------------------------------------------------------------+
//| 创建交易面板的函数                          |
//+------------------------------------------------------------------+
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(!CreateHLine(chart,subwin,BuySL,SL_Line_color,"买入止损"))
     {
      return false;
     }
   if(!CreateHLine(chart,subwin,SellSL,SL_Line_color,"卖出止损"))
     {
      return false;
     }
   if(!CreateHLine(chart,subwin,BuyTP,TP_Line_color,"买入获利"))
     {
      return false;
     }
   if(!CreateHLine(chart,subwin,SellTP,TP_Line_color,"卖出获利"))
     {
      return false;
     }
    return true;
  }


在我们创建线之后,我们将实现事件处理函数,它将根据处理之前事件的函数中使用的相同架构来构建,我们在私有部分声明事件处理函数:

private:
...............
   void              StopLossLineClick();                            // 点击止损线 
   void              TakeProfitLineClick();                          // 点击获利线

在事件处理函数种调用这些函数:

//+------------------------------------------------------------------+
//| 事件的处理                              |
//+------------------------------------------------------------------+
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)

并且,最终我们开发处理事件的函数。在函数的开始,我们检测复选框的状态,根据它将会有进一步的行为。如果它是按下的,显示的水平在显示线之前应该更新,然后我们再把线应用到图表。

//+------------------------------------------------------------------+
//| 显示和隐藏止损线                           |
//+------------------------------------------------------------------+
void CTradePanel::StopLossLineClick()
  {
   if(StopLoss_line.Pressed()) // 按下了按钮
     {
      if(BuySL.Price(0)<=0)
        {
         UpdateSLLines();
        }
      BuySL.Timeframes(OBJ_ALL_PERIODS);
      SellSL.Timeframes(OBJ_ALL_PERIODS);
     }

如果复选框没有被按下,线就是隐藏的。

   else                         // 按钮抬起
     {
      BuySL.Timeframes(OBJ_NO_PERIODS);
      SellSL.Timeframes(OBJ_NO_PERIODS);
     }
   ChartRedraw();
   return;
  }

在函数的末尾,我们调用图表的重绘。

4.6. 交易

现在,面板主要事件的处理控制功能已经描述过了,我们继续按下交易操作按钮事件的处理。对于在账户中的交易,我们也使用MQL5标准库中的"Trade.mqh", 其中实现了用于交易操作的 CTrade 类。

#include <Trade\Trade.mqh>

我们在私有部分声明交易操作的类:

private:
................
   CTrade            Trade;                           // 交易操作的类


然后我们在我们类的初始化函数中初始化交易类,在此我们设置交易的幻数,滑点水平以及执行交易订单的原则。

//+------------------------------------------------------------------+
//| 类初始化函数                             |
//+------------------------------------------------------------------+
CTradePanel::CTradePanel(void)
  {
   Trade.SetExpertMagicNumber(0);
   Trade.SetDeviationInPoints(5);
   Trade.SetTypeFilling((ENUM_ORDER_TYPE_FILLING)0);
   return;
  }

如果您想要,您可以在此增加额外的功能从外部程序中设置幻数和滑点。不要忘记,这些函数应该在公有部分声明。

在准备工作完成后,我们开发用于处理按下按钮事件的函数,首先,与以前一样,我们在私有部分声明函数:

private:
.....................
   void              BuyClick();                                     // 点击买入按钮
   void              SellClick();                                    // 点击卖出按钮
   void              CloseBuyClick();                                // 点击买入平仓按钮
   void              CloseSellClick();                               // 点击卖出平仓按钮
   void              CloseClick();                                   // 点击全部平仓按钮

然后我们在事件处理转发器中加上新的函数:

//+------------------------------------------------------------------+
//| 事件的处理                              |
//+------------------------------------------------------------------+
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)

然后,自然我们要事件事件处理函数。让我们以买入选项为例,它的行为就是我们程序对"买入"按钮被按下后进行的操作。

也许,首先我们应该确认交易量,我们从手数栏位读取数据,根据交易品种规格做调整,并检查是否有足够资金建仓,然后在图表上返回更新的数值。

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 函数                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//---
   if(id==CHARTEVENT_OBJECT_DRAG)
     {
      if(TradePanel.DragLine(sparam))
        {
         ChartRedraw();
        }
     }
...........

在处理事件的实际函数中我们必须:

  • 定义,哪条线应该被移动 (止损还是获利);
  • 把鼠标值计算为点数;
  • 在面板的相关栏位中显示取得的数值;
  • 计算面板上相关联的单元格的值;
  • 如有必要,改变单选框的值。

我们在事件处理函数的实现前三点,而对于最后一点,我们调用相关栏位的"人工"编辑函数。而且,在处理完事件之后我们自然还要去掉线的突出显示。

//+------------------------------------------------------------------+
//| 移动水平线的函数                          |
//+------------------------------------------------------------------+
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 类继承的是空白的函数:

   //--- 用于操作文件的方法
   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;                            // 显示下个订单的交易量
   CEdit_new         StopLoss_pips;                   // 显示止损点数
   CEdit_new         StopLoss_money;                  // 显示止损资金数
   CEdit_new         TakeProfit_pips;                 // 显示获利点数
   CEdit_new         TakeProfit_money;                // 显示获利资金数
   CEdit_new         Risk_percent;                    // 显示风险占净值百分比
   CEdit_new         Risk_money;                      // 显示风险资金数
   CBmpButton_new    StopLoss_line;                   // 显示止损线的复选框
   CBmpButton_new    TakeProfit_line;                 // 显示获利线的复选框
   CBmpButton_new    StopLoss_pips_b;                 // 选择止损点数
   CBmpButton_new    StopLoss_money_b;                // 选择止损资金数
   CBmpButton_new    TakeProfit_pips_b;               // 选择获利点数
   CBmpButton_new    TakeProfit_money_b;              // 选择获利资金数
   CBmpButton_new    Risk_percent_b;                  // 选择风险占净值百分比
   CBmpButton_new    Risk_money_b;                    // 选择风险资金数

保存信息还是不够的,您还需要读取它,为此,我们重写了载入我们交易面板的函数:

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


首先,我们读取保存的数据:

//+------------------------------------------------------------------+
//| 运行交易面板                            |
//+------------------------------------------------------------------+
bool CTradePanel::Run(void)
  {
   IniFileLoad();

然后,我们更新变量的值:

   cur_lot=StringToDouble(Lots.Text());
   cur_sl_pips=(int)StringToInteger(StopLoss_pips.Text());     // 止损点数
   cur_sl_money=StringToDouble(StopLoss_money.Text());         // 止损资金数
   cur_tp_pips=(int)StringToInteger(TakeProfit_pips.Text());   // 获利点数
   cur_tp_money=StringToDouble(TakeProfit_money.Text());       // 获利资金数
   cur_risk_percent=StringToDouble(Risk_percent.Text());       // 风险百分比
   cur_risk_money=StringToDouble(Risk_money.Text());           // 风险资金数
   RiskByValue=true;

然后,我们最后调用用于处理点击复选框的函数来处理止损和获利的水平:
   StopLossLineClick();
   TakeProfitLineClick();
   return(CAppDialog::Run());
  }

7. 清除

我们已经完成了大量的工作,希望用户会喜欢,但是有时候因为各种原因它们还是会关闭程序。而在我们退出之前,我们必须自己做清除任务:删除我们在图表上创建的所有对象,而保留用户或者第三方程序创建的那些对象。

在程序的终止化过程中,生成 Deinit 事件, 并且调用 OnDeinit 函数来指出终止的原因。所以,我们必须在主程序的函数中调用我们类的终止化函数:

//+------------------------------------------------------------------+
//| EA 终止化函数                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   TradePanel.Destroy(reason);
   return;
  }

这个函数应当在我们类的 public 部分声明:

public:
.............
   virtual void      Destroy(const int reason);

在函数体中,我们将从图表上删除水平线,并调用父类的清除函数来保存所需的信息并从图表上删除交易面板的对象。
//+------------------------------------------------------------------+
//| 应用程序终止函数                          |
//+------------------------------------------------------------------+
void CTradePanel::Destroy(const int reason)
  {
   BuySL.Delete();
   SellSL.Delete();
   BuyTP.Delete();
   SellTP.Delete();
   CAppDialog::Destroy(reason);
   return;
  }

结论

亲爱的读者,同事和朋友们!

我真心希望您在读完我的文章之后会觉得它有所帮助,

我尝试了分享我创建交易面板的经验并为您提供了可在市场操作中可以使用的工具,

如果您能把您对我们的交易面板的的想法,建议和期望告诉我,我将非常感激,我个人保证,我会在我未来的文章中实现其中最有趣的想法。