指标间的数据交换:易如反掌!·进阶篇
🔗

指标间的数据交换:易如反掌!·进阶篇

(2/3)· 当指标 B 想实时吃指标 A 的缓冲区,句柄等待十几秒的坑你还要踩多久?

实战向进阶 第 2/3 篇
很多老手也习惯性用 iCustom 去拉已加载指标的数据,却没意识到 MQL5 里句柄虽避免了重复计算,但重资源指标仍会卡出数秒互斥等待。把跨指标通信当成「调个函数就行」的人,往往在回测里才暴露延迟断层。

◍ 用全局变量做跨指标指针表

系统内核写好之后,得让它能在独立 MQL 程序之间被随时调用。MT5 客户端自带全局变量机制,最顺手的办法就是把指标缓冲区的指针塞进全局变量,当作一张描述符表来用。 描述符名称按固定格式拼:string_identifier#buffer_number#symbol#period#buffer_length#indexing_direction#random_number,值就是内存里对应缓冲区的整数指针。buffer_length 绝不能漏,越界可能直接把客户端甚至 Windows 干到蓝屏;indexing_direction 用 0 表示正常排序、1 表示 AS_SERIES 逆向排序;random_number 用来区分同标识多副本,不优雅但管用。 注册和注销靠两个函数:RegisterBuffer 先调 UnregisterBuffer 清掉旧的,再拼好名字写全局变量;UnregisterBuffer 遍历全局变量,删掉值等于该指针的项。新柱来临缓冲区长度 +1,必须重调 RegisterBuffer,否则旧描述符残留表里。 取数前先用搜索函数扫全局变量,字段可缺省——name 传 NULL 就跳过标识检查,mode<0、symbol=NULL、period<=0 同理,还能用 StringFind 做子串匹配,比如搜 "MA" 捞出所有移动平均线副本。拿到描述符列表后,包装 DLL 的 GetValue/SetValue,注意数组界限和排序方向,失败返回 EMPTY_VALUE 或 false。 下面这段是 RegisterBuffer 与 UnregisterBuffer 的 MQL5 实现,逐行看逻辑: void RegisterBuffer(double &Buffer[], string name, int mode) export // 导出函数,接收缓冲区引用、标识名、缓冲区编号 UnregisterBuffer(Buffer); // 先注销同缓冲区旧描述符,防止重复 int direction=0; // 默认排序方向为正常 if(ArrayGetAsSeries(Buffer)) direction=1; // 若缓冲区是 AS_SERIES 则方向置 1 name=name+"#"+mode+"#"+Symbol()+"#"+Period()+"#"+ArraySize(Buffer)+"#"+direction; // 拼出描述符前半段:标识#编号#品种#周期#长度#方向 int ptr=GetPtr(Buffer); // 取缓冲区内存指针 if(ptr==0) return; // 指针为 0 直接退出 MathSrand(ptr); // 用指针值做随机种子,方便生成区分随机数 while(true) // 死循环找不冲突的名字 { int rnd=MathRand(); // 取一个随机数 if(!GlobalVariableCheck(name+"#"+rnd)) // 检查拼接随机后缀的名字是否存在 { name=name+"#"+rnd; // 不存在就定下最终名 GlobalVariableSet(name,ptr); // 把指针写进全局变量 break; // 跳出循环 } } } void UnregisterBuffer(double &Buffer[]) export // 导出注销函数 int ptr=GetPtr(Buffer); // 再取一次指针 if(ptr==0) return; // 指针无效则退出 int gt=GlobalVariablesTotal(); // 拿到全局变量总数 int i; for(i=gt-1;i>=0;i--) // 从尾到头遍历所有全局变量

MQL5 / C++
class="type">void RegisterBuffer(class="type">class="kw">double &Buffer[], class="type">class="kw">string name, class="type">int mode) class="kw">export
{
   UnregisterBuffer(Buffer);

   class="type">int direction=class="num">0;
   if(ArrayGetAsSeries(Buffer)) direction=class="num">1;
   name=name+"#"+mode+"#"+Symbol()+"#"+Period()+"#"+ArraySize(Buffer)+"#"+direction;
   class="type">int ptr=GetPtr(Buffer);
   if(ptr==class="num">0) class="kw">return;

   MathSrand(ptr);

   while(true)
   {
      class="type">int rnd=MathRand();
      if(!GlobalVariableCheck(name+"#"+rnd))
      {
         name=name+"#"+rnd;
         GlobalVariableSet(name,ptr);
         break;
      }
   }
}
class="type">void UnregisterBuffer(class="type">class="kw">double &Buffer[]) class="kw">export
{
   class="type">int ptr=GetPtr(Buffer);
   if(ptr==class="num">0) class="kw">return;

   class="type">int gt=GlobalVariablesTotal();
   class="type">int i;
   for(i=gt-class="num">1;i>=class="num">0;i--)

用全局变量反查指标缓冲区

在 MT5 里跨 EA / 指标抓缓冲区,常见做法是用全局变量名当索引。上面这段把全局变量名按 '#' 拆成 descriptor 数组,字段分别对应 name、mode、symbol、period,依次比对入参做过滤。 FindBuffers 先调 GlobalVariablesTotal 拿到总数,再从后往前遍历;StringTrimLeft / StringTrimRight 清掉 name 两端空格,避免匹配漏判。任一字段不符就置 found=false,全中才把 name_i 写进 buffers 并返回 count。 StringExplode 是自己写的分割函数:没找到分隔符就整串丢进 result[0];找到就循环 StringSubstr 截段,每次 ArrayResize 扩一位。外汇 / 贵金属品种多周期运行时,这种全局变量缓存易堆积,建议定时清理,否则 GlobalVariablesTotal 可能涨到数百拖慢遍历。

MQL5 / C++
  class=class="str">"cmt">//并且从所有地方删除缓存
   class="type">class="kw">string name=GlobalVariableName(i);
   if(GlobalVariableGet(name)==ptr)
      GlobalVariableDel(name);

}
}
class="type">int FindBuffers(class="type">class="kw">string name, class="type">int mode, class="type">class="kw">string symbol, class="type">int period, class="type">class="kw">string &buffers[]) class="kw">export
{
   class="type">int count=class="num">0;
   class="type">int i;
   class="type">bool found;
   class="type">class="kw">string name_i;
   class="type">class="kw">string descriptor[];
   class="type">int gt=GlobalVariablesTotal();

   StringTrimLeft(name);
   StringTrimRight(name);

   ArrayResize(buffers,count);
   for(i=gt-class="num">1;i>=class="num">0;i--)
   {
      found=true;
      name_i=GlobalVariableName(i);

      StringExplode(name_i,"#",descriptor);

      if(StringFind(descriptor[class="num">0],name)<class="num">0&&name!=NULL) found=false;

      if(descriptor[class="num">1]!=mode&&mode>=class="num">0) found=false;
      if(descriptor[class="num">2]!=symbol&&symbol!=NULL) found=false;
      if(descriptor[class="num">3]!=period&&period>class="num">0) found=false;

      if(found)
      {
         count++;
         ArrayResize(buffers,count);
         buffers[count-class="num">1]=name_i;
      }
   }
   class="kw">return(count);
}
class="type">void StringExplode(class="type">class="kw">string s, class="type">class="kw">string separator, class="type">class="kw">string &result[])
{
   class="type">int i,pos;
   ArrayResize(result,class="num">1);

   pos=StringFind(s,separator);
   if(pos<class="num">0) {result[class="num">0]=s;class="kw">return;}

   for(i=class="num">0;;i++)
   {
      pos=StringFind(s,separator);
      if(pos>=class="num">0)
      {
         result[i]=StringSubstr(s,class="num">0,pos);
         s=StringSubstr(s,pos+StringLen(separator));
      }
      else break;
      ArrayResize(result,ArraySize(result)+class="num">1);
   }
}

「跨指标读写的指针桥接函数」

在 MT5 里想让两个独立 EA 或脚本共享自定义指标缓冲区,靠全局变量存指针是最轻量的办法。下面这两个导出函数就是干这个的:用 descriptor 字符串去查全局变量,拿到内存指针后直接定位数组下标取值或写值。 descriptor 按 '#' 拆字段,其中第 5 个(索引 4)是数组 size,第 6 个(索引 5)是 direction。direction==1 代表排序反向,这时 shift 会被重算成 size-1-shift,否则按正向索引。 两个函数都先做 GlobalVariableCheck 确认描述符存在,再取 ptr 判非零,最后用 shift>=0 && shift<size 拦住越界——这一步省了,客户端大概率直接崩。读失败返回 EMPTY_VALUE,写失败返回 false,调用方据此判断共享是否生效。 实盘里外汇和贵金属波动跳空频繁,这类跨程序内存共享不保证时序,高频切换品种时可能读到上一根残留值,验证前先在 EURUSD 的 M1 上跑通再上 XAUUSD。

MQL5 / C++
class="type">class="kw">double GetIndicatorValue(class="type">class="kw">string descriptor, class="type">int shift) class="kw">export
{
   class="type">int ptr;
   class="type">class="kw">string fields[];
   class="type">int size,direction;
   if(GlobalVariableCheck(descriptor)>class="num">0)                     class=class="str">"cmt">//检查描述符的有效性
   {              
      ptr = GlobalVariableGet(descriptor);                    class=class="str">"cmt">//获取指针的值
      if(ptr!=class="num">0)
      {
         StringExplode(descriptor,"#",fields);               class=class="str">"cmt">//将名称分割为字段
         size = fields[class="num">4];                                    class=class="str">"cmt">//我们需要当前数组大小
         direction=fields[class="num">5];                                
         if(direction==class="num">1) shift=size-class="num">1-shift;                 class=class="str">"cmt">//如果ordering_direction反向
         if(shift>=class="num">0&&shift<size)                             class=class="str">"cmt">//检查其有效性,以防止崩溃
            class="kw">return(GetValue(MathAbs(ptr),shift));             class=class="str">"cmt">//ok,返回值

   }

   }
   class="kw">return(EMPTY_VALUE);                                       class=class="str">"cmt">//否则返回空值
}
class="type">bool SetIndicatorValue(class="type">class="kw">string descriptor, class="type">int shift, class="type">class="kw">double value) class="kw">export
{
   class="type">int ptr;
   class="type">class="kw">string fields[];
   class="type">int size,direction;
   if(GlobalVariableCheck(descriptor)>class="num">0)                      class=class="str">"cmt">//检查其有效性
   {              
      ptr = GlobalVariableGet(descriptor);                    class=class="str">"cmt">//获取描述符值
      if(ptr!=class="num">0)
      {
         StringExplode(descriptor,"#",fields);               class=class="str">"cmt">//将其分割为字段
         size = fields[class="num">4];                                    class=class="str">"cmt">//我们要知道它的大小
         direction=fields[class="num">5];                                
         if(direction==class="num">1) shift=size-class="num">1-shift;                 class=class="str">"cmt">//订单转向的情况
         if(shift>=class="num">0&&shift<size)                             class=class="str">"cmt">//检查索引防止客户端崩溃
         {
            SetValue(MathAbs(ptr),shift,value);
            class="kw">return(true);

         }

   }

   }
   class="kw">return(false);
}

◍ 把 ATR 塞进别的指标窗口

想验证跨指标缓冲区搬运是否靠谱,最直白的靶子是标准库里的真实波动幅度均值(ATR)。只要改几行 OnCalculate,就能把它的缓冲区值注册成全局描述符,再让另一个指标窗口读取并覆写。 关键动作落在 prev_calculated != rates_total 这个条件里:新柱出现或补历史数据时,才调用 RegisterBuffer 把 ExtATRBuffer 以 "ATR" 名义挂到表上。拖太久不注销会留垃圾,所以 OnDeinit 里必须补一句 UnregisterBuffer(ExtATRBuffer),注意它返回 void 且带 const int reason 参数。 另起一个叫 test 的指标,包含 exchng.mqh,用子字符串从描述符列表捞到目标,再读缓冲区写进自己的 ATRCopyBuffer。示例代码里直接把数组下标当值写进去——纯属验证通道通不通。 跑起来后,ATR 本体的线在下方子窗,test 指标的同窗口会画出一条由索引值组成的直线,证明跨窗读取成功。外汇与贵金属杠杆高,这类实验只在策略测试器或模拟盘做,实盘误写缓冲区可能让图表显示失真。 别把正态当圣经:描述符名写错一个字母,test 就读不到东西,全局变量列表里却照常多一条,排查时先看图2那种列表再回头查代码。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 真实波动幅度均值                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnCalculate(const class="type">int rates_total,
                const class="type">int prev_calculated,
                const class="type">class="kw">datetime &Time[],
                const class="type">class="kw">double &Open[],
                const class="type">class="kw">double &High[],
                const class="type">class="kw">double &Low[],
                const class="type">class="kw">double &Close[],
                const class="type">long &TickVolume[],
                const class="type">long &Volume[],
                const class="type">int &Spread[])
  {
   if(prev_calculated!=rates_total)
      RegisterBuffer(ExtATRBuffer,"ATR",class="num">0);
…
class="type">void OnDeinit(const class="type">int reason)
{
   UnregisterBuffer(ExtATRBuffer);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                test.mq5 |
class=class="str">"cmt">//|                                                    Copyright class="num">2009, alsu |
class=class="str">"cmt">//|                                                      alsufx@gmail.com |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "class="num">2009, alsu"
class="macro">#class="kw">property link      "alsufx@gmail.com"
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#class="kw">property indicator_separate_window
class="macro">#class="kw">property indicator_buffers class="num">1
class="macro">#class="kw">property indicator_plots   class="num">1
class="macro">#include <exchng.mqh>
class=class="str">"cmt">//----绘制ATRCopy
class="macro">#class="kw">property indicator_label1  "ATRCopy"
class="macro">#class="kw">property indicator_type1   DRAW_LINE
class="macro">#class="kw">property indicator_color1  Red
class="macro">#class="kw">property indicator_style1  STYLE_SOLID
class="macro">#class="kw">property indicator_width1  class="num">1
class=class="str">"cmt">//--- 指标缓存
class="type">class="kw">double ATRCopyBuffer[];
class="type">class="kw">string atr_buffer;
class="type">class="kw">string buffers[];
class=class="str">"cmt">//+------------------------------------------------------------------+

把别人的 ATR 缓存直接搬进自定义指标

想在自己写的 MT5 指标里复用系统 ATR,不必重算,只要从全局缓冲区把现成数据抓出来。OnInit 里用 SetIndexBuffer(0,ATRCopyBuffer,INDICATOR_DATA) 把本地数组绑到第 0 号画线缓冲,指标才能把值画到副图。 OnCalculate 的核心是先 FindBuffers("ATR",0,NULL,0,buffers) 去全局变量里搜 ATR 描述符。返回 found>0 就说明目标指标已加载,把 buffers[0] 存进 atr_buffer;否则置 NULL,后面直接 break 不画,避免空指针报错。 抓到句柄后,从 prev_calculated 开始循环到 rates_total,每行调用 GetIndicatorValue(atr_buffer,i) 把 ATR 值抄进 ATRCopyBuffer[i],顺手 SetIndicatorValue(atr_buffer,i,i) 写回原缓冲。最后 return(rates_total) 而非 prev_calculated,能保证下一根 K 线重算时不会漏掉末尾数据。 这套写法在 EURUSD 15 分钟图上验证过:若图表未挂 ATR 指标,found 返回 0,副图全空;挂上默认 14 周期 ATR 后,重绘即同步。外汇与贵金属波动剧烈,指标复用仅降低算力占用,信号本身不代表方向,实盘仍属高风险。

MQL5 / C++
class=class="str">"cmt">//| 自定义指标初始化函数                                 			     |//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- 指标缓存映射
   SetIndexBuffer(class="num">0,ATRCopyBuffer,INDICATOR_DATA);
class=class="str">"cmt">//---
   class="kw">return(class="num">0);

}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 自定义指标迭代函数                             				      |//+------------------------------------------------------------------+
class="type">int OnCalculate(const class="type">int rates_total,
                const class="type">int prev_calculated,
                const class="type">class="kw">datetime& time[],
                const class="type">class="kw">double& open[],
                const class="type">class="kw">double& high[],
                const class="type">class="kw">double& low[],
                const class="type">class="kw">double& close[],
                const class="type">long& tick_volume[],
                const class="type">long& volume[],
                const class="type">int& spread[])
  {
class=class="str">"cmt">//---
   class="type">int found=FindBuffers("ATR",class="num">0,NULL,class="num">0,buffers);    class=class="str">"cmt">//在全局变量中搜索描述符
   if(found>class="num">0) atr_buffer=buffers[class="num">0];                class=class="str">"cmt">//如果我们发现它,将它保存到atr_buffer
   else atr_buffer=NULL;
   class="type">int i;
   for(i=prev_calculated;i<rates_total;i++)
   {
      if(atr_buffer==NULL) break;
      ATRCopyBuffer[i]=GetIndicatorValue(atr_buffer,i);  class=class="str">"cmt">//现在获取数据就简单了
      SetIndicatorValue(atr_buffer,i,i);                class=class="str">"cmt">//并且记录他们也方便

}
class=class="str">"cmt">//--- 返回prev_calculated的值用于下一次调用
   class="kw">return(rates_total);

}
class=class="str">"cmt">//+------------------------------------------------------------------+
让小布替你跑这套
跨指标缓冲区的读取链路小布盯盘已内置诊断,打开对应品种页即可看到哪些指标在反复占用句柄、哪些调用在拖慢刷新,你只管调架构。

常见问题

句柄只保证原指标计算一次,但首次绑定与缓冲区同步仍受终端互斥限制,占用资源高的指标初始化会阻塞其他调用方数秒,属于架构层瓶颈而非代码写错。
可以,小布盯盘的品种页会把已附加指标的句柄占用与缓冲区读取频次可视化,方便你定位是哪条链路在拖慢盘口响应。
客户端全局变量本质是跨程序拷贝,既吃 CPU 又引入传输时序问题,相关性校验得自己补,适合低频状态同步而非高频缓冲流转。
映射文件更轻量但 IO 边界明显,DLL 共享内存延迟低但要过签名与权限,实盘环境里后者维护成本常被低估。