创建多交易品种、多周期指标(基础篇)
「为什么要在 MT5 里做多品种多周期指标」
MT5 原生指标大多只盯一个品种一个周期,实战里盯盘经常要横向比 EURUSD、XAUUSD 在不同周期的结构。2024 年 4 月 3 日 Artyom Trishkin 发布的 MSTF 框架帖,在发布后累计获得 2455 次查看与 26 条讨论,说明多交易品种、多周期指标的需求是真实存在的。 这类指标的核心不是画一条线,而是把多个 symbol、多个 timeframe 的数据拉进同一个计算上下文。外汇与贵金属杠杆高、跳空频繁,跨周期验证能降低单一周期假突破的干扰概率,但无法消除风险。 MSTF 思路把指标拆成基类、按类型派生、再用集合类统一管理实例。交易者开 MT5 后,可直接参照该框架搭一个同时加载 4 小时与 15 分钟 XAUUSD 的自定义指标,验证跨周期信号是否同步。
◍ 把单品种指标改成多品种多周期
这一节起手要做的事,是把原本只在当前图表跑的指标,改造成能引用别的交易品种、别的时间框架数据的多指标结构。思路是先写一个基础多指标类,再按标准指标类型逐个派生子类,最后连自定义指标也能套一层“转换”类变成多周期多品种版本。 所有被创建出来的指标实例,都会统一塞进一个集合类里管理。通过集合提供的方法,你能直接按名称或句柄取到任意指标的缓冲区数据,不用在每个 EA 里重复写指标调用逻辑。 最终交付物是一套可复用的工具:开 MT5 新建一个 EA,include 这套基类后,调一行代码就能在 EURUSD 的 H1 上读 XAUUSD 的 M15 RSI 值。外汇与贵金属市场杠杆高、跳空频繁,跨周期引用数据时要注意不同品种交易时段错位带来的空洞值。
跨周期指标的数据搬运逻辑
MT5 指标内部拆成计算部分和绘图部分,两者互不感知。终端按名称与参数查重:图表上已有同参数实例就复用句柄,绘图端凭句柄从计算缓冲区取数,可能出现多个绘图端共用一个计算端。 计算缓冲区的数组按「现在→过去」排列,索引 0 即当前柱。要把数据画到别的品种或周期,得先算清目标图上哪个柱对应源缓冲区的哪个索引,再写进绘图缓冲。例如 M5 一根柱对应 M1 五根柱,这五根都得填同一个 M5 值;反向上高周期图上的一根柱,只取低周期里时间匹配的最后一根,精度取决于复制方向。 CopyBuffer() 是取数主函数,从指定句柄的缓冲区按起始位+数量(或起止时间)拉数据到 double 数组。索引 0 为当前柱;不知道量就用动态数组,函数会自动改大小;若目标是指标缓冲区允许部分复制,普通数组得借中间数组逐成员搬。预定量的话用静态数组省内存重分配。三种调用形式:起位+数量、起时+数量、起止时间。 跨品种/周期时,每 2 分钟至少访问一次源时间序列以免被释放,类构造里先触发一次构建、之后每 90 秒保活。资源高效做法靠 rates_total 与 prev_calculated:首启 limit=rates_total-1 算全史;limit=1 仅算新柱(1→0);limit=0 只算当前分时(0→0)。所用数组须带时间序列索引标志。 非当前图时改用 Bars() 与 BarsCalculated() 拿柱数和已算量。limit>1 整段复制,=1 或 =0 仅搬最后两根。假期 Bars() 常返 0,若首启算失败会反复清缓冲;解法:等 20–30 秒后用 ChartSetSymbolPeriod() 模拟分时重算,最多试 3 次强制置成功标志,仍不行只能手动刷图。外汇与贵金属跨周期计算受数据缺口影响,实盘前请在 MT5 策略测试器验证。
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
「CopyBuffer 与 OnCalculate 的参数骨架」
在 MT5 自定义指标里,把其他指标的数值搬进自己的数组靠的是 CopyBuffer。它有两个重载版本:一个按起始时间加复制条数取数,另一个按起始时间和结束时间框定区间,两者都要求先传指标句柄和缓冲序号,最后落到一个 double 型数组。 int CopyBuffer( int indicator_handle, // 指标句柄 int buffer_num, // 缓冲索引 datetime start_time, // 起始时间 int count, // 复制数量 double buffer[] // 目标数组 ); int CopyBuffer( int indicator_handle, // 指标句柄 int buffer_num, // 缓冲索引 datetime start_time, // 起始时间 datetime stop_time, // 结束时间 double buffer[] // 目标数组 ); 真正跑计算逻辑的是 OnCalculate,它有两类签名。只吃单序列的版本常见于依托收盘价做变换的指标;带 time/open/high/low/close 及成交量、点差数组的版本,则把整根 K 线的原始数据都递了进来,适合做多因子合成。 int OnCalculate( const int rates_total, // 价格数组长度 const int prev_calculated, // 上次已处理柱数 const int begin, // 有效数据起始索引 const double &price[] // 计算用数值数组 ); 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[], // tick 量 const long &volume[], // 真实量 const int &spread[] // 点差 ); 在函数体开头常看到这样一句:int limit=rates_total-prev_calculated; 它算出本次新增待算的柱数。当 limit>1 时,说明是首跑或历史被改写,这时往往要全量重算而非只补尾部。外汇与贵金属波动剧烈、点差跳变频繁,用 prev_calculated 做增量计算能显著降低 CPU 占用,但重算分支没写好可能漏掉信号。
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 date class="type">class="kw">double buffer[] class=class="str">"cmt">// array the data to be copied to ); class="type">int OnCalculate( const class="type">int rates_total, class=class="str">"cmt">// price[] array size const class="type">int prev_calculated,class=class="str">"cmt">// number of processed bars at the previous call const class="type">int begin, class=class="str">"cmt">// index number in the price[] array meaningful data starts from const class="type">class="kw">double &price[] class=class="str">"cmt">// array of values for calculation ); class="type">int OnCalculate( const class="type">int rates_total, class=class="str">"cmt">// size of input timeseries const class="type">int prev_calculated,class=class="str">"cmt">// number of processed bars at the previous call const class="type">class="kw">datetime &time[], class=class="str">"cmt">// Time array const class="type">class="kw">double &open[], class=class="str">"cmt">// Open array const class="type">class="kw">double &high[], class=class="str">"cmt">// High array const class="type">class="kw">double &low[], class=class="str">"cmt">// Low array const class="type">class="kw">double &close[], class=class="str">"cmt">// Close array const class="type">long &tick_volume[],class=class="str">"cmt">// Tick Volume array const class="type">long &volume[], class=class="str">"cmt">// Real Volume array const class="type">int &spread[] class=class="str">"cmt">// Spread array ); class=class="str">"cmt">//--- Number of bars for calculation class="type">int limit=rates_total-prev_calculated; class=class="str">"cmt">//--- If limit > class="num">1, then this is the first calculation or change in the history
◍ 历史数据边界与循环起始点的两种写法
MT5 自定义指标里,处理 limit 和 prev_calculated 的关系直接决定重绘开销。若 limit 大于 1,说明是首次计算或历史补齐,此时把 limit 拉到 rates_total-1,让循环覆盖全部可用 K 线,再对缓冲区做空值初始化。 另一种常见模式是用 fmax(0, prev_calculated-1) 作为起点,只算新增及最后一根重叠 bar,避免每 tick 重算全量。下面这段把两种思路的骨架都列出来,注意 ArraySetAsSeries 把数组倒序对齐时间轴。 SeriesInfoInteger 能拿到当前品种周期的 SERIES_FIRSTDATE 与 SERIES_LASTBAR_DATE,用来做时间边界断言很有用。外汇和贵金属波动受消息驱动,用这些接口做历史回看时仍属高风险,信号只代表概率倾向。
if(limit>class="num">1) { class=class="str">"cmt">//--- specify all the available history for calculation limit=rates_total-class="num">1; class=class="str">"cmt">//--- If the indicator has any buffers, initialize them here with empty values set for these buffers } ArrayInitialize(Buffer,EMPTY_VALUE); for(class="type">int i=limit;i>=class="num">0;i--) { class=class="str">"cmt">// Calculating indicator at each bar of the loop(i) } ArraySetAsSeries(array,true); class="type">int i=fmax(class="num">0, prev_calculated-class="num">1); for(;i<rates_total;i++) { class=class="str">"cmt">// Calculating indicator at each bar of the loop(i) } limit=rates_total-prev_calculated; SeriesInfoInteger(symbol,period,SERIES_FIRSTDATE); class=class="str">"cmt">// The very first date for a period and symbol at the moment SeriesInfoInteger(symbol,period,SERIES_LASTBAR_DATE); class=class="str">"cmt">// Time of opening the last bar for the period and symbol
多周期指标基类怎么搭
在 MT5 的 MQL5\Include\IndMSTF\ 目录下新建 IndMSTF.mqh,类 CIndMSTF 必须从标准库 CObject 继承,并在文件头部连接 Object.mqh。这一步是后续所有跨品种跨周期指标对象的地基,不挂对基类,终端编译会直接报找不到父对象。 基类里先铺三类东西:宏替换、枚举、指标缓冲区结构。两个计时器是硬需求——90 秒轮询去拉不在当前品种/周期上计算的指标时间序列,20 秒在周末模拟分时重绘。句柄从 10 开始编号,每新建一个成功指标 +1,这是终端分配计算部分的唯一 ID。 缓冲区分三层:计算部分缓冲区由终端管,零索引对应当前柱,用 CopyBuffer() 取数;指标对象缓冲区是类内动态数组,当前柱排在 ArraySize()-1;绘图缓冲区只在自定义指标里把对象缓冲区映射到图表。结构里直接存 init_value 和 shift,免得后面再查创建参数。 构造函数收品种名、周期、类型、缓冲数,非当前图就触发下载时间序列;析构释放句柄和数组;调整大小与初始化都按单缓冲和全缓冲两套写。计算法只在首启或历史变更时整段拷贝,其余分时增量更新,出错写错误码交回调用方。 派生类只须在构造里填具体指标固有参数,基类已包揽非当前图数据构建的全部功能。实盘接这类多周期信号前先认清:外汇和贵金属杠杆品种跳空频繁,跨周期重绘在周末模拟分时下可能偏离真实流动性。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| IndMSTF.mqh | class=class="str">"cmt">//| Copyright class="num">2023, MetaQuotes Ltd. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2023, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//--- includes class="macro">#include <Object.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+