多交易品种多周期指标中的 DRAW_ARROW 绘图类型(基础篇)
◍ 多品种多周期指标怎么画出箭头
在 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。因此类里要加一个“搜索起始柱”变量,标准指标写死索引,自定义指标由构造参数传入。外汇贵金属多周期叠加本就波动剧烈、滑点风险高,参数没调对图表直接静默漏信号。
PlotIndexSetDouble(buffer_index,PLOT_EMPTY_VALUE,empty_value);把临时数组提到全局省掉重复分配
在 CIndMSTF 基类里,原来每次复制数据都建本地临时数组,频繁重分配内存。现在把临时数组移到私有段做全局声明,并在受保护段加一个变量存「当前分时要计算的柱数」,构造时或复制失败时才设一次大小,后续复用。 基类构造函数用默认值调复制方法,默认柱数是 2(第一和零号柱),覆盖绝大多数指标。析构函数里释放临时数组内存,避免 MT5 终端跑久了漏内存。 复制方法改完之后,不再硬编码「2 柱」:用新变量检查复制量,循环按变量值拷贝,源/目标索引用循环变量算。分形指标比较特殊,绘制从索引 2 的柱开始,所以构造函数里把计算柱数重定义成 3,临时数组也扩到 3。 自定义指标因为事先不知道要几根柱,只能在构造函数参数里由输入参数传进来;指标集合类建对象时也要显式指定这个柱数并往下传。比尔·威廉姆斯类箭头指标(含 DRAW_ARROW / DRAW_SECTION / DRAW_ZIGZAG 等带断点样式)现在都能接这套多品种多周期基类。 下面这段是基类私有/受保护段的核心改动,黄色高亮的两行就是新提的全局临时数组:
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。外汇与贵金属杠杆高、滑点跳空频繁,这类底层封装最好在策略测试器里先跑一遍空指标,确认计时与缓冲无冲突再挂实盘。
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终端长时间挂多实例时内存泄漏。
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) 验证条数。
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;