MQL5 Cookbook: 处理典型图表事件
Denis Kirichenko | 22 十月, 2014
介绍
在我的文章中,我打算描述并亲手实践使用 OnChartEvent() 来处理 MQL5 开发者预定义的典型(标准)事件。在 MQL5 的文章和 代码库 中已经包含了使用处理器的例程。
不过,我的目的是分析在面向事件编程 (EOP) 背景下的表现。我相信这个处理器可以成功地用于全自动和半自动交易系统。
1. "ChartEvent" 事件
那么,首先让我们来看看事件类型是什么。
根据文档,该 ChartEvent 事件会在图表工作时出现,特别是当:
所以,这个事件可以与图表交互。此外,这种交互可以是手工交易结果,也可以是某些算法操作(自动交易)。
MQL5 开发者可以通过 ENUM_CHART_EVENT 枚举中的指定类型,对 ChartEvent 事件分类。
重点注意的是,这份列表有一个用户自定义事件的范围,其作用是作为程序员的隐藏保留。MQL5 开发者可以为其定制的事件提供 65535 个标识符。
为了利用自定义事件工作,一个特殊的生成器函数 EventChartCustom() 可满足程序员的需要。尽管,本文未考虑自定义事件。
2. ChartEvent 处理器和生成器
所有 ChartEvent 事件的处理均通过一个特殊的函数 OnChartEvent() 完成。这与 MQL5 语言的概念一致,例如 Trade 事件由 OnTrade() 函数处理,而 Init 事件则由 OnInit() 函数来处理,等等。
该 OnChartEvent() 函数有以下签名:
void OnChartEvent(const int id, // event identifier const long& lparam, // parameter of the event of type long const double& dparam, // parameter of the event of type double const string& sparam // parameter of the event of type string )
所有的输入参数是常量,并当处理器被调用时,它们传递有用的信息。
因此,id 参数的数值,可以透露是哪个特别事件调用了处理器。其它可能的数值类型是长整数,双精度和字符串。通过这种方式可以获取附加的有关事件信息。
稍后,我们将创建一个指定参数值的例程,用来分析将会发生什么。
该 ChartEvent 事件的自定义部分,由程序员来实现,与 EventChartCustom() 函数连接。这个函数可以生成事件。该函数如下:
bool EventChartCustom(long chart_id, // receiving chart identifier ushort custom_event_id, // event identifier long lparam, // the long parameter double dparam, // the double parameter string sparam // the string parameter )
实际上,生成器函数可以根据输入参数值创建事件,并将之发送到包括当前在内的任意图表。后者的类型是:无符号短整数,长整数,双精度,字符串。
该 OnChartEvent() 和 EventChartCustom() 函数一起组成了强大的工具,是体现面向事件编程优点的佳例。
3. 标准事件处理模板
现在,我将研究图表事件的类型,并给出每一个的例子。每个事件都有自己专用的 EventProcessor.mq5 版本,并且它的代码中包含图表事件处理。在 MQL5 中有 10 种典型事件。
对于其中的三个 (鼠标事件, 图形对象创建事件, 图形对象删除事件) 我们需要准备一个图表。可以通过使用 ChartSetInteger() 函数完成。它允许一个图表响应指定事件。
一个响应事件处理的典型模块看起来像:
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { string comment="Last event: "; //--- select event on chart switch(id) { //--- 1 case CHARTEVENT_KEYDOWN: { comment+="1) keystroke"; break; } //--- 2 case CHARTEVENT_MOUSE_MOVE: { comment+="2) mouse"; break; } //--- 3 case CHARTEVENT_OBJECT_CREATE: { comment+="3) create graphical object"; break; } //--- 4 case CHARTEVENT_OBJECT_CHANGE: { comment+="4) change object properties via properties dialog"; break; } //--- 5 case CHARTEVENT_OBJECT_DELETE: { comment+="5) delete graphical object"; break; } //--- 6 case CHARTEVENT_CLICK: { comment+="6) mouse click on chart"; break; } //--- 7 case CHARTEVENT_OBJECT_CLICK: { comment+="7) mouse click on graphical object"; break; } //--- 8 case CHARTEVENT_OBJECT_DRAG: { comment+="8) move graphical object with mouse"; break; } //--- 9 case CHARTEVENT_OBJECT_ENDEDIT: { comment+="9) finish editing text"; break; } //--- 10 case CHARTEVENT_CHART_CHANGE: { comment+="10) modify chart"; break; } } //--- Comment(comment); }
在每个条件分支里,我加入了字符串,描述选择的事件。作为结果,在注释行中我们可以看到图表上最后发生的事件。如果您运行该模板,并在图表中执行各种操作,您会注意到在注释行可能有不同的记录。
很明显,使用这个 EA 只能确定事件类型。我们需要扩充它的能力。
4. 标准事件处理例程
4.1. 按键事件
第一个条件分支可以与键盘按钮工作,所以我们的 EA 可以响应按键。当按下 "上箭头" 时,则买,而当按下 "下箭头" 时,则卖。那么这种条件分支如下所示:
//--- 1 case CHARTEVENT_KEYDOWN: { //--- "up" arrow if(lparam==38) TryToBuy(); //--- "down" arrow else if(lparam==40) TryToSell(); comment+="1) keystroke"; //--- break; }
参看附带的 EA 源代码,详细了解 TryToBuy() 和 TryToSell() 的实现。交易参数 (手数大小, 止损, 止盈, 等等) 作为输入变量指定 (InpLot, InpStopLoss, InpTakeProfit, 等等)。同样应该提到的是,参数 lparam 捕获按键的代码。
更新版本 EA 称作 EventProcessor1.mq5。
4.2. 鼠标事件
这个事件类型仅在属性 CHART_EVENT_MOUSE_MOVE 在图表中指定时才会被处理。据此原因,EA 的初始化模块包括这样的语句:
//--- mouse move bool is_mouse=false; if(InpIsEventMouseMove) is_mouse=true; ChartSetInteger(0,CHART_EVENT_MOUSE_MOVE,is_mouse);
应该注意的是,如果您使用鼠标,那么,自然地,鼠标事件会经常发生。出于这个原因,有机会来禁用该事件处理是有用的。该 lparam 和 dparam 处理器参数报告相应的 X 和 Y 坐标。
我们将继续讨论例程。让我们假设零柱线自右边框偏移。将悬浮在屏幕上的光标,从偏移处滑到右边,会弹出一个窗口,提示买或卖。
要做到这一点,我们首先要确定偏移量。我们将继续介绍一个输入变量用来确定自右边框至零柱线的距离百分比。 (InpChartShiftSize)。
图例.1 交易操作窗口
我们继续使用函数来启动偏移并确定它的大小 ChartShiftSet() 和 ChartShiftSizeSet()。然后我们应该识别光标的 X 坐标之前是否在左边界,并且它是否已经移动到右侧。如果是这样,那么提示买卖的窗口将会出现 (图例.1)。
实现这些目标集合的代码如下所示:
//--- 2 case CHARTEVENT_MOUSE_MOVE: { comment+="2) mouse"; //--- if a mouse event is processed if(InpIsEventMouseMove) { long static last_mouse_x; //--- enable shift if(ChartShiftSet(true)) //--- set shift size if(ChartShiftSizeSet(InpChartShiftSize)) { //--- chart width int chart_width=ChartWidthInPixels(); //--- calculate X coordinate of shift border int chart_shift_x=(int)(chart_width-chart_width*InpChartShiftSize/100.); //--- border crossing condition if(lparam>chart_shift_x && last_mouse_x<chart_shift_x) { int res=MessageBox("Yes: buy / No: sell","Trade operation",MB_YESNO); //--- buy if(res==IDYES) TryToBuy(); //--- sell else if(res==IDNO) TryToSell(); } //--- store mouse X coordinate last_mouse_x=lparam; } } //--- break; }
买与卖则通过之前创建的交易函数完成。更新版本 EA 名为 EventProcessor2.mq5。
4.3. 图形对象创建事件
这个事件类型是在图形对象在图表上创建时生成。与鼠标事件类似,这个类型需要通过属性 CHART_EVENT_OBJECT_CREATE 授予权限。如果我们打算在新图形对象出现时进行响应,则需要在初始化模块中指定一次。
//--- object create bool is_obj_create=false; if(InpIsEventObjectCreate) is_obj_create=true; ChartSetInteger(0,CHART_EVENT_OBJECT_CREATE,is_obj_create);
处理器中的唯一参数是将要包含的信息。有一个字符串参数 sparam 保留创建的图形对象的名字。我们可以通过名称找到对象,进行处理,并决定下一步做什么。
此处是简单的例程。我们要在图表上画一条水平线,让自动交易将其放置在图表所含的所有柱线的最高价位置,并且绘制其它两条线。底线将被放置在最低价,并且第三条线位于前两者之间的等距位置。
实现任务的代码:
//--- 3 case CHARTEVENT_OBJECT_CREATE: { comment+="3) create graphical object"; //--- if graphical object creation event is processed if(InpIsEventObjectCreate) { //--- capture creation of horizontal line int all_hor_lines=ObjectsTotal(0,0,OBJ_HLINE); //--- if this is the only line if(all_hor_lines==1) { string hor_line_name1=sparam; //--- calculate levels int visible_bars_num=ChartVisibleBars(); //--- arrays for high and low prices double highs[],lows[]; //--- int copied=CopyHigh(_Symbol,_Period,0,visible_bars_num-1,highs); if(copied!=visible_bars_num-1) { Print("Failed to copy highs!"); return; } copied=CopyLow(_Symbol,_Period,0,visible_bars_num-1,lows); if(copied!=visible_bars_num-1) { Print("Failed to copy lows!"); return; } //--- high and low prices double ch_high_pr,ch_low_pr,ch_mid_pr; //--- ch_high_pr=NormalizeDouble(highs[ArrayMaximum(highs)],_Digits); ch_low_pr=NormalizeDouble(lows[ArrayMinimum(lows)],_Digits); ch_mid_pr=NormalizeDouble((ch_high_pr+ch_low_pr)/2.,_Digits); //--- place created line on high if(ObjectFind(0,hor_line_name1)>-1) if(!ObjectMove(0,hor_line_name1,0,0,ch_high_pr)) { Print("Failed to move!"); return; } //--- create line on low string hor_line_name2="Hor_line_min"; //--- if(!ObjectCreate(0,hor_line_name2,OBJ_HLINE,0,0,ch_low_pr)) { Print("Failed to create the 2nd horizontal line!"); return; } //--- create line between high and low string hor_line_name3="Hor_line_mid"; //--- if(!ObjectCreate(0,hor_line_name3,OBJ_HLINE,0,0,ch_mid_pr)) { Print("Failed to create the 3rd horizontal line!"); return; } } } break; }
更新版本 EA 名为 EventProcessor3.mq5。
图例. 2. 创建图形对象事件的处理结果
在我完成这个过程之后,我得到了如下画面 (图例. 2)。因此,集成功能赋予 EA 一种能力,可以对图形对象的创建做出反应,并采取行动。
4.4. 通过属性对话框修改图形对象属性事件
这个事件类型与前一个十分类似。当通过属性对话框修改图形对象属性时,它被触发。这个工具也许十分有用,例如,同步相同类型的对象的图形属性。
想象一下,图表上有若干对象。交易者通常在图表上绘制很多种线条。这些线有时需要隐藏,但并非删除。我们将找到一个解决这个任务的方案。修改后的线条可以脱色,对于其它图形对象也可同样完成。之后,代码如下所示:
//--- 4 case CHARTEVENT_OBJECT_CHANGE: { comment+="4) change object properties via properties dialog"; //--- string curr_obj_name=sparam; //--- find the changed object if(ObjectFind(0,curr_obj_name)>-1) { //--- get object color color curr_obj_color=(color)ObjectGetInteger(0,curr_obj_name,OBJPROP_COLOR); //--- total number of objects on chart int all_other_objects=ObjectsTotal(0); //--- find other objects for(int obj_idx=0;obj_idx<all_other_objects;obj_idx++) { string other_obj_name=ObjectName(0,obj_idx); if(StringCompare(curr_obj_name,other_obj_name)!=0) if(!ObjectSetInteger(0,other_obj_name,OBJPROP_COLOR,curr_obj_color)) { Print("Failed to change the object color!"); return; } } //--- redraw chart ChartRedraw(); } //--- break;
让我们假设图表上有一组线条 (图例.3)。
图例.3. 多色动态线条
如果我们试图改变任何线条的颜色,或者准确地说,在属性对话框中将之脱色 (图例.4),则在图表上将没有线条可见。同时,该图形对象仍将在那里存在。
图例.4. 修改线条颜色
更新版本 EA 称作 EventProcessor4.mq5。
4.5. 图形对象删除事件
如同此事件类型的名称所暗示的,当从图表中删除对象时它会出现。它是最后一组需要直接预先授权才可进行处理的事件。这可以通过属性 CHART_EVENT_OBJECT_DELETE 完成。
//--- object delete bool is_obj_delete=false; if(InpIsEventObjectDelete) is_obj_delete=true; ChartSetInteger(0,CHART_EVENT_OBJECT_DELETE,is_obj_delete);
这里是另一个假想的例子。在 EA 加载的图表上,有一组不同类型的图形对象。假设我们需要删除一个特定类型的对象。例如,垂直线 (图例.5)。
图例.5. 五条垂直线和其它线条
我们只需要删除一条垂线,之后 EA 将删除其它 (图例.6)。
图例.6. 剩余线条
以下条目将出现在标栏 "Experts":
NS 0 10:31:17.937 EventProcessor5 (EURUSD.e,W1) Vertical lines before removing: 4 MD 0 10:31:17.937 EventProcessor5 (EURUSD.e,W1) Vertical lines removed from the chart: 4 QJ 0 10:31:18.078 EventProcessor5 (EURUSD.e,W1) Vertical lines after removing: 0
一个必须提及的重要方面。一旦对象被删除,则无法再访问其属性。这意味着,如果我们没有预先恢复该对象所需的数据,那么在它被删除之后,它将无法访问。因此,如果我们要找出已删除对象的类型,我们必须在删除对象之前将其保存。我对 MQL5 的开发者有个建议,在终端里创建一个可用图表的历史。这将让我们可以引用已删除对象的属性。
我们将最终的 EA 版本称为 EventProcessor5.mq5。
4.6. 在图表上鼠标点击事件
如果在图表中点击鼠标左键这个事件将会产生。在图表上右键点击则会打开菜单,点击中键会弹出十字光标。该 lparam 和 dparam 处理器参数报告相应的 X 和 Y 坐标。
下面的简单任务将作为一个例子。我们要安排在鼠标点击发生的位置绘制一个"买"箭头。该对象 'arrow' 仅需一个定位点。因此,只需要将 X 和 Y 坐标变换为定位点的时间和价格数值。
以上例子的代码:
//--- 6 case CHARTEVENT_CLICK: { comment+="6) mouse click on chart"; //--- object counter static uint sign_obj_cnt; string buy_sign_name="buy_sign_"+IntegerToString(sign_obj_cnt+1); //--- coordinates int mouse_x=(int)lparam; int mouse_y=(int)dparam; //--- time and price datetime obj_time; double obj_price; int sub_window; //--- convert the X and Y coordinates to the time and price values if(ChartXYToTimePrice(0,mouse_x,mouse_y,sub_window,obj_time,obj_price)) { //--- create object if(!ObjectCreate(0,buy_sign_name,OBJ_ARROW_BUY,0,obj_time,obj_price)) { Print("Failed to create buy sign!"); return; } //--- redraw chart ChartRedraw(); //--- increase object counter sign_obj_cnt++; } //--- break; }
当前版本 EA 名为 EventProcessor6.mq5.
4.7. 鼠标点击图形对象事件
这种类型的图表事件不同于前一个,只有在鼠标点击发生在一个图形对象时成立。字符串参数 sparam 将包含点击对象的名称。在前一个例子中我们创建了 '买' 箭头。让我们来完成当点击这类型对象时将它转为 '卖' 箭头。
处理器的模块代码如下所示:
//--- 7 case CHARTEVENT_OBJECT_CLICK: { comment+="7) mouse click on graphical object"; //--- string sign_name=sparam; //--- delete buy arrow if(ObjectDelete(0,sign_name)) { //--- redraw chart ChartRedraw(); //--- static uint sign_obj_cnt; string sell_sign_name="sell_sign_"+IntegerToString(sign_obj_cnt+1); //--- coordinates int mouse_x=(int)lparam; int mouse_y=(int)dparam; //--- time and price datetime obj_time; double obj_price; int sub_window; //--- convert the X and Y coordinates to the time and price values if(ChartXYToTimePrice(0,mouse_x,mouse_y,sub_window,obj_time,obj_price)) { //--- create object if(!ObjectCreate(0,sell_sign_name,OBJ_ARROW_SELL,0,obj_time,obj_price)) { Print("Failed to create sell sign!"); return; } //--- redraw chart ChartRedraw(); //--- increase object counter sign_obj_cnt++; } } //--- break; }
为了说明这个例子,我保持鼠标点击处理的条件分支不变。启动 EA,左击鼠标三次并得到三个买箭头 (图例.7)。我用黄色突显它们的位置。
图例.7. '买' 箭头
如果我们现在点击每一个 '买' 箭头,我们将得到如下图片 (图例.8)。
图例.8. '买' 与 '卖' 箭头
'卖' 箭头如期出现,但没有设计 '买' 箭头出现。这就是为什么我打开图表上的对象列表,并用黄色来突显名为"买"的箭头。
很容易注意到 EA 创建了第四,第五,第六个 '买' 箭头。为什么会发生这种情况?这种情况发生是因为第一次在对象上点击触发了两个事件: 第一个事件是在对象上的真实点击,而第二个 - 则是在图表上点击。最后一个事件生成了创建"买"箭头动作。出现这种情况,有必要补充机制,避免处理第二个在图表上点击事件。而我觉得按照时间进行控制可以作为这样的机制。
让我们添加一个全局变量 gLastTime。这将有利于控制"买"箭头的创建时间。如果点击处理器在"卖"箭头创建后小于 250ms 的时间内被调用,则此调用被忽略。
在图表重绘之前,在处理对象点击模块中加入以下语句:
//--- store the moment of creation gLastTime=GetTickCount();
时间验证也要加入到处理图表点击模块中。
uint lastTime=GetTickCount(); if((lastTime-gLastTime)>250) { //--- click handling }
让我们在图表上再次创建三个'买' 箭头 (图例.9)。
图例.9. '买' 箭头
我们尝试点击它们,尽管它们的尺寸很小。在点击后箭头变成 '卖' 类型 (图例.10)。
图例.10. '卖' 箭头
与以前类似,我们将新版本命名为 EventProcessor7.mq5。
4.8. 鼠标移动图形对象事件
当对象在图表区域内移动时,此事件发生。该处理器接收字符串参数 sparam 传递的移动对象名称。
此处是另外的例子。日内交易者往往在一个确定时间间隔内交易。垂直线将是一个时间间隔的界限。画面看起来大约像 图例.11。而感兴趣的间隔则高亮突显。
图例.11. 时间间隔界限
该时间间隔可以手工修改。那么,我们的半自动将不得不应对这样的变化。
在全局层面,我们将创建两个垂线变量名为 - gTimeLimit1_name 和 gTimeLimit2_name。我们还需要为矩形创建一对变量,在图表上将非交易时段变暗。全局变量的定位点也要创建。由于我们有两个矩形,我们将需要四个定位点。
条件分支 CHARTEVENT_OBJECT_DRAG 处理器的代码:
//--- 8 case CHARTEVENT_OBJECT_DRAG: { comment+="8) move graphical object with mouse"; string curr_obj_name=sparam; //--- if one of the vertical lines is moved if(!StringCompare(curr_obj_name,gTimeLimit1_name) || !StringCompare(curr_obj_name,gTimeLimit2_name)) { //--- the time coordinate of vertical lines datetime time_limit1=0; datetime time_limit2=0; //--- find the first vertical line if(ObjectFind(0,gTimeLimit1_name)>-1) time_limit1=(datetime)ObjectGetInteger(0,gTimeLimit1_name,OBJPROP_TIME); //--- find the second vertical line if(ObjectFind(0,gTimeLimit2_name)>-1) time_limit2=(datetime)ObjectGetInteger(0,gTimeLimit2_name,OBJPROP_TIME); //--- if vertical lines are found if(time_limit1>0 && time_limit2>0) if(time_limit1<time_limit2) { //--- update properties of rectangles datetime start_time=time_limit1; datetime finish_time=time_limit2; //--- if(RefreshRecPoints(start_time,finish_time)) { //--- if(!ObjectMove(0,gRectLimit1_name,0,gRec1_time1,gRec1_pr1)) { Print("Failed to move the 1st point!"); return; } if(!ObjectMove(0,gRectLimit1_name,1,gRec1_time2,gRec1_pr2)) { Print("Failed to move the 2nd point!"); return; } //--- if(!ObjectMove(0,gRectLimit2_name,0,gRec2_time1,gRec2_pr1)) { Print("Failed to move the 1st point!"); return; } if(!ObjectMove(0,gRectLimit2_name,1,gRec2_time2,gRec2_pr2)) { Print("Failed to move the 2nd point!"); return; } } } } //--- break; }
此代码包含一个自定义的函数 RefreshRecPoints()。它处理两个矩形定位点的数值更新。该 EA 的初始化块可以提供有关创建图形对象的信息。更新版本 EA 称作 EventProcessor8.mq5。
4.9. 在文本框中完成文本编辑 事件
此事件类型有一个很专业的性质,并且在数据输入域内编辑文本时出现。参数 sparam 包含工作对象的名称。
下面来考虑一个例子。在数据输入域,我们输入将要执行的交易操作。只有两种操作 - 买和卖。如果我们在输入域输入词 'Buy',则 EA 将购买资产,并且若我们输入 'Sell',资产将会被出售。我们会安排该字段不区分大小写,即我们可以输入'buy' 和 'sell'。文本和输入域在出售时为红色,且在买进时为蓝色 (图例.12)。
图例.12. 通过文本域买
条件分支 CHARTEVENT_OBJECT_ENDEDIT 的代码:
//--- 9 case CHARTEVENT_OBJECT_ENDEDIT: { comment+="9) end of editing a text in the data entry field"; //--- string curr_obj_name=sparam; //--- if specified text field is being edited if(!StringCompare(curr_obj_name,gEdit_name)) { //--- get object description string obj_text=NULL; if(ObjectGetString(0,curr_obj_name,OBJPROP_TEXT,0,obj_text)) { //--- check value if(!StringCompare(obj_text,"Buy",false)) { if(TryToBuy()) //--- set text color ObjectSetInteger(0,gEdit_name,OBJPROP_COLOR,clrBlue); } else if(!StringCompare(obj_text,"Sell",false)) { if(TryToSell()) //--- set text color ObjectSetInteger(0,gEdit_name,OBJPROP_COLOR,clrRed); } else { //--- set text color ObjectSetInteger(0,gEdit_name,OBJPROP_COLOR,clrGray); } //--- redraw chart ChartRedraw(); } } //--- break; }
更新版本 EA 称作 EventProcessor9.mq5。您可以在原文件中找到创建文本域的模块。
4.10. 图表修改事件
本文中我们要研究的最后一个事件是修改图表设置。这是一个奇特的事件,因为此时我们处理图表本身,而非图表上的对象。开发者说,在图表的大小被改变,或新设置生效时,产生此事件。
此处是另外的例子。让我们假设禁止改变图表设置。则在此限制下,所有试图修改设置的操作将被忽略。事实上,该 EA 将会简单的返回之前的数值。让我们修订图表的以下参数:
- 显示栅格;
- 图表显示类型;
- 背景颜色。
条件分支代码:
//--- 10 case CHARTEVENT_CHART_CHANGE: { //--- current height and width of the chart int curr_ch_height=ChartHeightInPixelsGet(); int curr_ch_width=ChartWidthInPixels(); //--- if chart height and width have not changed if(gChartHeight==curr_ch_height && gChartWidth==curr_ch_width) { //--- fix the properties: //--- display grid if(!ChartShowGridSet(InpToShowGrid)) { Print("Failed to show grid!"); return; } //--- type of chart display if(!ChartModeSet(InpMode)) { Print("Failed to set mode!"); return; } //--- background color if(!ChartBackColorSet(InpBackColor)) { Print("Failed to set background сolor!"); return; } } //--- store window dimensions else { gChartHeight=curr_ch_height; gChartWidth=curr_ch_width; } //--- comment+="10) modify chart"; //--- break; }
最后版本 EA 称作 EventProcessor10.mq5。
结论
在本文中,我尝试描绘 MetaTrader 5 中典型图表事件的多样性。我希望这些事件处理例程将会对那些开始编写 MQL5 代码的程序员有所帮助。