
MQL5 Cookbook: 处理自定义图表事件
介绍
本文逻辑上延续文章 MQL5 Cookbook: 处理典型图表事件。它覆盖了自定义图表事件的工作方法。在此读者可以找到自定义事件的开发与处理例程。本文中讨论的所有思想,均以面向对象工具实现。
自定义事件的题材是相当广泛的,尤其是当程序员和开发者将创意引入他们的工作之时。
1. 自定义图表事件
很明显,这个事件是由用户定义的。它由程序员来决定 究竟是何 以及 哪些 任务或程序模块可采用一个事件形式。而 MQL5 开发者可以创建自己的事件,扩展语言能力,用于实现复杂的算法。
自定义事件是第二种可能的图表事件类型。第一个种类是典型事件。尽管在文档里没有 "典型图表事件" 这样的术语,我依然建议将它用于第一部分的十个图表事件类型。
开发者仅建议一个包括所有图表事件的枚举 — ENUM_CHART_EVENT。
根据文档,自定义事件有 65535 个标识符。自定义事件的第一个和最后一个标识符由明确的数值 CHARTEVENT_CUSTOM 和 CHARTEVENT_CUSTOM_LAST 设定,它在数值上相应的等于 1000 和 66534 (图例.1)。
图例.1 自定义事件的第一个和最后一个标识符
简单的计算,考虑第一个和最后一个标识符将产生: 66534-1000+1=65535。
使用自定义的事件之前,它们必须首先设定。在此意义上,开发者成为策划者,以及在将来的 EA 中实现该事件概念算法的作者。将自定义事件进行分类是极其有用的。这种认知方法将不能摆脱模糊,但肯定会降低它的程度,并会安排推理的路线。
让我们考虑这样一个自定义事件标准作为源。例如,开发者 sergeev 提出了一个自动交易原型的思路。他将所有事件划分成三组 (图例.2)。
图例.2 自定义事件源分组
然后,根据这一主要思想,自定义事件依据自己的组属关系得以发展。
让我们试着从一些简单的开始做。首先,我们采用第一组,其中包括指标事件。可以属于这个组的事件是:创建和删除一个指标,收到开仓和平仓的信号。第二组包括修改订单和仓位状态的事件。在我们的例子中,开、平仓是在这个组。这是十分简单的。并且,在最后,最难以正规化的组,是外部事件组。
让我们用两个事件: 启用和禁用手工交易。
图例.3 自定义事件源
主要的范例可以通过演绎法建立 (从一般到特殊) (图利.3)。这个范例,我们将稍后用于在对应的类中创建事件类型 (表格 1)。
表格 1 自定义事件
这张表还不能称作 "事件概念",但它是一个开始。这是另一种途径。众所周知,一个抽象的交易系统模型由三个子系统组成 — 基础模块 (图例.4)。
图例.4 抽象系统模型
基于"源"的自定义事件,可由事件产生的标准进行分类:
- 信号子系统;
- 持仓追踪子系统;
- 资金管理子系统。
后者,举例,可以包括这样的事件,如达到回撤许可级别,通过设定数值增加交易量,增加亏损限制百分比,等等。
2. 图表事件处理器和生成器
以下几行将专用于图表事件处理器和生成器。至于处理自定义图表事件,它的原理类似于处理一个典型图表事件之一。
处理器中,OnChartEvent() 函数需要四个常量作为参数。显然,开发者利用这一机制来实现事件识别以及获取有关它的更多信息的想法。在我看来,这是一个非常精巧的程序机制。
函数 EventChartCustom() 生成一个自定义的图表事件。值得注意的是,被创建的自定义图表事件,可以用于图表“自身”,也可以给“外人”。我认为,有关自身和外来图表的含义,最有趣的文章是在 MetaTrader 5 中实现多币种模式。
在我看来,有一个不和谐的事实,即事件标识符在生成器中是 ushort 型,而在处理器中它是 int 型的。合乎逻辑的作法是在处理器中也使用 ushort 数据类型。
3. 自定义事件类
正如我前面提到的,事件的概念取决于 EA 的开发者。现在我们继续处理来自表 1. 的事件。首先,我们将自定义事件类 CEventBase 及其衍生类进行排序 (图例.5)。
图例.5 事件类的层次结构
基础类如下所示:
//+------------------------------------------------------------------+ //| Class CEventBase. | //| Purpose: base class for a custom event | //| Derives from class CObject. | //+------------------------------------------------------------------+ class CEventBase : public CObject { protected: ENUM_EVENT_TYPE m_type; ushort m_id; SEventData m_data; public: void CEventBase(void) { this.m_id=0; this.m_type=EVENT_TYPE_NULL; }; void ~CEventBase(void){}; //-- bool Generate(const ushort _event_id,const SEventData &_data, const bool _is_custom=true); ushort GetId(void) {return this.m_id;}; private: virtual bool Validate(void) {return true;}; };
事件类型通过 ENUM_EVENT_TYPE 枚举设置:
//+------------------------------------------------------------------+ //| A custom event type enumeration | //+------------------------------------------------------------------+ enum ENUM_EVENT_TYPE { EVENT_TYPE_NULL=0, // no event //--- EVENT_TYPE_INDICATOR=1, // indicator event EVENT_TYPE_ORDER=2, // order event EVENT_TYPE_EXTERNAL=3, // external event };
数据成员包括事件标识符和数据结构。
该 CEventBase 基础类的 Generate() 方法处理事件生成。而 GetId() 方法返回事件 id,虚拟方法 Validate() 检查事件标识符的值。起初,我将事件处理方法包含进类中,但之后我意识到,每一个事件都是独特的,在此一个抽象的方法是不足的。我最终放弃了将自定义事件处理加进 CEventProcessor 类的委派任务。
4. 自定义事件处理器类
该 CEventProcessor 类假设生成并处理八个给出的事件。类的数据成员如下所示:
//+------------------------------------------------------------------+ //| Class CEventProcessor. | //| Purpose: base class for an event processor EA | //+------------------------------------------------------------------+ class CEventProcessor { //+----------------------------Data members--------------------------+ protected: ulong m_magic; //--- flags bool m_is_init; bool m_is_trade; //--- CEventBase *m_ptr_event; //--- CTrade m_trade; //--- CiMA m_fast_ema; CiMA m_slow_ema; //--- CButton m_button; bool m_button_state; //+------------------------------------------------------------------+ };
在属性列表当中也有初始化和交易的标志。如果它不能正常启动,第一个标志不允许 EA 进行交易。第二个检查交易权限。
还有指向对象 CEventBase 类型的指针,它利用多态与不同类型的事件工作。一个 CTrade 类的实例可以存取交易操作。
而 CiMA 类型的对象便于处理从指标接收的数据。为了简化例程,我用了两条移动平均线作为将要接收的交易信号。还有一个 "CButton" 类的实例将用来手工启动/禁止 EA。
划分类的方法依照 "模块 – 过程 – 函数 – 宏" 原理:
//+------------------------------------------------------------------+ //| Class CEventProcessor. | //| Purpose: base class for an event processor EA | //+------------------------------------------------------------------+ class CEventProcessor { //+-------------------------------Methods----------------------------+ public: //--- constructor/destructor void CEventProcessor(const ulong _magic); void ~CEventProcessor(void); //--- Modules //--- event generating bool Start(void); void Finish(void); void Main(void); //--- event processing void ProcessEvent(const ushort _event_id,const SEventData &_data); private: //--- Procedures void Close(void); void Open(void); //--- Functions ENUM_ORDER_TYPE CheckCloseSignal(const ENUM_ORDER_TYPE _close_sig); ENUM_ORDER_TYPE CheckOpenSignal(const ENUM_ORDER_TYPE _open_sig); bool GetIndicatorData(double &_fast_vals[],double &_slow_vals[]); //--- Macros void ResetEvent(void); bool ButtonStop(void); bool ButtonResume(void); };
在模块当中,有三个只生成事件: 开始是一个 — Start(),结束是一个 — Finish() 以及主程序是一个 — Main()。而第四个模块 ProcessEvent() 则同时是事件处理器和生成器。
4.1开始模块
该模块被设计在 OnInit() 处理器内调用。
//+------------------------------------------------------------------+ //| Start module | //+------------------------------------------------------------------+ bool CEventProcessor::Start(void) { //--- create an indicator event object this.m_ptr_event=new CIndicatorEvent(); if(CheckPointer(this.m_ptr_event)==POINTER_DYNAMIC) { SEventData data; data.lparam=(long)this.m_magic; //--- generate CHARTEVENT_CUSTOM+1 event if(this.m_ptr_event.Generate(1,data)) //--- create a button if(this.m_button.Create(0,"Start_stop_btn",0,25,25,150,50)) if(this.ButtonStop()) { this.m_button_state=false; return true; } } //--- return false; }
指向事件对象的指针在这个模块中被创建。之后,"指针创建" 事件被生成。一个按钮最后被创建。它切换为 "停止" 模式。这意味着,在按钮被按下时,EA 将停止工作。
而 SEventData 结构也在该方法的定义中涉及。这是一个简单的容器,将参数传递到自定义事件生成器。唯一的结构字段将被在此填充 — 它是长整型的字段。它将持有 EA 的魔幻数字。
4.2结束模块
该模块假设应该在 OnDeinit() 处理器内调用。
//+------------------------------------------------------------------+ //| Finish module | //+------------------------------------------------------------------+ void CEventProcessor::Finish(void) { //--- reset the event object this.ResetEvent(); //--- create an indicator event object this.m_ptr_event=new CIndicatorEvent(); if(CheckPointer(this.m_ptr_event)==POINTER_DYNAMIC) { SEventData data; data.lparam=(long)this.m_magic; //--- generate CHARTEVENT_CUSTOM+2 event bool is_generated=this.m_ptr_event.Generate(2,data,false); //--- process CHARTEVENT_CUSTOM+2 event if(is_generated) this.ProcessEvent(CHARTEVENT_CUSTOM+2,data); } }
在此,先前的事件指针被清零,并生成了"指标缺失"事件。我必须指出,如果自定义事件在 OnDeinit() 处理器中被生成,您将得到一个运行时错误 4001 (外部异常错误)。因此,在此方法中执行的事件产生和处理,都未调用 OnChartEvent()。
再次,EA 的魔术数字将使用 SEventData 结构进行存储。
4.3主程序模块
该模块假设应该在 OnTick() 处理器内调用。
//+------------------------------------------------------------------+ //| Main module | //+------------------------------------------------------------------+ void CEventProcessor::Main(void) { //--- a new bar object static CisNewBar newBar; //--- if initialized if(this.m_is_init) //--- if not paused if(this.m_is_trade) //--- if a new bar if(newBar.isNewBar()) { //--- close module this.Close(); //--- open module this.Open(); } }
过程 Open() 和 Close() 在此模块中被调用。第一个过程能够生成 "收到一个开仓信号" 事件,第二个 — "收到一个平仓信号" 事件。模块的当前版本在新柱线出现时功能齐全。用来检测 新柱线 的类已由 Konstantin Gruzdev 描述。
4.4事件处理模块
该模块假设应该在 OnChartEvent() 处理器内调用。这个模块的大小和功能都是最大的。
//+------------------------------------------------------------------+ //| Process event module | //+------------------------------------------------------------------+ void CEventProcessor::ProcessEvent(const ushort _event_id,const SEventData &_data) { //--- check event id if(_event_id==CHARTEVENT_OBJECT_CLICK) { //--- button click if(StringCompare(_data.sparam,this.m_button.Name())==0) { //--- button state bool button_curr_state=this.m_button.Pressed(); //--- to stop if(button_curr_state && !this.m_button_state) { if(this.ButtonResume()) { this.m_button_state=true; //--- reset the event object this.ResetEvent(); //--- create an external event object this.m_ptr_event=new CExternalEvent(); //--- if(CheckPointer(this.m_ptr_event)==POINTER_DYNAMIC) { SEventData data; data.lparam=(long)this.m_magic; data.dparam=(double)TimeCurrent(); //--- generate CHARTEVENT_CUSTOM+7 event ushort curr_id=7; if(!this.m_ptr_event.Generate(curr_id,data)) PrintFormat("Failed to generate an event: %d",curr_id); } } } //--- to resume else if(!button_curr_state && this.m_button_state) { if(this.ButtonStop()) { this.m_button_state=false; //--- reset the event object this.ResetEvent(); //--- create an external event object this.m_ptr_event=new CExternalEvent(); //--- if(CheckPointer(this.m_ptr_event)==POINTER_DYNAMIC) { SEventData data; data.lparam=(long)this.m_magic; data.dparam=(double)TimeCurrent(); //--- generate CHARTEVENT_CUSTOM+8 event ushort curr_id=8; if(!this.m_ptr_event.Generate(curr_id,data)) PrintFormat("Failed to generate an event: %d",curr_id); } } } } } //--- user event else if(_event_id>CHARTEVENT_CUSTOM) { long magic=_data.lparam; ushort curr_event_id=this.m_ptr_event.GetId(); //--- check magic if(magic==this.m_magic) //--- check id if(curr_event_id==_event_id) { //--- process the definite user event switch(_event_id) { //--- 1) indicator creation case CHARTEVENT_CUSTOM+1: { //--- create a fast ema if(this.m_fast_ema.Create(_Symbol,_Period,21,0,MODE_EMA,PRICE_CLOSE)) if(this.m_slow_ema.Create(_Symbol,_Period,55,0,MODE_EMA,PRICE_CLOSE)) if(this.m_fast_ema.Handle()!=INVALID_HANDLE) if(this.m_slow_ema.Handle()!=INVALID_HANDLE) { this.m_trade.SetExpertMagicNumber(this.m_magic); this.m_trade.SetDeviationInPoints(InpSlippage); //--- this.m_is_init=true; } //--- break; } //--- 2) indicator deletion case CHARTEVENT_CUSTOM+2: { //---release indicators bool is_slow_released=IndicatorRelease(this.m_fast_ema.Handle()); bool is_fast_released=IndicatorRelease(this.m_slow_ema.Handle()); if(!(is_slow_released && is_fast_released)) { //--- to log? if(InpIsLogging) Print("Failed to release the indicators!"); } //--- reset the event object this.ResetEvent(); //--- break; } //--- 3) check open signal case CHARTEVENT_CUSTOM+3: { MqlTick last_tick; if(SymbolInfoTick(_Symbol,last_tick)) { //--- signal type ENUM_ORDER_TYPE open_ord_type=(ENUM_ORDER_TYPE)_data.dparam; //--- double open_pr,sl_pr,tp_pr,coeff; open_pr=sl_pr=tp_pr=coeff=0.; //--- if(open_ord_type==ORDER_TYPE_BUY) { open_pr=last_tick.ask; coeff=1.; } else if(open_ord_type==ORDER_TYPE_SELL) { open_pr=last_tick.bid; coeff=-1.; } sl_pr=open_pr-coeff*InpStopLoss*_Point; tp_pr=open_pr+coeff*InpStopLoss*_Point; //--- to normalize prices open_pr=NormalizeDouble(open_pr,_Digits); sl_pr=NormalizeDouble(sl_pr,_Digits); tp_pr=NormalizeDouble(tp_pr,_Digits); //--- open the position if(!this.m_trade.PositionOpen(_Symbol,open_ord_type,InpTradeLot,open_pr, sl_pr,tp_pr)) { //--- to log? if(InpIsLogging) Print("Failed to open the position: "+_Symbol); } else { //--- pause Sleep(InpTradePause); //--- reset the event object this.ResetEvent(); //--- create an order event object this.m_ptr_event=new COrderEvent(); if(CheckPointer(this.m_ptr_event)==POINTER_DYNAMIC) { SEventData data; data.lparam=(long)this.m_magic; data.dparam=(double)this.m_trade.ResultDeal(); //--- generate CHARTEVENT_CUSTOM+5 event ushort curr_id=5; if(!this.m_ptr_event.Generate(curr_id,data)) PrintFormat("Failed to generate an event: %d",curr_id); } } } //--- break; } //--- 4) check close signal case CHARTEVENT_CUSTOM+4: { if(!this.m_trade.PositionClose(_Symbol)) { //--- to log? if(InpIsLogging) Print("Failed to close the position: "+_Symbol); } else { //--- pause Sleep(InpTradePause); //--- reset the event object this.ResetEvent(); //--- create an order event object this.m_ptr_event=new COrderEvent(); if(CheckPointer(this.m_ptr_event)==POINTER_DYNAMIC) { SEventData data; data.lparam=(long)this.m_magic; data.dparam=(double)this.m_trade.ResultDeal(); //--- generate CHARTEVENT_CUSTOM+6 event ushort curr_id=6; if(!this.m_ptr_event.Generate(curr_id,data)) PrintFormat("Failed to generate an event: %d",curr_id); } } //--- break; } //--- 5) position opening case CHARTEVENT_CUSTOM+5: { ulong ticket=(ulong)_data.dparam; ulong deal=(ulong)_data.dparam; //--- datetime now=TimeCurrent(); //--- check the deals & orders history if(HistorySelect(now-PeriodSeconds(PERIOD_H1),now)) if(HistoryDealSelect(deal)) { double deal_vol=HistoryDealGetDouble(deal,DEAL_VOLUME); ENUM_DEAL_ENTRY deal_entry=(ENUM_DEAL_ENTRY)HistoryDealGetInteger(deal,DEAL_ENTRY); //--- if(deal_entry==DEAL_ENTRY_IN) { //--- to log? if(InpIsLogging) { Print("\nNew position for: "+_Symbol); PrintFormat("Volume: %0.2f",deal_vol); } } } //--- break; } //--- 6) position closing case CHARTEVENT_CUSTOM+6: { ulong ticket=(ulong)_data.dparam; ulong deal=(ulong)_data.dparam; //--- datetime now=TimeCurrent(); //--- check the deals & orders history if(HistorySelect(now-PeriodSeconds(PERIOD_H1),now)) if(HistoryDealSelect(deal)) { double deal_vol=HistoryDealGetDouble(deal,DEAL_VOLUME); ENUM_DEAL_ENTRY deal_entry=(ENUM_DEAL_ENTRY)HistoryDealGetInteger(deal,DEAL_ENTRY); //--- if(deal_entry==DEAL_ENTRY_OUT) { //--- to log? if(InpIsLogging) { Print("\nClosed position for: "+_Symbol); PrintFormat("Volume: %0.2f",deal_vol); } } } //--- break; } //--- 7) stop trading case CHARTEVENT_CUSTOM+7: { datetime stop_time=(datetime)_data.dparam; //--- this.m_is_trade=false; //--- to log? if(InpIsLogging) PrintFormat("Expert trading is stopped at: %s", TimeToString(stop_time,TIME_DATE|TIME_MINUTES|TIME_SECONDS)); //--- break; } //--- 8) resume trading case CHARTEVENT_CUSTOM+8: { datetime resume_time=(datetime)_data.dparam; this.m_is_trade=true; //--- to log? if(InpIsLogging) PrintFormat("Expert trading is resumed at: %s", TimeToString(resume_time,TIME_DATE|TIME_MINUTES|TIME_SECONDS)); //--- break; } } } } }
它由两部分组成。第一部分是处理事件,与"按钮" 对象的点击相连。这个点击将产生一个外部自定义事件,这将由之后的处理器进行处理。
第二部分设计用来处理生成的自定义事件。它包括两块,在一个相关事件已被处理之后,一个新的又被生成。该 "收到一个开仓信号" 事件在第一块中被处理。若成功处理,则生成一个新的订单事件 "开仓"。该 "收到平仓信号" 事件在第二块中被处理。如果信号已被处理,则 "平仓" 事件发生。
该 EA CustomEventProcessor.mq5 是使用 CEventProcessor 类的极好例程。该 EA 被设计用来创建事件以及适当地响应它们。利用 OPP 范例,我们可以将源代码的行数尽量减至最低。EA 的源代码可以在本文的附件中找到。
在我看来,没必要每次都引用自定义事件机制。在策略中,有许多次要的,微不足道和无关紧要的事情,可以有不同的形式。
结论
在本文中,我尝试描绘 MQL5 环境当中的自定义事件的工作原则。我希望本文中涉及的思想,可以引发具有不同经验的程序员的兴趣,而不仅仅是那些新手。
我很高兴 MQL5 语言正在继续发展。也许,在不久的将来,会有类模板,而且或许会有指向函数的指针。那么,我们就能够写出一个不折不扣的委托,指向任意对象的方法。
存档中的源文件可以放置到项目文件夹中。就我而言,它是文件夹 MQL5\Projects\ChartUserEvent。
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/1163
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.




语言中没有代理,总的来说,缺少了很多东西。甚至连错误处理程序都没有。在这种情况下读到语言开发者的评论,说你应该在编写时捕捉所有错误,真是有趣。对这门语言感到失望 =(