用于在EA交易中包含指标的现成模板(第一部分):振荡指标·进阶篇
📊

用于在EA交易中包含指标的现成模板(第一部分):振荡指标·进阶篇

(2/3)· 14个振荡指标的输入变量与生命周期管理,省去反复翻文档的重复劳动

偏理论 第 2/3 篇
把指标硬塞进EA时,多数人直接抄iCustom参数,漏掉句柄有效性检查,行情重启后EA静默失联。线形状态靠肉眼判断,回测和实盘的信号经常对不上。模板化封装不是炫技,是少踩坑的底线。

枚举线与面板变量的底层骨架

这段声明把价格相对基准线的七种状态先固化成枚举:上穿、下穿、从下触、从上触、等于,外加单纯的上方与下方。实盘里你若想区分“刺穿”和“贴着走”,靠的就是 LINE_STATE_CROSS_UP 与 LINE_STATE_TOUCH_BELOW 这类细分标签,而不是肉眼估。 紧跟着的输入参数把 ATR 周期写死成 14,这是 MT5 里 iATR 的默认回看窗口;全局里 handle 初始化为 INVALID_HANDLE,意味着指标句柄在 OnInit 之前一律无效,漏了这步就会在后续 CopyBuffer 直接返回 -1。 面板相关变量里 mouse_bar_index 负责记录鼠标取数那根 K 线的序号,CDashboard *panel 则用空指针占位,等资源创建后再挂实体。开 MT5 把这段贴进 EA 头部,编译能过则说明你的枚举与全局声明没有作用域冲突。

MQL5 / C++
  LINE_STATE_ABOVE,        class=class="str">"cmt">// Above value
  LINE_STATE_UNDER,        class=class="str">"cmt">// Below value
  LINE_STATE_CROSS_UP,     class=class="str">"cmt">// Crossing value upwards
  LINE_STATE_CROSS_DOWN,   class=class="str">"cmt">// Crossing value downwards
  LINE_STATE_TOUCH_BELOW,  class=class="str">"cmt">// Touching value from below
  LINE_STATE_TOUCH_ABOVE,  class=class="str">"cmt">// Touch value from above
  LINE_STATE_EQUALS,       class=class="str">"cmt">// Equal to value
  };
class=class="str">"cmt">//--- input parameters
input class="type">uint   InpPeriod  =  class="num">14;   class=class="str">"cmt">/* ATR Period */
class=class="str">"cmt">//--- global variables
class="type">int     handle=INVALID_HANDLE;   class=class="str">"cmt">// Indicator handle
class="type">int     period=class="num">0;                class=class="str">"cmt">// ATR calculation period
class="type">int     ind_digits=class="num">0;            class=class="str">"cmt">// Number of decimal places in the indicator values
class="type">class="kw">string  ind_title;               class=class="str">"cmt">// Indicator description
class=class="str">"cmt">//--- variables for the panel
class="type">int     mouse_bar_index;         class=class="str">"cmt">// Index of the bar the data is taken from
CDashboard *panel=NULL;          class=class="str">"cmt">// Pointer to the panel object

「EA初始化里把周期归零的坑」

写EA时有个容易忽略的兼容点:很多指标在输入周期值为0时会回退到默认周期,EA最好也照做。把平滑周期参数设成0,就应该让内部走和标准指标一致的默认逻辑,否则面板数据和原指标对不上。 另外部分指标周期填1并不计算,最短有效周期是2;不同品种的小数位数(Digits)也不一样,初始化阶段就要把这些都处理掉,不然跨品种加载可能显示错位。 下面这段OnInit就是按上述规则写的:周期小于1统一当14处理,顺手用Digits()锁定小数位,再建ATR句柄。若句柄创建失败直接INIT_FAILED并打错误码,方便在MT5日志里抓。 [CODE] //+------------------------------------------------------------------+

//Expert initialization function

//+------------------------------------------------------------------+ int OnInit() { //--- create timer EventSetTimer(60); //--- Indicator //--- Set and adjust the calculation period if necessary period=int(InpPeriod<1 ? 14 : InpPeriod); //--- Set the indicator name and the number of decimal places ind_title=StringFormat("ATR(%lu)",period); ind_digits=Digits(); //--- Create indicator handle ResetLastError(); handle=iATR(Symbol(),PERIOD_CURRENT,period); if(handle==INVALID_HANDLE) { PrintFormat("%s: Failed to create indicator handle %s. Error %ld",__FUNCTION__,ind_title,GetLastError()); return INIT_FAILED; } //--- Successful initialization return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //+------------------------------------------------------------------+

//Expert initialization function

//+------------------------------------------------------------------+ int OnInit() { //--- create timer EventSetTimer(60); //--- Indicator //--- Set and adjust the calculation period if necessary period=int(InpPeriod<1 ? 14 : InpPeriod); //--- Set the indicator name and the number of decimal places ind_title=StringFormat("ATR(%lu)",period); ind_digits=Digits(); //--- Create indicator handle ResetLastError(); handle=iATR(Symbol(),PERIOD_CURRENT,period); if(handle==INVALID_HANDLE) { PrintFormat("%s: Failed to create indicator handle %s. Error %ld",__FUNCTION__,ind_title,GetLastError()); return INIT_FAILED; } //--- Dashboard //--- Create the panel panel=new CDashboard(1,20,20,199,225); if(panel==NULL) { Print("Error. Failed to create panel object"); return INIT_FAILED; } //--- Set font parameters panel.SetFontParams("Calibri",9); //--- Display the panel with the "Symbol, Timeframe description" header text

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- create timer
   EventSetTimer(class="num">60);
class=class="str">"cmt">//--- Indicator
class=class="str">"cmt">//--- Set and adjust the calculation period if necessary
   period=class="type">int(InpPeriod<class="num">1 ? class="num">14 : InpPeriod);
class=class="str">"cmt">//--- Set the indicator name and the number of decimal places
   ind_title=StringFormat("ATR(%lu)",period);
   ind_digits=Digits();
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iATR(Symbol(),PERIOD_CURRENT,period);
   if(handle==INVALID_HANDLE)
     {
       PrintFormat("%s: Failed to create indicator handle %s. Error %ld",__FUNCTION__,ind_title,GetLastError());
       class="kw">return INIT_FAILED;
     }
class=class="str">"cmt">//--- Successful initialization
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- create timer
   EventSetTimer(class="num">60);
class=class="str">"cmt">//--- Indicator
class=class="str">"cmt">//--- Set and adjust the calculation period if necessary
   period=class="type">int(InpPeriod<class="num">1 ? class="num">14 : InpPeriod);
class=class="str">"cmt">//--- Set the indicator name and the number of decimal places
   ind_title=StringFormat("ATR(%lu)",period);
   ind_digits=Digits();
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iATR(Symbol(),PERIOD_CURRENT,period);
   if(handle==INVALID_HANDLE)
     {
       PrintFormat("%s: Failed to create indicator handle %s. Error %ld",__FUNCTION__,ind_title,GetLastError());
       class="kw">return INIT_FAILED;
     }
class=class="str">"cmt">//--- Dashboard
class=class="str">"cmt">//--- Create the panel
   panel=new CDashboard(class="num">1,class="num">20,class="num">20,class="num">199,class="num">225);
   if(panel==NULL)
     {
       Print("Error. Failed to create panel object");
       class="kw">return INIT_FAILED;
     }
class=class="str">"cmt">//--- Set font parameters
   panel.SetFontParams("Calibri",class="num">9);
class=class="str">"cmt">//--- Display the panel with the "Symbol, Timeframe description" header text

◍ 面板表格的布局与初始化落点

在指标初始化阶段,先把交易品种和周期拼成标题写进面板:Symbol() 取当前品种,Period() 转枚举后从下标 7 截掉前缀,拼成 "EURUSD, H1" 这类显示串。 随后建两张表——ID 0 放 K 线数据,ID 1 放指标数据。DrawGrid(0,2,20,6,2,18,97) 表示表 0 从第 2 列、Y=20 像素起画,6 行 2 列,列宽 18、行高 97;表 1 的 Y 坐标不写死,而是取表 0 的底边 TableY2(0) 再加 22 像素,避免两张表叠在一起。 最后把两张表内容用 GridPrint 打到日志便于排查,鼠标游标柱索引初设为 0,并立刻用 DrawData(0,TimeCurrent()) 把当前柱刷到面板上。外汇与贵金属波动剧烈、杠杆风险高,这段代码仅解决显示层,不含任何方向判断。

MQL5 / C++
  panel.View(Symbol()+", "+StringSubstr(EnumToString(Period()),class="num">7));
class=class="str">"cmt">//--- Create a table with ID class="num">0 to display bar data in it
  panel.CreateNewTable(class="num">0);
class=class="str">"cmt">//--- Draw a table with ID class="num">0 on the panel background
  panel.DrawGrid(class="num">0,class="num">2,class="num">20,class="num">6,class="num">2,class="num">18,class="num">97);
class=class="str">"cmt">//--- Create a table with ID class="num">1 to display indicator data in it
  panel.CreateNewTable(class="num">1);
class=class="str">"cmt">//--- Get the Y2 table coordinate with ID class="num">0 and
class=class="str">"cmt">//--- set the Y1 coordinate for the table with ID class="num">1
  class="type">int y1=panel.TableY2(class="num">0)+class="num">22;
class=class="str">"cmt">//--- Draw a table with ID class="num">1 on the panel background
  panel.DrawGrid(class="num">1,class="num">2,y1,class="num">3,class="num">2,class="num">18,class="num">97);

class=class="str">"cmt">//--- Display tabular data in the journal
  panel.GridPrint(class="num">0,class="num">2);
  panel.GridPrint(class="num">1,class="num">2);
class=class="str">"cmt">//--- Initialize the variable with the index of the mouse cursor bar
  mouse_bar_index=class="num">0;
class=class="str">"cmt">//--- Display the data of the current bar on the panel
  DrawData(mouse_bar_index,TimeCurrent());
class=class="str">"cmt">//--- Successful initialization
  class="kw">return(INIT_SUCCEEDED);
}

EA退出时怎么清干净指标和面板

EA从图表卸载时,OnDeinit()是最后一道关口。这里不把申请的资源还回去,MT5终端里会留下悬空句柄和孤儿对象,多次加载卸载后可能拖慢终端响应。 第一段代码只做了三件事:杀掉定时器、释放指标句柄、清空图表上的Comment文字。IndicatorRelease(handle)若返回false,会用PrintFormat把错误码打进日志,方便你定位是句柄失效还是指标已被占用。 第二段补了面板对象的回收。panel指针若不为NULL,直接delete panel即可让自定义面板从图表消失。外汇与贵金属杠杆高,EA异常退出概率不低,这类清理逻辑写全了,回测和实盘切换时才不会莫名其妙多出一堆残留对象。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">const class="type">int reason)
  {
class=class="str">"cmt">//--- destroy timer
   EventKillTimer();
   
class=class="str">"cmt">//--- Release handle of the indicator
   ResetLastError();
   if(!IndicatorRelease(handle))
      PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError());
class=class="str">"cmt">//--- Clear all comments on the chart
   Comment("");
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">const class="type">int reason)
  {
class=class="str">"cmt">//--- destroy timer
   EventKillTimer();
   
class=class="str">"cmt">//--- Release handle of the indicator
   ResetLastError();
   if(!IndicatorRelease(handle))
      PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError());
class=class="str">"cmt">//--- Clear all comments on the chart
   Comment("");
   
class=class="str">"cmt">//--- If the panel object exists, class="kw">delete it
   if(panel!=NULL)
      class="kw">delete panel;
   }

「用 CopyBuffer 抓取指标缓冲区的实战取法」

在 MT5 里读任意指标线,核心就一个函数:CopyBuffer()。它把指定指标句柄、缓冲区编号和起始位置的数据搬进你的 double 数组,失败则返回 EMPTY_VALUE。函数有三种重载:按柱索引+数量、按起始时间+数量、按起止时间区间,我们盯盘 EA 里只用第一种——按柱索引取单点,逻辑最直。 为什么不全量拉一段数组再算偏移?那样要把结果塞进内存按索引换算,反而把“线态监测”搞复杂。直接取指定索引的值,再取另一个值做对比,既省内存又方便。比如取当前柱和前两根柱共三个值,足够拼出简单线形;用归一化差值和零比,就能判断升降。 线相对于水平位的判定,可以写个函数吃两个相邻柱的值,再和传入的 level0 / level1 比。level1 若留 EMPTY_VALUE,就只和 level0 比;若给了具体数,则 index+1 柱比 level1、index 柱比 level0,这样能抓两线交叉。 把这些函数原样塞进 EA,就能在 OnChartEvent 里随光标移动刷面板。编译后挂上默认参数 ATR,面板铭牌会随鼠标跳数;TestOscillationATR.mq5 是可照抄的样例。外汇贵金属波动剧烈,这类读取仅作状态参考,不代表方向确定性。

MQL5 / C++
class="type">int  CopyBuffer(
   class="type">int       indicator_handle,     class=class="str">"cmt">// indicator handle
   class="type">int       buffer_num,           class=class="str">"cmt">// indicator buffer index
   class="type">int       start_pos,            class=class="str">"cmt">// starting point 
   class="type">int       count,                class=class="str">"cmt">// amount to copy
   class="type">class="kw">double    buffer[]              class=class="str">"cmt">// array the data to be copied to
   );
class="type">int  CopyBuffer(
   class="type">int       indicator_handle,     class=class="str">"cmt">// indicator handle
   class="type">int       buffer_num,           class=class="str">"cmt">// indicator buffer index
   class="type">class="kw">datetime  start_time,           class=class="str">"cmt">// starting date
   class="type">int       count,                class=class="str">"cmt">// amount to copy
   class="type">class="kw">double    buffer[]              class=class="str">"cmt">// array the data to be copied to
   );
class="type">int  CopyBuffer(
   class="type">int       indicator_handle,     class=class="str">"cmt">// indicator handle
   class="type">int       buffer_num,           class=class="str">"cmt">// indicator buffer index
   class="type">class="kw">datetime  start_time,           class=class="str">"cmt">// starting date
   class="type">class="kw">datetime  stop_time,            class=class="str">"cmt">// end time
   class="type">class="kw">double    buffer[]              class=class="str">"cmt">// array the data to be copied to
   );
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the indicator data on the specified bar                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double IndicatorValue(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num)
  {
   class="type">class="kw">double array[class="num">1]={class="num">0};
   ResetLastError();
   if(CopyBuffer(ind_handle,buffer_num,index,class="num">1,array)!=class="num">1)
     {

◍ 用三段取值判定指标线姿态

判定指标线当前处于反转、延续还是停滞,核心是把相邻三根 K 线上的缓冲值拉出来比对。下面这段函数一次取 index、index+1、index+2 三个位置的数值,任一为 EMPTY_VALUE 就直接返回未定义状态,避免脏数据污染后续逻辑。 比大小之前必须用 NormalizeDouble 按指标小数位 ind_digits 对齐,否则浮点误差会让 value2-value1 在理论等于 0 时算出 1e-16 这类极小正数,把「停滞」误判成「方向」。向上反转要求 value2>value1 且 value0>value1,即中间那根是最低点;向上延续则是 value2<=value1 且 value0>value1。 向下一侧完全对称:value2<value1 且 value0<value1 为向下反转,value2>=value1 且 value0<value1 为向下延续,两者差值归一后等于 0 则归入对应方向的停滞态。外汇与贵金属杠杆高、滑点突发,这类线态判定只作概率参考,实盘须配合止损。 LineStateRelative 在函数尾部接着收口,多传 level0 与可选 level1 两个阈值,就能把「线在不在某区间」也编进状态机,省得在主循环里再写一层判断。

MQL5 / C++
ENUM_LINE_STATE LineState(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num)
  {
class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1,class="num">2) relative to the passed index
   class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index,  buffer_num);
   class="kw">const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num);
   class="kw">const class="type">class="kw">double value2=IndicatorValue(ind_handle,index+class="num">2,buffer_num);
class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value 
   if(value0==EMPTY_VALUE || value1==EMPTY_VALUE || value2==EMPTY_VALUE)
      class="kw">return LINE_STATE_NONE;
class=class="str">"cmt">//--- Line upward reversal(value2>value1 && value0>value1)
   if(NormalizeDouble(value2-value1,ind_digits)>class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0)
      class="kw">return LINE_STATE_TURN_UP;
class=class="str">"cmt">//--- Line upward direction(value2<=value1 && value0>value1)
   else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0)
      class="kw">return LINE_STATE_UP;
class=class="str">"cmt">//--- Line upward stop(value2<=value1 && value0==value1)
   else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0)
      class="kw">return LINE_STATE_STOP_UP;
class=class="str">"cmt">//--- Line downward reversal(value2<value1 && value0<value1)
   if(NormalizeDouble(value2-value1,ind_digits)<class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0)
      class="kw">return LINE_STATE_TURN_DOWN;
class=class="str">"cmt">//--- Line downward direction(value2>=value1 && value0<value1)
   else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0)
      class="kw">return LINE_STATE_DOWN;
class=class="str">"cmt">//--- Line downward stop(value2>=value1 && value0==value1)
   else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0)
      class="kw">return LINE_STATE_STOP_DOWN;
class=class="str">"cmt">//--- Undefined state
   class="kw">return LINE_STATE_NONE;
  }
交给小布盯盘看模板落地
这些振荡指标的句柄创建与线形状态判定逻辑,小布盯盘的AIGC已内置到对应品种页,打开即可对照自己EA的封装是否漏了析构与跨周期读取边界。

常见问题

常见原因是句柄创建失败或未做有效性轮询,以及读取索引超出缓冲区长度。模板里把初始化和读取拆开,就是先保句柄活再取数。
可以,小布盯盘的AIGC模块内置了多指标参数与生命周期封装示例,你贴入品种周期即可拿到可改写的骨架,不必从零翻参考文档。
ATR虽常作波动率尺,但在模板归类里以归一化线形参与状态判定,因此纳入振荡类统一封装参数与析构流程。
反转看值2大于值1且值1小于值0,停止则是值1等于值0,前者倾向趋势折返概率更高,后者多为动能暂歇,需结合水平交叉过滤。
通常70/30作超买超卖标高,但模板把标高作为参数暴露,实盘外汇贵金属波动剧烈时可能需按品种分段调整。