MQL5 Cookbook: 处理自定义图表事件·进阶篇
🧩

MQL5 Cookbook: 处理自定义图表事件·进阶篇

(2/3)· 从 CHARTEVENT_CUSTOM 到类封装,解决 EA 里事件泛滥却无人分类的烂摊子

偏理论进阶 第 2/3 篇
很多交易者把自定义事件当成临时补丁,想到一个 EventChartCustom 就发一个,到最后 EA 里 65535 个槽位用了几十个却没人记得谁是谁。事件不分类,调试时只能靠打印硬猜,回测日志变成天书。

「事件处理类的私有接口与启动流程」

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,说明原设计里事件对象不跨生命周期复用,改代码时别手滑写成成员长期持有。

MQL5 / C++
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 数据跑一遍确认间隔。

MQL5 / C++
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)

正文

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;SEventData data; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;data.lparam=(<span class="keyword">long</span>)<span class="keyword">this</span>.m_magic; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;data.dparam=(<span class="keyword">double</span>)<span class="functions">TimeCurrent</span>(); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">//--- generate CHARTEVENT_CUSTOM+7 event</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">ushort</span> curr_id=<span class="number">7</span>; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(!<span class="keyword">this</span>.m_ptr_event.Generate(curr_id,data)) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="functions">PrintFormat</span>(<span class="string">"Failed to generate an event: %d"</span>,curr_id); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&

「用自定义事件触发双均线初始化」

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 查句柄是否归零。

MQL5 / C++
   {
      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 会直接拒单。外汇与贵金属杠杆高、滑点随机,这套计算只解决“价怎么来”,不保证信号本身胜率。

MQL5 / C++
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 号事件做后续平仓或追踪止损逻辑。外汇与贵金属杠杆高,自动开仓后事件链路任一段失效都可能让仓位脱离管控,建议先用策略测试器跑一轮看事件触发率。

MQL5 / C++
   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);
         }
      }
让小布替你跑这套事件分类
把指标、订单、外部三组事件的触发记录交给小布盯盘,AIGC 会按你定义的类结构自动标色,打开对应品种页就能看到哪类事件在刷屏,你只管判断信号真假。

常见问题

从 CHARTEVENT_CUSTOM 等于 1000 到 CHARTEVENT_CUSTOM_LAST 等于 66534,合计 65535 个,计算方式为 66534-1000+1。
分组降低概念模糊度,让推理路线有秩序;尤其外部事件最难正规化,先分源再演绎具体类型可避免后期逻辑纠缠。
OOP 能把事件类型、源、处理方法封进类,后续 EA 扩展新事件只改派生类,不改主循环,维护成本显著下降。
目前小布盯盘内置的是通用事件流诊断,你若按本文三组分类输出日志,它可自动归并显示;深度绑定需接 WebRequest 推送到品种页。
基础模块、决策模块、执行模块对应不同事件源,依此映射能让事件表直接反映系统架构,而非散落的补丁。