在 EA 中读取任意指标:CopyBuffer 中的箭头、线和颜色索引

13 十一月 2026, 08:30
Bohdan Klok
0
2

引言

每个使用指标的交易者迟早都会提出这样的要求:"还是这个指标,但要能自己交易。"程序员的回答听起来很简单:用 iCustom() 或 IndicatorCreate() 创建指标,用 CopyBuffer() 读取它的数值,信号出现时开仓。实际上,第一个问题根本与交易无关,而是:信号在哪个缓冲区里?

箭头指标可能用一个缓冲区存放买入箭头、另一个存放卖出箭头,也可能用一个缓冲区同时存放两者,再在旁边配一个颜色索引。"趋势"指标可能只画一条会变色的线,而颜色保存在一个从不绘制的单独缓冲区中。在 Market 上购买的已编译指标不附带源代码,因此无法查看其中的 SetIndexBuffer() 调用。数据窗口(Data Window)能帮上一点忙,但它每次只显示一根 K 线上被绘制的数值,颜色缓冲区和计算缓冲区则完全隐藏。

本文介绍如何从外部为任意指标回答这个问题:缓冲区是如何存储的,如何从另一个程序读取全部缓冲区,如何仅凭数值区分箭头缓冲区、线和颜色索引,如何检查信号是否重绘,以及如何把答案标回图表,让用户看到哪个编号对应哪个箭头。成果是三个附带源代码的小程序:一个检查图表上每个指标的脚本(在代码库 Code Base 中免费提供),一个包含所有类型缓冲区的演示指标,以及一个按四条通用规则交易任意指标的智能交易系统(EA);后两者的完整代码附在文末。

1. 指标缓冲区究竟是什么

指标把输出保存在用 SetIndexBuffer() 绑定的数组中。缓冲区共有三种:

  • INDICATOR_DATA:被绘制(线、柱状图、箭头)并显示在数据窗口中的数值;
  • INDICATOR_COLOR_INDEX:用于 DRAW_COLOR_* 样式,表示每根 K 线所用颜色的编号;
  • INDICATOR_CALCULATIONS:既不绘制也不显示的中间数组。

这三种缓冲区按指标声明它们的顺序统一编号,并且都可以用 CopyBuffer() 读取。这是让许多程序员感到意外的第一点:在图表上从来看不到的缓冲区依然可以读取,而对于彩色指标,它往往恰恰是最有用的那一个。

绘图(plot)和缓冲区不是一回事。一个 DRAW_COLOR_LINE 绘图使用两个缓冲区(数值和颜色索引),DRAW_COLOR_CANDLES 使用五个(四个价格和颜色索引),DRAW_COLOR_ARROW 使用两个(箭头的价格及其颜色)。标准安装包中的 Heiken Ashi 指标就是一个很好的例子:缓冲区 0 到 3 是 K 线的价格,缓冲区 4 是颜色索引,阳线为 0,阴线为 1。

第二点是"没有值"如何存储。线缓冲区通常在每根 K 线上都有数值。箭头缓冲区只在有箭头的 K 线上存放价格,其余位置是"空"值。哪个值代表空由作者决定:通常的选择是 EMPTY_VALUE(DBL_MAX),通过 PlotIndexSetDouble(PLOT_EMPTY_VALUE) 设置,但许多指标使用 0.0。如果读取方把 0.0 当作有效的箭头价格,就会在每根 K 线上都看到箭头。

为了有一个具体的观察对象,DemoBuffers.mq5 指标(源代码见文末)声明了所有类型的缓冲区:以线绘制的简单移动平均线、收盘价向上穿越该均线处的箭头、以彩色线绘制的指数移动平均线,以及作为计算缓冲区的该均线斜率。

SetIndexBuffer(0,ExtSma,INDICATOR_DATA);             // line
SetIndexBuffer(1,ExtCross,INDICATOR_DATA);           // arrows, EMPTY_VALUE where there is none
SetIndexBuffer(2,ExtEma,INDICATOR_DATA);             // colour line: the value
SetIndexBuffer(3,ExtEmaColor,INDICATOR_COLOR_INDEX); // colour line: 0 rising, 1 falling
SetIndexBuffer(4,ExtSlope,INDICATOR_CALCULATIONS);   // not drawn, not in the Data Window
PlotIndexSetInteger(1,PLOT_ARROW,233);
PlotIndexSetDouble(1,PLOT_EMPTY_VALUE,EMPTY_VALUE);

数据窗口为它显示三个数值:均线、箭头和指数均线。缓冲区 3 和 4 在那里看不到,然而线的颜色——这个指标最自然的"趋势"信号——恰恰保存在缓冲区 3 中。图1 显示了程序读取全部五个缓冲区时看到的内容。

图1. EURUSD H1 上的 DemoBuffers:由 Buffer Inspector Kb 脚本读出的两条线、一个箭头缓冲区、一个颜色索引和一个计算缓冲区。

2. 读取指标的全部缓冲区

对于已经加载到图表上的指标,无需知道其输入参数就能获得句柄:

for(int w=0; w<(int)ChartGetInteger(0,CHART_WINDOWS_TOTAL); w++)
   for(int i=0; i<ChartIndicatorsTotal(0,w); i++)
     {
      string name=ChartIndicatorName(0,w,i);
      int    h=ChartIndicatorGet(0,w,name);   // the same instance the user sees
      ...
      IndicatorRelease(h);                    // releases our handle, not the indicator
     }

这一点比看上去更重要:用户已经在图表上选好了指标的输入参数,而程序读取的正是这个实例。对于不在图表上的指标,可以使用带 IND_CUSTOM 的 IndicatorCreate(),它接受相对于 MQL5\Indicators 的名称和一个 MqlParam 数组。如果输入参数来自文本字段,例如 "14,0,1.5,true",就必须为每一项指定正确的类型:

int CreateByName(const string name,const string params)
  {
   MqlParam p[];
   ArrayResize(p,1);
   p[0].type=TYPE_STRING;               // the first element is always the name
   p[0].string_value=name;
   string parts[];
   const int n=(StringLen(params)>0) ? StringSplit(params,',',parts) : 0;
   ArrayResize(p,n+1);
   for(int i=0; i<n; i++)
     {
      string s=parts[i];
      StringTrimLeft(s);
      StringTrimRight(s);
      if(s=="true" || s=="false")
        { p[i+1].type=TYPE_BOOL;   p[i+1].integer_value=(s=="true"); }
      else if(StringFind(s,".")>=0 && MathIsValidNumber(StringToDouble(s)))
        { p[i+1].type=TYPE_DOUBLE; p[i+1].double_value=StringToDouble(s); }
      else if(s=="0" || StringToInteger(s)!=0)
        { p[i+1].type=TYPE_INT;    p[i+1].integer_value=StringToInteger(s); }
      else
        { p[i+1].type=TYPE_STRING; p[i+1].string_value=s; }
     }
   return(IndicatorCreate(_Symbol,_Period,IND_CUSTOM,ArraySize(p),p));
  }

使用这个函数时,记住三条规则能省下大量时间。数值必须遵循指标输入参数窗口中的顺序,而且不能跳过任何参数:要设置第三个参数,前两个也必须给出。枚举按其数值传递,因此 MODE_EMA 是 1,PRICE_CLOSE 也是 1。未给出的参数保持默认值,因此空字符串会完全按原样创建指标。

一共有多少个缓冲区?句柄不会告诉您,但 CopyBuffer() 会:对于不存在的缓冲区编号,它返回 -1。依次读取缓冲区 0、1、2……直到第一次失败,就能得到缓冲区总数,数据、颜色和计算缓冲区都包括在内。

两个实用细节:

  • 句柄创建后,指标需要时间完成计算;在计算完成之前,BarsCalculated(handle) 返回 -1,因此脚本用一个带 Sleep() 的短循环等待,而 EA 只需在下一个报价时重试;
  • 从 K 线 1,即最后一根已收盘的 K 线开始复制。许多箭头指标在正在形成的 K 线上的值会一直变化,直到该 K 线收盘;基于它的分析会显示出之后消失的箭头。

3. 区分箭头、线和颜色索引

把缓冲区在 N 根已收盘 K 线上的数值读入数组后,三个简单的统计量就足够了:

  • 有效值:既不是 EMPTY_VALUE 也不是 NaN 的数值;
  • 箭头:有效且不为 0.0 的数值;
  • 小整数:绝对值较小的整数值。

由此即可得出分类:

缓冲区存放的内容 在 500 根 K 线上的表现
无内容 没有有效值,或者全是零
箭头 有一些数值,但只出现在不超过 30% 的 K 线上
颜色索引 只有窄范围内的小整数(例如 0 和 1)
线 其他所有情况:(几乎)每根 K 线上都有数值
int valid=0, arrows=0, ints=0;
double lo=DBL_MAX, hi=-DBL_MAX;
for(int i=0; i<n; i++)
  {
   if(v[i]==EMPTY_VALUE || !MathIsValidNumber(v[i])) continue;
   valid++; lo=MathMin(lo,v[i]); hi=MathMax(hi,v[i]);
   if(v[i]!=0.0) arrows++;
   if(MathAbs(v[i]-MathRound(v[i]))<1e-9 && MathAbs(v[i])<64) ints++;
  }
if(valid==0 || (arrows==0 && hi==0.0 && lo==0.0))  kind="empty";
else if(arrows>0 && arrows<=n*0.3)                  kind="arrows";
else if(ints==valid && hi-lo<=16)                    kind="colour index";
else                                                 kind="line";

为什么是 30%?箭头指标标记的是转折点,而转折点本来就少:Fractals 只标记百分之几的 K 线,典型的"信号"指标不到 10%。在部分 K 线上为空的线(例如历史开头处的移动平均线)在大多数 K 线上仍然有数值。这个阈值是有意放宽的;接近阈值的情况会报告给用户,而不是悄悄地猜测。

为什么用"窄范围内的小整数"来识别颜色?颜色索引的取值为 0、1、2 等,每个值对应绘图的一种颜色。线也可能是整数(被作者取整的振荡指标、阶梯函数),但它的变化范围要比一个绘图可能拥有的寥寥几种颜色宽得多。

图2 显示了 Heiken Ashi 的结果:四条价格线,以及缓冲区 4 中取值为 0 和 1 的颜色索引——这正是 EA 对 K 线颜色变化作出反应所需要的。

图2. Heiken Ashi:四条价格线,以及缓冲区 4 中数据窗口不显示的颜色索引。

有几种情况值得单独说明,因为匆匆一看往往会在这些地方得出错误的答案:

  • 用 0.0 而不是 EMPTY_VALUE 存储的箭头。上面的分类已经把 0.0 视为"没有箭头",但只与 EMPTY_VALUE 比较的程序会在每根 K 线上触发。务必同时检查两者。
  • 每根 K 线上都有点,例如 Parabolic SAR。它们在图表上看起来像箭头,但缓冲区在每根 K 线上都有数值,因此它是一条线;信号是点跳到价格另一侧的那一刻,而不是点的存在。
  • 彩色箭头(DRAW_COLOR_ARROW)。箭头缓冲区给出 K 线和价格,旁边的颜色索引说明它是买入箭头还是卖出箭头。一个信号需要同时用到这两个缓冲区。
  • 计算缓冲区。它们通常被归类为线,数值往往与价格相差很远。它们值得一看:有些指标把平滑或过滤之前的"原始"信号保存在那里。

4. 向用户显示哪个箭头对应哪个缓冲区

一张写着"缓冲区 1:箭头,37 个,最近一个在 2026.10.07 08:00"的表格是正确的,但交易者仍然需要把日期与图表对照,才能看出缓冲区 1 是向上箭头还是向下箭头。把缓冲区编号直接写在箭头旁边要快得多:

for(int i=n-1, done=0; i>=0 && done<20; i--)
  {
   if(v[i]==EMPTY_VALUE || v[i]==0.0) continue;
   int      shift=n-i;                          // v[] runs from shift n (oldest) to shift 1
   datetime t=iTime(_Symbol,_Period,shift);
   string   obj=StringFormat("BufInspector_%d_%d",buffer,shift);
   ObjectCreate(0,obj,OBJ_TEXT,0,t,v[i]);       // at the arrow's own price
   ObjectSetString(0,obj,OBJPROP_TEXT,"B"+(string)buffer);
   done++;
  }

标签放在缓冲区中保存的数值处,对箭头指标而言就是绘制箭头的价格,因此 "B0" 和 "B1" 正好出现在箭头上。每个缓冲区使用一种颜色,两个箭头缓冲区就很容易区分。

图3. Examples\Fractals:B0 标在上分形上方,B1 标在下分形下方。

5. 指标会重绘吗?

存放箭头的缓冲区只是答案的一半。另一半是:箭头一旦出现在已收盘的 K 线上,是否会一直留在那里。有些指标在新 K 线到来时会移动或删除箭头——Fractals 按设计会对最后两根 K 线这样做,ZigZag 类指标涉及的范围则长得多。EA 如果按一个后来消失的箭头交易,交易的就是一个事后看来从未存在过的信号,它在策略测试器中的结果也不会与图表相符。

这项检查很容易自动化:按开盘时间记住最后几根已收盘 K 线的数值,等待几根新 K 线,再次读取同样的 K 线并进行比较。

datetime ExtTimes[];   // open times of the remembered bars
double   ExtValues[];  // buffer values on those bars
//--- remember the last 100 closed bars of buffer b
int Remember(const int handle,const int b)
  {
   if(CopyTime(_Symbol,_Period,1,100,ExtTimes)!=100) return(0);
   return(CopyBuffer(handle,b,1,100,ExtValues));
  }
//--- later: how many of the remembered bars changed their value?
int Changed(const int handle,const int b)
  {
   int changed=0;
   for(int i=0; i<ArraySize(ExtTimes); i++)
     {
      double v[];
      if(CopyBuffer(handle,b,ExtTimes[i],1,v)==1 && v[0]!=ExtValues[i])
         changed++;
     }
   return(changed);
  }

这里关键是按时间而不是按索引读取:新 K 线到来后,同一根 K 线的索引会变大。结果表明指标在往回多少根 K 线的范围内仍会改变主意,这个数字决定了应使用哪根信号 K 线。对于 Fractals,答案总是两根 K 线,这就是为什么要在 K 线 3 而不是 K 线 1 上读取分形。一个在十根 K 线之前仍会修改箭头的指标,不能在箭头首次出现的那根 K 线上交易,无论它在策略测试器中的图形看起来如何。

6. 没有可读取的缓冲区时

有些指标用 ObjectCreate() 而不是缓冲区来绘制箭头。箭头在图表上,数据窗口却是空的,CopyBuffer() 只返回线,或者什么也不返回。任何程序都无法通过指标句柄读取这类信号。值得直截了当地告诉用户这一点,否则寻找"正确缓冲区编号"的过程永远不会结束。脚本可以间接地发现这种情况:如果用户能看到箭头,指标却没有箭头缓冲区,那么它几乎肯定是在绘制对象。Buffer Inspector Kb 会在报告中指出这一点。

7. 从缓冲区到交易规则

一旦知道了缓冲区,四条规则就能覆盖交易者带来的几乎所有指标:

规则 典型指标 信号 K 线上的条件
出现箭头 Fractals、箭头指标 数值不为空且不为 0
缓冲区穿越某一水平 RSI 30/70、CCI +-100、围绕 0 波动的振荡指标 前值 < 水平 <= 当前值
一个缓冲区穿越另一个 MACD 主线/信号线、Stochastic %K/%D、Alligator 的各条线 前一根 a < b 且当前 a >= b
颜色索引变为某个值 Heiken Ashi、彩色均线、"趋势"柱 前值 != k 且当前值 == k

BufferSignalEA.mq5(源代码见文末)实现的正是这些规则,指标名称、指标的输入参数、规则和缓冲区编号都作为输入参数,因此同一个 EA 可以指向任意指标。它的核心是一个函数,读取信号 K 线及其前一根 K 线上的两个数值:

bool RuleFires(const ENUM_SIGNAL_RULE rule,const int buffer,const double level,const int buffer2)
  {
   if(rule==SIGNAL_OFF)
      return(false);
   double a[],b[];
   //--- a[0] = the bar before the signal bar, a[1] = the signal bar
   if(CopyBuffer(ExtHandle,buffer,InpSignalBar,2,a)!=2)
      return(false);
   switch(rule)
     {
      case SIGNAL_ARROW:
         return(!IsEmpty(a[1]) && a[1]!=0.0);
      case SIGNAL_CROSS_UP:
         return(!IsEmpty(a[0]) && !IsEmpty(a[1]) && a[0]<level && a[1]>=level);
      case SIGNAL_CROSS_DOWN:
         return(!IsEmpty(a[0]) && !IsEmpty(a[1]) && a[0]>level && a[1]<=level);
      case SIGNAL_BECOMES:
         return(!IsEmpty(a[1]) && MathAbs(a[1]-level)<0.5 && (IsEmpty(a[0]) || MathAbs(a[0]-level)>=0.5));
      case SIGNAL_LINE_UP:
      case SIGNAL_LINE_DOWN:
        {
         if(CopyBuffer(ExtHandle,buffer2,InpSignalBar,2,b)!=2)
            return(false);
         if(IsEmpty(a[0]) || IsEmpty(a[1]) || IsEmpty(b[0]) || IsEmpty(b[1]))
            return(false);
         if(rule==SIGNAL_LINE_UP)
            return(a[0]<b[0] && a[1]>=b[1]);
         return(a[0]>b[0] && a[1]<=b[1]);
        }
     }
   return(false);
  }

颜色索引以 0.5 的容差进行比较,因为它是以 double 类型存储的。决策在每根新 K 线上只做一次,并且在 OnTick() 中先于其他所有操作:

const datetime bar=iTime(_Symbol,_Period,0);
if(bar==ExtLastBar)
   return;
if(BarsCalculated(ExtHandle)<InpSignalBar+2)
   return;                                     // the indicator is not ready yet: try on the next tick
ExtLastBar=bar;
const bool buy =RuleFires(InpBuyRule, InpBuyBuffer, InpBuyLevel, InpBuyBuffer2);
const bool sell=RuleFires(InpSellRule,InpSellBuffer,InpSellLevel,InpSellBuffer2);
if(buy==sell || HasPosition())
   return;                                     // no signal, both at once, or already in the market

如果在正在形成的 K 线的每个报价上评估规则,它会多次触发,而且会对随后消失的信号触发;在已收盘的 K 线上每根 K 线只决策一次,能让策略测试器与实时图表保持一致。

为了检验每条规则确实会触发,我们在策略测试器中用五个指标运行了这个 EA,所有指标使用相同的简单出场方式(0.1 手,止损 300 点,止盈 600 点,同一时间只持有一个仓位)。下面的数字是对规则的功能测试,而不是交易成绩:不加任何过滤、没有仓位管理地按指标交易,并不是一个策略。

指标与规则 缓冲区 交易数 盈利交易
Fractals,出现箭头(信号 K 线 3) 买入 1,卖出 0 184 53
MACD,主线穿越信号线 0 和 1 160 46
RSI(14),向上穿越 30 / 向下穿越 70 0 113 36
Heiken Ashi,颜色变为 0 / 1 4 170 53
DemoBuffers,箭头买入,颜色 1 卖出 1 和 3 152 39

EURUSD H1,2025.10.01 - 2026.10.01,模型为"1 minute OHLC"。五次运行中没有出现一次失败的请求;交易数的差异来自每个指标在没有持仓时给出信号的频率。

8. 读取其他周期上的指标

同样的读取方法也适用于更高周期上的指标,大多数过滤条件正是这样构建的("仅当价格位于 H4 的 200 EMA 之上时才买入")。创建句柄时指定周期,该句柄的 K 线 1 就是该周期最后一根已收盘的 K 线:

int h4=iMA(_Symbol,PERIOD_H4,200,0,MODE_EMA,PRICE_CLOSE);
...
double ma[];
if(CopyBuffer(h4,0,1,1,ma)==1 && iClose(_Symbol,PERIOD_H4,1)>ma[0])
   allowBuy=true;

有两点需要说明。第一,程序启动时其他周期的数据可能尚未就绪:在终端构建好历史数据之前,BarsCalculated() 返回 -1,CopyBuffer() 失败,因此需要重复检查,而不能当作"没有信号"。第二,在较低周期上做决策时,切勿读取较高周期的 K 线 0。在 H1 图表上,正在形成的 H4 K 线每小时都在变化,因此读取它的过滤条件在测试器中会显得完美(测试器已经知道这根 H4 K 线如何收尾),而在实时图表上的表现却会不同。

9. 程序

  • Buffer Inspector Kb(脚本,MQL5\Scripts,在代码库中免费提供):把它拖到已经加载了该指标的图表上。它会检查每个窗口中的每个指标,把表格输出到"专家"日志,用 Comment() 显示在图表上,并为每个箭头缓冲区最近的几个箭头加上标签。如果在输入参数中填写指标名称,它会自行创建该指标,这对来自 Market 的已编译指标很有用。启用 "Only remove the labels" 运行它即可清理图表。
  • DemoBuffers.mq5(指标,MQL5\Indicators):一个包含线、箭头、带颜色索引的彩色线和计算缓冲区的指标,供实验使用。源代码见下文。
  • BufferSignalEA.mq5(EA,MQL5\Experts):按上述四条规则交易任意指标;指标、指标的输入参数、规则和缓冲区编号都是输入参数。源代码见下文。

结论

指标的缓冲区就是它的公共接口,即使没有源代码也是如此。读取全部缓冲区、按数值对它们分类、在图表上标注箭头,并检查数值在往回多远的范围内仍会变化,就能把"我该用哪个缓冲区编号?"从一个要问指标作者的问题,变成一次十秒钟的检查。同样的分类也是四条通用交易规则——箭头、水平、线交叉和颜色变化——的基础,它们让同一个 EA 能够在已收盘的 K 线上使用它从未见过的指标,从而使策略测试器显示的结果就是实时图表上将发生的情况。

源代码

将每个代码清单以其名称保存到所示的文件夹中(在终端中选择"文件 → 打开数据文件夹"),然后在 MetaEditor 中按 F7 编译。

DemoBuffers.mq5 (MQL5\Indicators)

//+------------------------------------------------------------------+
//|                                                  DemoBuffers.mq5 |
//|                                                      Bohdan Klok |
//|                             https://www.mql5.com/en/users/kbposht |
//+------------------------------------------------------------------+
#property copyright   "Bohdan Klok"
#property link        "https://www.mql5.com/en/users/kbposht"
#property version     "1.00"
#property description "Example: one indicator with every kind of buffer - a line, arrows,"
#property description "a colour line with its colour index, and a calculation buffer that is not drawn."
#property indicator_chart_window
#property indicator_buffers 5
#property indicator_plots   3
//--- plot 0: a plain line (buffer 0)
#property indicator_label1  "SMA"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrSilver
#property indicator_width1  1
//--- plot 1: arrows (buffer 1), drawn only where the close crosses the SMA upward
#property indicator_label2  "Cross up"
#property indicator_type2   DRAW_ARROW
#property indicator_color2  clrDodgerBlue
#property indicator_width2  2
//--- plot 2: a colour line (buffer 2 = value, buffer 3 = colour index)
#property indicator_label3  "Trend EMA"
#property indicator_type3   DRAW_COLOR_LINE
#property indicator_color3  clrSeaGreen,clrOrangeRed
#property indicator_width3  2
//--- input parameters
input int InpSmaPeriod=20;   // SMA period
input int InpEmaPeriod=50;   // EMA period
//--- indicator buffers
double ExtSma[];             // 0: line
double ExtCross[];           // 1: arrows, EMPTY_VALUE where there is none
double ExtEma[];             // 2: colour line value
double ExtEmaColor[];        // 3: colour index, 0 rising, 1 falling
double ExtSlope[];           // 4: calculations, EMA change over one bar
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
   SetIndexBuffer(0,ExtSma,INDICATOR_DATA);
   SetIndexBuffer(1,ExtCross,INDICATOR_DATA);
   SetIndexBuffer(2,ExtEma,INDICATOR_DATA);
   SetIndexBuffer(3,ExtEmaColor,INDICATOR_COLOR_INDEX);
   SetIndexBuffer(4,ExtSlope,INDICATOR_CALCULATIONS);
   PlotIndexSetInteger(1,PLOT_ARROW,233);
   PlotIndexSetDouble(1,PLOT_EMPTY_VALUE,EMPTY_VALUE);
   PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,InpSmaPeriod);
   PlotIndexSetInteger(2,PLOT_DRAW_BEGIN,InpEmaPeriod);
   IndicatorSetString(INDICATOR_SHORTNAME,StringFormat("DemoBuffers(%d,%d)",InpSmaPeriod,InpEmaPeriod));
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
   if(rates_total<InpEmaPeriod+2)
      return(0);
   const double k=2.0/(InpEmaPeriod+1.0);
   int start=(prev_calculated>1) ? prev_calculated-1 : 1;
   if(prev_calculated==0)
     {
      ExtEma[0]=close[0];
      ExtSlope[0]=0.0;
      ExtEmaColor[0]=0.0;
     }
   for(int i=start; i<rates_total; i++)
     {
      //--- SMA, empty until there are enough bars
      if(i>=InpSmaPeriod-1)
        {
         double sum=0.0;
         for(int j=0; j<InpSmaPeriod; j++)
            sum+=close[i-j];
         ExtSma[i]=sum/InpSmaPeriod;
        }
      else
         ExtSma[i]=EMPTY_VALUE;
      //--- EMA, its slope and the colour of the slope
      ExtEma[i]=ExtEma[i-1]+k*(close[i]-ExtEma[i-1]);
      ExtSlope[i]=ExtEma[i]-ExtEma[i-1];
      ExtEmaColor[i]=(ExtSlope[i]>=0.0) ? 0.0 : 1.0;
      //--- an arrow under the bar where the close crosses the SMA upward
      ExtCross[i]=EMPTY_VALUE;
      if(i>=InpSmaPeriod && ExtSma[i-1]!=EMPTY_VALUE && close[i-1]<ExtSma[i-1] && close[i]>=ExtSma[i])
         ExtCross[i]=low[i];
     }
   return(rates_total);
  }
//+------------------------------------------------------------------+

BufferSignalEA.mq5 (MQL5\Experts)

//+------------------------------------------------------------------+
//|                                               BufferSignalEA.mq5 |
//|                                                      Bohdan Klok |
//|                             https://www.mql5.com/en/users/kbposht |
//+------------------------------------------------------------------+
#property copyright   "Bohdan Klok"
#property link        "https://www.mql5.com/en/users/kbposht"
#property version     "1.00"
#property description "Example: trades the signals of any indicator by four generic rules on its"
#property description "buffers - an arrow appears, a level is crossed, one buffer crosses another, a colour"
#property description "index becomes a value. Signals are read on the closed bar, once per bar."
#include <Trade\Trade.mqh>
//--- the four generic rules
enum ENUM_SIGNAL_RULE
  {
   SIGNAL_OFF=0,          // Disabled
   SIGNAL_ARROW=1,        // Arrow appears in the buffer
   SIGNAL_CROSS_UP=2,     // Buffer crosses the level upward
   SIGNAL_CROSS_DOWN=3,   // Buffer crosses the level downward
   SIGNAL_LINE_UP=4,      // Buffer crosses the second buffer upward
   SIGNAL_LINE_DOWN=5,    // Buffer crosses the second buffer downward
   SIGNAL_BECOMES=6       // Buffer becomes equal to the level (colour index)
  };
//--- input parameters
input string           InpIndicator="Examples\\Fractals"; // Indicator name inside MQL5\Indicators
input string           InpParams="";                      // Its inputs, comma separated (empty = defaults)
input int              InpSignalBar=3;                    // Signal bar (1 = last closed bar)
input ENUM_SIGNAL_RULE InpBuyRule=SIGNAL_ARROW;           // Buy rule
input int              InpBuyBuffer=1;                    // Buy: buffer
input double           InpBuyLevel=0.0;                   // Buy: level
input int              InpBuyBuffer2=0;                   // Buy: second buffer
input ENUM_SIGNAL_RULE InpSellRule=SIGNAL_ARROW;          // Sell rule
input int              InpSellBuffer=0;                   // Sell: buffer
input double           InpSellLevel=0.0;                  // Sell: level
input int              InpSellBuffer2=1;                  // Sell: second buffer
input double           InpLots=0.1;                       // Lot
input int              InpStopPoints=300;                 // Stop loss, points
input int              InpTakePoints=600;                 // Take profit, points
input ulong            InpMagic=20261010;                 // Magic number
//--- global variables
CTrade   ExtTrade;
int      ExtHandle=INVALID_HANDLE;
datetime ExtLastBar=0;
//+------------------------------------------------------------------+
//| Indicator inputs from "14,0,1.5,true,text"                       |
//+------------------------------------------------------------------+
int CreateByName(const string name,const string params)
  {
   MqlParam p[];
   ArrayResize(p,1);
   p[0].type=TYPE_STRING;
   p[0].string_value=name;
   string parts[];
   const int n=(StringLen(params)>0) ? StringSplit(params,',',parts) : 0;
   ArrayResize(p,n+1);
   for(int i=0; i<n; i++)
     {
      string s=parts[i];
      StringTrimLeft(s);
      StringTrimRight(s);
      if(s=="true" || s=="false")
        {
         p[i+1].type=TYPE_BOOL;
         p[i+1].integer_value=(s=="true");
        }
      else
         if(StringFind(s,".")>=0 && MathIsValidNumber(StringToDouble(s)))
           {
            p[i+1].type=TYPE_DOUBLE;
            p[i+1].double_value=StringToDouble(s);
           }
         else
            if(s=="0" || StringToInteger(s)!=0)
              {
               p[i+1].type=TYPE_INT;
               p[i+1].integer_value=StringToInteger(s);
              }
            else
              {
               p[i+1].type=TYPE_STRING;
               p[i+1].string_value=s;
              }
     }
   return(IndicatorCreate(_Symbol,_Period,IND_CUSTOM,ArraySize(p),p));
  }
//+------------------------------------------------------------------+
//| A value is empty if it is EMPTY_VALUE or not a number            |
//+------------------------------------------------------------------+
bool IsEmpty(const double v)
  {
   return(v==EMPTY_VALUE || !MathIsValidNumber(v));
  }
//+------------------------------------------------------------------+
//| Does the rule fire on the signal bar?                            |
//+------------------------------------------------------------------+
bool RuleFires(const ENUM_SIGNAL_RULE rule,const int buffer,const double level,const int buffer2)
  {
   if(rule==SIGNAL_OFF)
      return(false);
   double a[],b[];
   //--- two values: a[0] = the bar before the signal bar, a[1] = the signal bar
   if(CopyBuffer(ExtHandle,buffer,InpSignalBar,2,a)!=2)
      return(false);
   switch(rule)
     {
      case SIGNAL_ARROW:
         return(!IsEmpty(a[1]) && a[1]!=0.0);
      case SIGNAL_CROSS_UP:
         return(!IsEmpty(a[0]) && !IsEmpty(a[1]) && a[0]<level && a[1]>=level);
      case SIGNAL_CROSS_DOWN:
         return(!IsEmpty(a[0]) && !IsEmpty(a[1]) && a[0]>level && a[1]<=level);
      case SIGNAL_BECOMES:
         return(!IsEmpty(a[1]) && MathAbs(a[1]-level)<0.5 && (IsEmpty(a[0]) || MathAbs(a[0]-level)>=0.5));
      case SIGNAL_LINE_UP:
      case SIGNAL_LINE_DOWN:
        {
         if(CopyBuffer(ExtHandle,buffer2,InpSignalBar,2,b)!=2)
            return(false);
         if(IsEmpty(a[0]) || IsEmpty(a[1]) || IsEmpty(b[0]) || IsEmpty(b[1]))
            return(false);
         if(rule==SIGNAL_LINE_UP)
            return(a[0]<b[0] && a[1]>=b[1]);
         return(a[0]>b[0] && a[1]<=b[1]);
        }
     }
   return(false);
  }
//+------------------------------------------------------------------+
//| Is there an open position of this EA?                            |
//+------------------------------------------------------------------+
bool HasPosition()
  {
   for(int i=PositionsTotal()-1; i>=0; i--)
      if(PositionGetSymbol(i)==_Symbol && PositionGetInteger(POSITION_MAGIC)==(long)InpMagic)
         return(true);
   return(false);
  }
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   if(InpSignalBar<1)
     {
      Print("Signal bar must be 1 or more: the forming bar can still change");
      return(INIT_PARAMETERS_INCORRECT);
     }
   ExtHandle=CreateByName(InpIndicator,InpParams);
   if(ExtHandle==INVALID_HANDLE)
     {
      PrintFormat("Cannot load %s (error %d)",InpIndicator,GetLastError());
      return(INIT_FAILED);
     }
   ExtTrade.SetExpertMagicNumber(InpMagic);
   ExtLastBar=0;
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   if(ExtHandle!=INVALID_HANDLE)
      IndicatorRelease(ExtHandle);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   //--- one decision per bar
   const datetime bar=iTime(_Symbol,_Period,0);
   if(bar==ExtLastBar)
      return;
   if(BarsCalculated(ExtHandle)<InpSignalBar+2)
      return;
   ExtLastBar=bar;
   const bool buy=RuleFires(InpBuyRule,InpBuyBuffer,InpBuyLevel,InpBuyBuffer2);
   const bool sell=RuleFires(InpSellRule,InpSellBuffer,InpSellLevel,InpSellBuffer2);
   if(buy==sell || HasPosition())
      return;
   const double point=SymbolInfoDouble(_Symbol,SYMBOL_POINT);
   if(buy)
     {
      const double ask=SymbolInfoDouble(_Symbol,SYMBOL_ASK);
      ExtTrade.Buy(InpLots,_Symbol,ask,ask-InpStopPoints*point,ask+InpTakePoints*point);
     }
   else
     {
      const double bid=SymbolInfoDouble(_Symbol,SYMBOL_BID);
      ExtTrade.Sell(InpLots,_Symbol,bid,bid+InpStopPoints*point,bid-InpTakePoints*point);
     }
  }
//+------------------------------------------------------------------+