MQL5 Cookbook: 处理自定义图表事件·进阶篇
(2/3)· 从 CHARTEVENT_CUSTOM 到类封装,解决 EA 里事件泛滥却无人分类的烂摊子
「事件处理类的私有接口与启动流程」
CEventProcessor 把开平仓逻辑收敛到私有段:Close、Open 负责执行,CheckCloseSignal / CheckOpenSignal 用 ENUM_ORDER_TYPE 返回信号类型,GetIndicatorData 则以数组引用方式取快慢线数值。这种划分让外部只碰 ProcessEvent,内部状态不外露。 Start 方法先 new 一个 CIndicatorEvent 对象,用 CheckPointer 确认是 POINTER_DYNAMIC 后才继续。随后把 magic 号塞进 data.lparam,调用 Generate(1, data) 抛出 CHARTEVENT_CUSTOM+1 事件,等于给指标层发个心跳。 按钮创建坐标写死在 Create(0,"Start_stop_btn",0,25,25,150,50):左上角偏移 25,25,宽 150 高 50。创建成功就跑 ButtonStop 把 m_button_state 置 false,返回 true;任何一步失败 Start 直接返回 false。外汇与贵金属 EA 加载时若按钮没出来,优先怀疑这组坐标被其他面板占了。 Finish 里先 ResetEvent 清场,再 new 一个事件对象准备收尾。注意它和 Start 都重复 new CIndicatorEvent,说明原设计里事件对象不跨生命周期复用,改代码时别手滑写成成员长期持有。
class="type">void ProcessEvent(class="kw">const class="type">class="kw">ushort _event_id,class="kw">const SEventData &_data); class="kw">private: class=class="str">"cmt">//--- Procedures class="type">void Close(class="type">void); class="type">void Open(class="type">void); class=class="str">"cmt">//--- Functions ENUM_ORDER_TYPE CheckCloseSignal(class="kw">const ENUM_ORDER_TYPE _close_sig); ENUM_ORDER_TYPE CheckOpenSignal(class="kw">const ENUM_ORDER_TYPE _open_sig); class="type">bool GetIndicatorData(class="type">class="kw">double &_fast_vals[],class="type">class="kw">double &_slow_vals[]); class=class="str">"cmt">//--- Macros class="type">void ResetEvent(class="type">void); class="type">bool ButtonStop(class="type">void); class="type">bool ButtonResume(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Start module | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CEventProcessor::Start(class="type">void) { class=class="str">"cmt">//--- create an indicator event object this.m_ptr_event=new CIndicatorEvent(); if(CheckPointer(this.m_ptr_event)==POINTER_DYNAMIC) { SEventData data; data.lparam=(class="type">long)this.m_magic; class=class="str">"cmt">//--- generate CHARTEVENT_CUSTOM+class="num">1 event if(this.m_ptr_event.Generate(class="num">1,data)) class=class="str">"cmt">//--- create a button if(this.m_button.Create(class="num">0,"Start_stop_btn",class="num">0,class="num">25,class="num">25,class="num">150,class="num">50)) if(this.ButtonStop()) { this.m_button_state=class="kw">false; class="kw">return true; } } class=class="str">"cmt">//--- class="kw">return class="kw">false; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Finish module | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CEventProcessor::Finish(class="type">void) { class=class="str">"cmt">//--- reset the event object this.ResetEvent(); class=class="str">"cmt">//--- create an indicator event object this.m_ptr_event=new CIndicatorEvent(); if(CheckPointer(this.m_ptr_event)==POINTER_DYNAMIC) { SEventData data;
◍ 用自定义事件驱动每根K线的开关仓
EA 里想让策略只在每根新 K 线触发一次,又不想把逻辑全堆在主循环里,可以借图表自定义事件做解耦。下面这段把 magic 号塞进事件参数,再发出 CHARTEVENT_CUSTOM+2,随即将事件送进自身处理函数,相当于内部广播一次信号。 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); 上面三行是事件派发的核心:Generate 第二个参数 2 代表自定义事件偏移号,false 表示不广播给其它图表。若返回 true,立刻本地处理,省去等待系统消息队列。 主循环里用静态 CisNewBar 判新柱,只在 m_is_init 与 m_is_trade 同时为真时才跑。一旦 isNewBar() 成立就先 Close 再 Open,等于每根 BAR 重置一次头寸逻辑。 void CEventProcessor::Main(void) { static CisNewBar newBar; if(this.m_is_init) if(this.m_is_trade) if(newBar.isNewBar()) { this.Close(); this.Open(); } } 按钮交互走 CHARTEVENT_OBJECT_CLICK:比对 sparam 与按钮名,若状态从未按到按下,就 ButtonResume 并重建外部事件对象。外汇与贵金属波动剧烈,这类自动重开仓逻辑在高滑点时段可能连续触发,实盘前务必在 MT5 策略测试器用 2023 年 XAUUSD 数据跑一遍确认间隔。
data.lparam=(class="type">long)this.m_magic; class=class="str">"cmt">//--- generate CHARTEVENT_CUSTOM+class="num">2 event class="type">bool is_generated=this.m_ptr_event.Generate(class="num">2,data,class="kw">false); class=class="str">"cmt">//--- process CHARTEVENT_CUSTOM+class="num">2 event if(is_generated) this.ProcessEvent(CHARTEVENT_CUSTOM+class="num">2,data); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Main | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CEventProcessor::Main(class="type">void) { class=class="str">"cmt">//--- a new bar object class="kw">static CisNewBar newBar; class=class="str">"cmt">//--- if initialized if(this.m_is_init) class=class="str">"cmt">//--- if not paused if(this.m_is_trade) class=class="str">"cmt">//--- if a new bar if(newBar.isNewBar()) { class=class="str">"cmt">//--- close module this.Close(); class=class="str">"cmt">//--- open module this.Open(); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Process event module | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CEventProcessor::ProcessEvent(class="kw">const class="type">class="kw">ushort _event_id,class="kw">const SEventData &_data) { class=class="str">"cmt">//--- check event id if(_event_id==CHARTEVENT_OBJECT_CLICK) { class=class="str">"cmt">//--- button click if(StringCompare(_data.sparam,this.m_button.Name())==class="num">0) { class=class="str">"cmt">//--- button state class="type">bool button_curr_state=this.m_button.Pressed(); class=class="str">"cmt">//--- to stop if(button_curr_state && !this.m_button_state) { if(this.ButtonResume()) { this.m_button_state=true; class=class="str">"cmt">//--- reset the event object this.ResetEvent(); class=class="str">"cmt">//--- create an external event object this.m_ptr_event=new CExternalEvent(); class=class="str">"cmt">//--- if(CheckPointer(this.m_ptr_event)==POINTER_DYNAMIC)
正文
{ SEventData data; data.lparam=(<span class="keyword">long</span>)<span class="keyword">this</span>.m_magic; data.dparam=(<span class="keyword">double</span>)<span class="functions">TimeCurrent</span>(); <span class="comment">//--- generate CHARTEVENT_CUSTOM+7 event</span> <span class="keyword">ushort</span> curr_id=<span class="number">7</span>; <span class="keyword">if</span>(!<span class="keyword">this</span>.m_ptr_event.Generate(curr_id,data)) <span class="functions">PrintFormat</span>(<span class="string">"Failed to generate an event: %d"</span>,curr_id); &
「用自定义事件触发双均线初始化」
EA 里把指标创建和销毁从 OnTick 剥离,改用图表自定义事件驱动,能避免每帧重复建句柄。下面这段 switch 挂在 OnChartEvent 的 CHARTEVENT_CUSTOM 分支,靠 +1 / +2 两个事件号区分「建」与「拆」。 事件 +1 时先建快线 EMA(21) 再建慢线 EMA(55),都吃收盘价、偏移 0。只有两个 Handle 都不是 INVALID_HANDLE,才把 m_is_init 置真并写入魔术码与滑点(InpSlippage 点)。 事件 +2 走 IndicatorRelease 释放快慢线句柄,返回布尔存 is_slow_released / is_fast_released。若两者不都为真且开了 InpIsLogging,就进日志分支——外汇与贵金属杠杆高,句柄泄漏会拖慢终端,释放失败必须可观测。 直接在 MT5 策略测试器外挂脚本里 EventChartCustom 发 +1,看 m_is_init 是否翻转;发 +2 后用手动 Print 查句柄是否归零。
{
class=class="str">"cmt">//--- process the definite user event
class="kw">switch(_event_id)
{
class=class="str">"cmt">//--- class="num">1) indicator creation
case CHARTEVENT_CUSTOM+class="num">1:
{
class=class="str">"cmt">//--- create a fast ema
if(this.m_fast_ema.Create(_Symbol,_Period,class="num">21,class="num">0,MODE_EMA,PRICE_CLOSE))
if(this.m_slow_ema.Create(_Symbol,_Period,class="num">55,class="num">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);
class=class="str">"cmt">//---
this.m_is_init=true;
}
class=class="str">"cmt">//---
class="kw">break;
}
class=class="str">"cmt">//--- class="num">2) indicator deletion
case CHARTEVENT_CUSTOM+class="num">2:
{
class=class="str">"cmt">//---release indicators
class="type">bool is_slow_released=IndicatorRelease(this.m_fast_ema.Handle());
class="type">bool is_fast_released=IndicatorRelease(this.m_slow_ema.Handle());
if(!(is_slow_released && is_fast_released))
{
class=class="str">"cmt">//--- to log?
if(InpIsLogging)◍ 自定义事件触发后的开仓价与止损计算
当图表收到 CHARTEVENT_CUSTOM+3 这类自定义事件时,EA 会先抓取当前品种的实时tick,再依据信号方向还原开仓价与防护价位。买价取自 ask、卖价取自 bid,这一步决定了后续所有距离的基准点。
信号类型由 _data.dparam 强制转为 ENUM_ORDER_TYPE,若是 ORDER_TYPE_BUY 则 coeff 置 1,若是 ORDER_TYPE_SELL 则 coeff 置 -1。止损价按 open_pr - coeff*InpStopLoss*_Point 推算,止盈价按 open_pr + coeff*InpStopLoss*_Point 推算,注意原文中止盈那行变量名有笔误(InpStopLos),实盘复制时大概率要改回 InpStopLoss。
得到原始价格后必须过一遍 NormalizeDouble(...,_Digits),否则在报价小数位为 3 或 5 的贵金属品种上,MT5 会直接拒单。外汇与贵金属杠杆高、滑点随机,这套计算只解决“价怎么来”,不保证信号本身胜率。
case CHARTEVENT_CUSTOM+class="num">3: { class="type">MqlTick last_tick; if(SymbolInfoTick(_Symbol,last_tick)) { ENUM_ORDER_TYPE open_ord_type=(ENUM_ORDER_TYPE)_data.dparam; class="type">class="kw">double open_pr,sl_pr,tp_pr,coeff; open_pr=sl_pr=tp_pr=coeff=class="num">0.; if(open_ord_type==ORDER_TYPE_BUY) { open_pr=last_tick.ask; coeff=class="num">1.; } else if(open_ord_type==ORDER_TYPE_SELL) { open_pr=last_tick.bid; coeff=-class="num">1.; } sl_pr=open_pr-coeff*InpStopLoss*_Point; tp_pr=open_pr+coeff*InpStopLos*_Point; open_pr=NormalizeDouble(open_pr,_Digits); sl_pr=NormalizeDouble(sl_pr,_Digits);
开仓后的事件复位与自定义事件派发
持仓真正打开之后,代码先对止盈价做 NormalizeDouble(tp_pr,_Digits) 对齐报价精度,再调用 PositionOpen 以当前图表品种、计算好的订单类型、手数、开仓价、止损价和止盈价下单。若返回失败且 InpIsLogging 为真,会在终端打印 Failed to open the position: 加品种名,方便你直接在 MT5 日志里抓漏单。 下单成功分支里先 Sleep(InpTradePause) 留出券商撮合间隔,随后 ResetEvent() 清掉旧事件对象,再 new 一个 COrderEvent 准备向图表推事件。这里用 CheckPointer 确认是 POINTER_DYNAMIC 才继续,避免空指针导致 EA 在实时外汇或贵金属行情中异常退出,这类品种点差跳变频繁,防护不能省。 事件体里把 magic 塞进 lparam、成交单号 ResultDeal() 塞进 dparam,再 Generate(CHARTEVENT_CUSTOM+5 对应的 curr_id=5, data) 抛出自定义事件。若生成失败会 PrintFormat 报出 id=5,你可在 OnChartEvent 里监听这个 5 号事件做后续平仓或追踪止损逻辑。外汇与贵金属杠杆高,自动开仓后事件链路任一段失效都可能让仓位脱离管控,建议先用策略测试器跑一轮看事件触发率。
tp_pr=NormalizeDouble(tp_pr,_Digits); class=class="str">"cmt">//--- open the position if(!this.m_trade.PositionOpen(_Symbol,open_ord_type,InpTradeLot,open_pr, sl_pr,tp_pr)) { class=class="str">"cmt">//--- to log? if(InpIsLogging) Print("Failed to open the position: "+_Symbol); } else { class=class="str">"cmt">//--- pause Sleep(InpTradePause); class=class="str">"cmt">//--- reset the event object this.ResetEvent(); class=class="str">"cmt">//--- create an order event object this.m_ptr_event=new COrderEvent(); if(CheckPointer(this.m_ptr_event)==POINTER_DYNAMIC) { SEventData data; data.lparam=(class="type">long)this.m_magic; data.dparam=(class="type">class="kw">double)this.m_trade.ResultDeal(); class=class="str">"cmt">//--- generate CHARTEVENT_CUSTOM+class="num">5 event class="type">class="kw">ushort curr_id=class="num">5; if(!this.m_ptr_event.Generate(curr_id,data)) PrintFormat("Failed to generate an event: %d",curr_id); } }