为EA交易提供指标的现成模板(第3部分):趋势指标·进阶篇
📈

为EA交易提供指标的现成模板(第3部分):趋势指标·进阶篇

(2/3)·从AMA到抛物线转向,复制粘贴即可在EA里拉出指标句柄与数据

含代码示例实战向 第 2/3 篇
不少交易者手写指标调用时把周期参数和句柄释放搞混,EA跑几天就报句柄无效。趋势类指标比振荡器更吃时间框架匹配,错配会在回测里掩盖、实盘里爆发。本篇给的是已验证的现成模板,别再从头造轮子。

◍ 枚举线与输入参数的骨架

这段声明把价格相对参考线的状态拆成了 7 种:上方、下方、向上穿越、向下穿越、从下触碰、从上触碰、等于。实际写 EA 时,靠这套枚举就能区分「碰线」和「穿线」,避免把回调触碰误判成趋势反转。 输入参数里 InpPeriod=9 是 AMA 默认周期,快线 InpPeriodFast=2、慢线 InpPeriodSlow=30,三者拉开差距才能看出自适应均线的斜率变化。InpShift=0 表示不对齐偏移,InpPrice 锁定 PRICE_CLOSE,用收盘价算更抗影线噪声。 全局变量里 handle 存指标句柄,period_fast / period_slow 分开记录两条 EMA 周期,ind_digits 抓小数位是为了面板显示不串位。最后 mouse_bar_index 和 CDashboard 指针是给悬浮面板用的,鼠标指哪根 K 线就取哪根的状态。外汇和贵金属波动大,这类状态判断只是信号过滤的一环,实盘仍属高风险。

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">//--- class="kw">input parameters
class="kw">input class="type">uint                InpPeriod      =  class="num">9;             class=class="str">"cmt">/* Period         */
class="kw">input class="type">uint                InpPeriodFast  =  class="num">2;             class=class="str">"cmt">/* Fast EMA Period*/
class="kw">input class="type">uint                InpPeriodSlow  =  class="num">30;            class=class="str">"cmt">/* Slow EMA Period*/
class="kw">input class="type">int                 InpShift       =  class="num">0;             class=class="str">"cmt">/* AMA Shift      */
class="kw">input ENUM_APPLIED_PRICE  InpPrice       =  PRICE_CLOSE;   class=class="str">"cmt">/* Applied Price  */
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">// AMA calculation period
class="type">int    period_fast=class="num">0;          class=class="str">"cmt">// Fast EMA calculation period
class="type">int    period_slow=class="num">0;          class=class="str">"cmt">// Slow EMA 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 启动时的句柄与面板挂载」

MT5 的 OnInit 是 EA 加载后第一个被执行的函数,所有指标句柄和图形面板都得在这里一次性建好,否则后续 OnTick 里取数会直接报 INVALID_HANDLE。 先用 EventSetTimer(60) 挂一个 60 秒心跳,保证即便行情静止也能周期性刷新仪表板;周期参数做了下限保护,InpPeriod 小于 1 时回退到 9,快线回退 2、慢线回退 30,避免用户输入 0 把 AMA 算崩。 iAMA 创建句柄后必须判空,失败就打印错误码并返回 INIT_FAILED,这一步能让你在 MT5 终端日志里直接看到是哪一行参数不合法。 如果策略带可视化面板,OnInit 里用 new CDashboard(1,20,20,197,225) 在坐标 (20,20) 拉出 197×225 的悬浮窗,字体固定 Calibri 9 号;面板对象创建失败同样返回 INIT_FAILED,终端会拒绝加载 EA。外汇与贵金属杠杆高,EA 初始化失败可能让你错过风控触发,建议每次改完参数先在策略测试器跑一遍 OnInit 日志。

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 and levels if necessary
   period=class="type">int(InpPeriod<class="num">1 ? class="num">9 : InpPeriod);
   period_fast=class="type">int(InpPeriodFast<class="num">1 ? class="num">2 : InpPeriodFast);
   period_slow=class="type">int(InpPeriodSlow<class="num">1 ? class="num">30 : InpPeriodSlow);
class=class="str">"cmt">//--- Set the indicator name and the number of decimal places
   ind_title=StringFormat("AMA(%lu,%lu,%lu)",period,period_fast,period_slow);
   ind_digits=Digits()+class="num">1;
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iAMA(Symbol(),PERIOD_CURRENT,period,period_fast,period_slow,InpShift,InpPrice);
   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">197,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);

面板与双表格的初始化落点

这段初始化逻辑把信息分层做得很直接:先用 Symbol() 拼接 Period() 的枚举字符串(从第7个字符截起)作为面板抬头,能一眼看清当前品种和周期。 创建两张表,ID 0 放 K 线数据、ID 1 放指标数据,DrawGrid 的参数里藏着布局细节——ID 0 的 Y 起点是 20、行高 6、列宽 18,ID 1 则靠 TableY2(0)+22 算出 Y1,避免和上面表格叠在一起。 最后把两张表用 GridPrint 推到日志,mouse_bar_index 置 0 并调 DrawData 画当前柱,返回 INIT_SUCCEEDED。开 MT5 把这段塞进 OnInit,能立刻看到带品种周期抬头的双层面板,外汇和贵金属品种的高波动下,这种分层比堆在一个框里更不容易看错行。

MQL5 / C++
class=class="str">"cmt">//--- Display the panel with the "Symbol, Timeframe description" header text
   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">96);
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">96);

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退出时把指标句柄和面板清干净

MT5 的 EA 在卸载或图表关闭时会走进 OnDeinit(),这一步最容易被忽略的是指标句柄没释放。句柄不回收,重复加载 EA 可能让终端里残留无主指标,累积到几十个后会拖慢报价刷新。 先杀掉定时器,再调用 IndicatorRelease(handle) 交还句柄。释放前用 ResetLastError() 清掉旧错误码,失败时打印具体错误号,方便在「专家日志」里定位是哪次加载出的问题。 如果 EA 自己画了仪表板面板对象,得在析构里判空后 delete。panel 指针不为 NULL 才删,否则对空指针 delete 会抛运行时异常。下面两段代码分别对应「只释放指标」和「释放指标+删面板」两种写法,直接贴进 MT5 的 EA 模板就能编译。 外汇和贵金属品种波动大、滑点频繁,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;
   }

「把指标缓冲读进EA的正确姿势」

做价格行为辅助判定时,EA 必须先把指标缓冲区的数值抓到本地数组里,否则面板上的线状态全是空谈。下面这套取数函数可以直接塞进自定义程序里用,不需要改结构。 核心入口是 IndicatorValue(),它用 CopyBuffer 按指定句柄、偏移量和缓冲区序号取 1 个值。若返回值等于 EMPTY_VALUE,说明这一根柱子的数据没拿全,调用方要自己兜底。 LineState() 则连续取 index、index+1、index+2 三根柱的值,用相邻差值的符号给线段打状态:向上反转、向上运行、向上停顿、向下反转、向下运行、向下停顿共六类。注意它用 NormalizeDouble 按 ind_digits 精度对齐,避免浮点误差把状态判错。 编译挂到图表后,面板会实时刷这些状态和数值;外汇与贵金属波动大、滑点频繁,这类读取在跳空时可能返回异常,需在实盘前用 MT5 策略测试器跑几组极端行情验证。

MQL5 / C++
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)
     {
      PrintFormat("%s: CopyBuffer failed. Error %ld",__FUNCTION__,GetLastError());
      class="kw">return EMPTY_VALUE;
     }
   class="kw">return array[class="num">0];
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the state of the indicator line                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
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)

相对指定水平的线路状态判定

在拿到指标缓冲区的两根相邻值(index 与 index+1)后,真正有用的不是数值本身,而是它们相对某个水平线的空间关系。下面这个函数把这种关系压缩成枚举状态,方便后续逻辑直接 switch 而不用反复写浮点比较。 调用时若只传 level0,则 level1 取 EMPTY_VALUE,函数内部会把它回退成 level0,也就是用同一条水平线做上下两侧判断;传了 level1 才变成双水平带区间判定。 比较全部走 NormalizeDouble(..., ind_digits),ind_digits 一般取自指标小数位。这样能避开黄金 0.001 与美日 0.001 不同精度下的相等误判——比如 value0-level0 严格等于 0 才返回 TOUCH 类状态,否则哪怕差 1e-9 也会落进 CROSS 或 UNDER/ABOVE。 注意原文里有一处明显笔误:从上方触碰水平线本应返回 LINE_STATE_TOUCH_ABOVE,但代码写成了 LINE_STATE_TOUCH_BELOW。开 MT5 跑这套时若发现触顶信号被归到触底桶里,优先查这一行而不是怀疑自己的调用参数。外汇与贵金属杠杆高,这类状态错分可能让策略在反转点反向开仓,实盘前务必在策略测试器用历史 tick 复验。

MQL5 / C++
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;
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the state of the line relative to the specified level       |
class=class="str">"cmt">//+------------------------------------------------------------------+
ENUM_LINE_STATE LineStateRelative(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num,class="kw">const class="type">class="kw">double level0,class="kw">const class="type">class="kw">double level1=EMPTY_VALUE)
   {
class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1) 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=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)
      class="kw">return LINE_STATE_NONE;
class=class="str">"cmt">//--- Define the second level to compare
   class="type">class="kw">double level=(level1==EMPTY_VALUE ? level0 : level1);
class=class="str">"cmt">//--- The line is below the level(value1<level && value0<level0)
   if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0)
      class="kw">return LINE_STATE_UNDER;
class=class="str">"cmt">//--- The line is above the level(value1>level && value0>level0)
   if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0)
      class="kw">return LINE_STATE_ABOVE;
class=class="str">"cmt">//--- The line crossed the level upwards(value1<=level && value0>level0)
   if(NormalizeDouble(value1-level,ind_digits)<=class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0)
      class="kw">return LINE_STATE_CROSS_UP;
class=class="str">"cmt">//--- The line crossed the level downwards(value1>=level && value0<level0)
   if(NormalizeDouble(value1-level,ind_digits)>=class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0)
      class="kw">return LINE_STATE_CROSS_DOWN;
class=class="str">"cmt">//--- The line touched the level from below(value1<level0 && value0==level0)
   if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0)
      class="kw">return LINE_STATE_TOUCH_BELOW;
class=class="str">"cmt">//--- The line touched the level from above(value1>level0 && value0==level0)
   if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0)
      class="kw">return LINE_STATE_TOUCH_BELOW;
class=class="str">"cmt">//--- Line is equal to the level value(value1==level0 && value0==level0)
   if(NormalizeDouble(value1-level,ind_digits)==class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0)
      class="kw">return LINE_STATE_EQUALS;
class=class="str">"cmt">//--- Undefined state
   class="kw">return LINE_STATE_NONE;
   }
让小布替你跑这套
这些趋势指标的诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到AMA与布林带的实时状态,把重复劳动交给小布,你专注决策。

常见问题

模板里给了默认参数组合,fast通常取2、slow取30上下;若品种波动特性不同,可在输入参数段改全局变量后重编译,不必动核心逻辑。
多在读取前未判断句柄有效或缓冲区索引写错。本篇读取数据节给出按带宽取上下轨的固定写法,直接套用可避坑。
它包含转折线、基准线、延展带等多组线,模板中拆成独立读取函数,初始化时建全句柄,析构统一释放即可。
可以,小布盯盘的品种页已集成AMA、布林带等趋势诊断,无需自行编译EA也能看信号倾向,外汇贵金属波动大属高风险,仅作参考。
在参数小节对应的全局输入变量中调整,初始化用iSAR()建句柄,读取数据节示范了按柱号取值的写法。
FRAMA用分形维数调平滑,对突变更钝;模板里两者并列,初始化与析构结构一致,换函数名和参数即可互换。