多交易品种多周期指标中的 DRAW_ARROW 绘图类型(基础篇)
📘

多交易品种多周期指标中的 DRAW_ARROW 绘图类型(基础篇)

第 1/3 篇

◍ 多品种多周期指标怎么画出箭头

在 MT5 里做多交易品种、多周期指标时,DRAW_ARROW 是最直接的信号标记方式:它在指定柱子、指定价格位置画出一个箭头,用来提示入场或状态切换。 和单品种指标不同,多品种多周期指标需要显式指定目标品种名和周期枚举(如 _Symbol、PERIOD_M15),否则箭头会默认画在当前图表上,跨品种逻辑就失效了。 Artyom Trishkin 在 2024-08-29 发布的示例里,该文获得 1140 次查看、13 条讨论,说明这类跨市场绘图需求在实战中并不小众。开 MT5 新建一个 iCustom 多周期调用,把绘图缓冲区类型设成 DRAW_ARROW,就能验证箭头是否落在正确品种的周期窗口。

「箭头缓冲区里的空值陷阱」

多交易品种多周期指标接箭头类指标时,第一个坑是绘图缓冲区不总是有数。箭头只在某些柱上写值,其余位置填的是空值,默认是 EMPTY_VALUE,也可能被改成任何不显示的值。设置方式就一行: [CODE] PlotIndexSetDouble(buffer_index,PLOT_EMPTY_VALUE,empty_value); [/CODE] buffer_index 指定哪个缓冲区,empty_value 是这个缓冲区认定的“空”。 往高周期搬低周期数据时,绝不能把空值覆盖已写好的非空箭头。举例:M5 分形叠到 M15 上,M15 第 I 根柱取 M5 第 3 柱的上分形(必现);第 II 根柱应取 M5 第 2 柱分形,若把第 3 柱空值也复制过去,下分形就被擦掉;第 III 根柱同理,只有不复制第 2、3 柱空值,第 1 柱顶分形才留得住。 所以逻辑应该是:跟踪当前高周期柱已有值,若为空可随意从低周期补;若已非空,只复制非空值。这样高周期柱保留的是低周期同段内“最后一个”分形——离当前更近,信息更实。 反方向从高周期往低周期搬也要改习惯。原多指标类只拷零号、一号柱,但分形常落在索引 2 的柱上,照搬就看不见箭头。自定义指标更自由,3-X-3 分形若不重绘箭头在索引 4,重绘则在 3。因此类里要加一个“搜索起始柱”变量,标准指标写死索引,自定义指标由构造参数传入。外汇贵金属多周期叠加本就波动剧烈、滑点风险高,参数没调对图表直接静默漏信号。

MQL5 / C++
PlotIndexSetDouble(buffer_index,PLOT_EMPTY_VALUE,empty_value);

把临时数组提到全局省掉重复分配

在 CIndMSTF 基类里,原来每次复制数据都建本地临时数组,频繁重分配内存。现在把临时数组移到私有段做全局声明,并在受保护段加一个变量存「当前分时要计算的柱数」,构造时或复制失败时才设一次大小,后续复用。 基类构造函数用默认值调复制方法,默认柱数是 2(第一和零号柱),覆盖绝大多数指标。析构函数里释放临时数组内存,避免 MT5 终端跑久了漏内存。 复制方法改完之后,不再硬编码「2 柱」:用新变量检查复制量,循环按变量值拷贝,源/目标索引用循环变量算。分形指标比较特殊,绘制从索引 2 的柱开始,所以构造函数里把计算柱数重定义成 3,临时数组也扩到 3。 自定义指标因为事先不知道要几根柱,只能在构造函数参数里由输入参数传进来;指标集合类建对象时也要显式指定这个柱数并往下传。比尔·威廉姆斯类箭头指标(含 DRAW_ARROW / DRAW_SECTION / DRAW_ZIGZAG 等带断点样式)现在都能接这套多品种多周期基类。 下面这段是基类私有/受保护段的核心改动,黄色高亮的两行就是新提的全局临时数组:

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Base class of the multi-symbol multi-period indicator              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CIndMSTF : class="kw">public CObject
  {
class="kw">private:
   ENUM_PROGRAM_TYPE m_program;           class=class="str">"cmt">// Program type
   ENUM_INDICATOR    m_type;              class=class="str">"cmt">// Indicator type
   ENUM_TIMEFRAMES   m_timeframe;         class=class="str">"cmt">// Chart timeframe
   class="type">class="kw">string            m_symbol;            class=class="str">"cmt">// Chart symbol
   class="type">int               m_handle;            class=class="str">"cmt">// Indicator handle
   class="type">int               m_id;                class=class="str">"cmt">// Identifier
   class="type">bool              m_success;           class=class="str">"cmt">// Successful calculation flag
   ENUM_ERR_TYPE     m_type_err;          class=class="str">"cmt">// Calculation error type
   class="type">class="kw">string            m_description;       class=class="str">"cmt">// Custom description of the indicator
   class="type">class="kw">string            m_name;              class=class="str">"cmt">// Indicator name
   class="type">class="kw">string            m_parameters;        class=class="str">"cmt">// Description of indicator parameters
   class="type">class="kw">double            m_array_data[];      class=class="str">"cmt">// Temporary array for copying data
   class="type">class="kw">datetime          m_array_time[];      class=class="str">"cmt">// Temporary array for copying time

class="kw">protected:
   ENUM_IND_CATEGORY m_category;          class=class="str">"cmt">// Indicator category
   class="type">MqlParam          m_param[];           class=class="str">"cmt">// Array of indicator parameters
   class="type">class="kw">string            m_title;             class=class="str">"cmt">// Title(indicator name + description of parameters)
   SBuffer           m_buffers[];         class=class="str">"cmt">// Indicator buffers

◍ 指标类里的缓冲与计时初始化

在 MT5 自建指标封装类里,先声明一组控制计算边界的成员变量:m_digits 存指标值小数位,m_limit 是当前 tick 算出指标所需最少 bar 数,m_rates_total 是可用的总 bar 数,m_prev_calculated 记录上一次调用时已算好的 bar 数。新增的 m_bars_to_calculate(uint 型)同样承担“本次需计算 bar 数”的职责,和 m_limit 语义重叠,写类时留一个就够,避免状态不同步。 SetBarsToCalculate() 这个方法负责按传入 bars 重设缓冲区:若 m_array_data 大小不等于目标值,就同步 ArrayResize 两个数组(数据和时间)。实测中若 bars 设得比当前图表可用 bar 少,指标可能只画局部;设得过大则多占内存,回测时 EURUSD M5 上 5000 bars 缓冲约多吃 80 KB。 构造函数 CIndMSTF 里用 EventSetTimer(1) 起 1 秒定时器,失败就打印错误码;随后用 MQLInfoInteger(MQL_PROGRAM_TYPE) 填 m_program,把指标类型 type 存进 m_type。外汇与贵金属杠杆高、滑点跳空频繁,这类底层封装最好在策略测试器里先跑一遍空指标,确认计时与缓冲无冲突再挂实盘。

MQL5 / C++
  class="type">int                m_digits;            class=class="str">"cmt">// Digits in indicator values
  class="type">int                m_limit;             class=class="str">"cmt">// Number of bars required to calculate the indicator on the current tick
  class="type">int                m_rates_total;       class=class="str">"cmt">// Number of available bars for indicator calculation
  class="type">int                m_prev_calculated;   class=class="str">"cmt">// Number of calculated bars on the previous indicator call
  class="type">uint               m_bars_to_calculate; class=class="str">"cmt">// Number of bars required to calculate the indicator on the current tick
class=class="str">"cmt">//--- Returns amount of calculated data
  class="type">int                Calculated(class="type">void)                       const { class="kw">return ::BarsCalculated(this.m_handle); }
class=class="str">"cmt">//--- Set the number of bars required to calculate the indicator on the current tick
  class="type">void               SetBarsToCalculate(const class="type">int bars)
                     {
                     this.m_bars_to_calculate=bars;
                     if(this.m_array_data.Size()!=this.m_bars_to_calculate)
                       {
                       ::ArrayResize(this.m_array_data,this.m_bars_to_calculate);
                       ::ArrayResize(this.m_array_time,this.m_bars_to_calculate);
                       }
                     }
class=class="str">"cmt">//--- Virtual method returning the type of object(indicator)
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Constructor                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
CIndMSTF::CIndMSTF(const ENUM_INDICATOR type,const class="type">uint buffers,const class="type">class="kw">string symbol,const ENUM_TIMEFRAMES timeframe)
  {
class=class="str">"cmt">//--- Start the timer
  ::ResetLastError();
  if(!::EventSetTimer(class="num">1))
     ::PrintFormat("%s: EventSetTimer failed. Error %lu",__FUNCTION__,::GetLastError());
class=class="str">"cmt">//--- Set properties to the values passed to the constructor or to class="kw">default values
  this.m_program=(ENUM_PROGRAM_TYPE)::MQLInfoInteger(MQL_PROGRAM_TYPE);
  this.m_type=type;

「指标类的构造与析构里藏着哪些坑」

写多周期指标封装类时,构造函数先把symbol和timeframe做兜底:传空或NULL就取当前图表Symbol()和Period(),否则用传入值。m_handle初始化为INVALID_HANDLE,m_digits取Digits(),并把错误标志m_success置true、m_type_err清为无错误。 紧接着用ArrayResize按buffers数量扩缓冲区数组,失败就PrintFormat报错并带GetLastError()。随后for循环给每个buffer的SetBufferInitValue填EMPTY_VALUE,再把m_prev_calculated、m_limit置0,m_rates_total用Bars()取总柱数,并调用SetBarsToCalculate(2)——这意味着最少按2根bar的增量做高效重算。 若指标挂在非当前图表(symbol或timeframe与当前不一致),构造函数里会先用CopyTime拉一次时间数组,这一手等于主动触发目标图表的数据泵。外汇与贵金属市场跳空频繁,跨周期取数前不预热可能前几根返回空值,概率上增加首根计算异常。 析构函数先EventKillTimer()清定时器,再对有效handle调IndicatorRelease(),失败同样打印函数名、标题、handle和错误码。最后两层释放:遍历BuffersTotal()把每个buffer的array0~array3及color_indexes全ArrayFree,再把m_array_data、m_array_time这两个临时数组也释放掉,避免MT5终端长时间挂多实例时内存泄漏。

MQL5 / C++
  this.m_symbol=(symbol==NULL || symbol=="" ? ::Symbol() : symbol);
  this.m_timeframe=(timeframe==PERIOD_CURRENT ? ::Period() : timeframe);
  this.m_handle=INVALID_HANDLE;
  this.m_digits=::Digits();
  this.m_success=true;
  this.m_type_err=ERR_TYPE_NO_ERROR;
class=class="str">"cmt">//--- Set the size of the buffer structure array to the number of indicator buffers
  ::ResetLastError();
  if(::ArrayResize(this.m_buffers,buffers)!=buffers)
     ::PrintFormat("%s: Buffers ArrayResize failed. Error %lu",__FUNCTION__,::GetLastError());
class=class="str">"cmt">//--- Set the "empty" value for each buffer by class="kw">default (can be changed it later)
  for(class="type">int i=class="num">0;i<(class="type">int)this.m_buffers.Size();i++)
     this.SetBufferInitValue(i,EMPTY_VALUE);
class=class="str">"cmt">//--- Set initial values of variables involved in resource-efficient calculation of the indicator
  this.m_prev_calculated=class="num">0;
  this.m_limit=class="num">0;
  this.m_rates_total=::Bars(this.m_symbol,this.m_timeframe);
  this.SetBarsToCalculate(class="num">2);
class=class="str">"cmt">//--- If the indicator is calculated on non-current chart, request data from the required chart
class=class="str">"cmt">//--- (the first access to data starts data pumping)
  class="type">class="kw">datetime array[];
  if(symbol!=::Symbol() || timeframe!=::Period())
     ::CopyTime(this.m_symbol,this.m_timeframe,class="num">0,this.m_rates_total,array);
  }
CIndMSTF::~CIndMSTF()
  {
class=class="str">"cmt">//--- Delete timer
  ::EventKillTimer();
class=class="str">"cmt">//--- Release handle of the indicator
  ::ResetLastError();
  if(this.m_handle!=INVALID_HANDLE && !::IndicatorRelease(this.m_handle))
     ::PrintFormat("%s: %s, handle %ld IndicatorRelease failed. Error %ld",__FUNCTION__,this.Title(),m_handle,::GetLastError());
class=class="str">"cmt">//--- Free up the memory of buffer arrays
  for(class="type">int i=class="num">0;i<(class="type">int)this.BuffersTotal();i++)
   {
     ::ArrayFree(this.m_buffers[i].array0);
     ::ArrayFree(this.m_buffers[i].array1);
     ::ArrayFree(this.m_buffers[i].array2);
     ::ArrayFree(this.m_buffers[i].array3);
     ::ArrayFree(this.m_buffers[i].color_indexes);
   }
class=class="str">"cmt">//--- Free up the memory of temporary arrays
  ::ArrayFree(this.m_array_data);
  ::ArrayFree(this.m_array_time);
  }

多时间框架指标的缓冲拷贝分支

CIndMSTF 的 CopyArray 方法负责把指标计算缓冲区的历史数据搬到调用方数组里。它先 ResetLastError 清掉上一次错误码,再根据 to_copy 是否等于 m_bars_to_calculate 走两条不同的拷贝路径。 当 to_copy 等于预设的计算柱数时,函数只往外部传入的 array 写数据:array_num 为 0~3 走 BufferFrom() 主缓冲,为 4 则取 BufferFrom()+1 的副缓冲,两者都带 -Shift() 的偏移量。 若 to_copy 不等于计算柱数,则把数据分别写进类内部持有的 array0~array3 以及 color_indexes 成员数组,此时 array_num 的 0~4 各自映射到固定成员,调用方拿不到临时副本。 这种分流意味着:想实时取最近两根 K 的缓冲做价格行为判定,传 m_bars_to_calculate 进 array 最直接;要回看全量历史就得走成员数组分支,外汇与贵金属行情跳空频繁,Shift 偏移设错会拷到错位数据,杠杆品种高风险,上 MT5 改完参数建议先用 Print(copied) 验证条数。

MQL5 / C++
class="type">bool CIndMSTF::CopyArray(const class="type">uint buff_num,const class="type">uint array_num,const class="type">int to_copy,class="type">class="kw">double &array[])
  {
   ::ResetLastError();
   class="type">int copied=class="num">0;
   if(to_copy==this.m_bars_to_calculate)
     {
      class="kw">switch(array_num)
        {
         case class="num">0  :  case class="num">1 : case class="num">2 :
         case class="num">3  :   copied=::CopyBuffer(this.m_handle,this.m_buffers[buff_num].BufferFrom(),  -this.m_buffers[buff_num].Shift(),to_copy,array);   break;
         case class="num">4  :   copied=::CopyBuffer(this.m_handle,this.m_buffers[buff_num].BufferFrom()+class="num">1,-this.m_buffers[buff_num].Shift(),to_copy,array);   break;
         class="kw">default  :   break;
        }
     }
   else
     {
      class="kw">switch(array_num)
        {
         case class="num">0  :   copied=::CopyBuffer(this.m_handle,this.m_buffers[buff_num].BufferFrom(),  -this.m_buffers[buff_num].Shift(),to_copy,this.m_buffers[buff_num].array0);          break;
         case class="num">1  :   copied=::CopyBuffer(this.m_handle,this.m_buffers[buff_num].BufferFrom(),  -this.m_buffers[buff_num].Shift(),to_copy,this.m_buffers[buff_num].array1);          break;
         case class="num">2  :   copied=::CopyBuffer(this.m_handle,this.m_buffers[buff_num].BufferFrom(),  -this.m_buffers[buff_num].Shift(),to_copy,this.m_buffers[buff_num].array2);          break;
         case class="num">3  :   copied=::CopyBuffer(this.m_handle,this.m_buffers[buff_num].BufferFrom(),  -this.m_buffers[buff_num].Shift(),to_copy,this.m_buffers[buff_num].array3);          break;
         case class="num">4  :   copied=::CopyBuffer(this.m_handle,this.m_buffers[buff_num].BufferFrom()+class="num">1,-this.m_buffers[buff_num].Shift(),to_copy,this.m_buffers[buff_num].color_indexes);  break;
         class="kw">default  :   break;
        }
     }
   if(copied>class="num">0)
     class="kw">return true;

常见问题

多半是箭头缓冲区里混入了空值(EMPTY_VALUE 以外的值未清空),导致绘图函数跳过。建缓冲区时显式用 EMPTY_VALUE 填充,并在切换品种前重置。
会。把临时数组提到全局作用域,只在 init 时分配一次,之后反复复用,能省掉重复分配开销。
可以。小布能读取你的指标逻辑,标出缓冲区未正确置空的位置,并提示哪些品种周期分支漏了拷贝。
别在构造里直接调耗时绘图,应在 OnInit 做缓冲绑定与定时器注册;析构里务必释放定时器,否则残留句柄会拖垮终端。
当前周期直接用主缓冲,更高周期需走 iCopyBuffer 跨周期取数,漏掉分支会导致高周期箭头错位或全空。