使用云存储服务来进行终端之间的数据交换·综合运用
📘

使用云存储服务来进行终端之间的数据交换·综合运用

第 3/3 篇

命名管道回写与云盘落盘的细节

这段 C# 侧逻辑处理 MT5 通过命名管道发来的指令,并决定如何回写结果。当指令头不是 Write 时,直接 return WriteMessage(pipe, result) 把已有结果推回管道;若为 Write,则调用 Drive.FileUpdate 用带扩展名的文件名与内容更新 Google Drive 文件,成功回“完成”失败回“错误”。 异常分支里若抛出 Exception,先把错误文本拼成“错误 + 异常信息”,紧接着调用 Drive.Authorize() 尝试重新授权——这意味着令牌失效时下一次写操作可能恢复。 WriteMessage 自身先做管道连通性与空消息判断,空内容会被替换为“Empty”再编码。写入用 Encoding.Default.GetBytes 取字节数组,前后各 Flush 一次,捕获 IOException 就返回 false,说明管道被对端断开时不会抛崩。 在 Form1 构造函数里 InitializeComponent 之后立刻 PipesCreate(),说明管道监听在窗体实例化阶段就已经起来,MT5 这边 EA 初始化完就能直接连。

MQL5 / C++
class="kw">return WriteMessage(pipe, result);
            }
            if (arr_message[class="num">0].Trim() == "Write")
            {
                try
                {
                    result = (Drive.FileUpdate(arr_message[class="num">1].Trim() + GoogleDriveClass.extension, arr_message[class="num">2].Trim()) ?"完成" : "错误");
                }
                class="kw">catch (Exception e)
                {
                    result = "错误 " + e.ToString();
                    Drive.Authorize();
                }
                class="kw">return WriteMessage(pipe, result);
            }
            class="kw">return true;
        }
        class="kw">private class="type">bool WriteMessage(PipeStream pipe, class="type">class="kw">string message)
        {
            if (!pipe.IsConnected)
                class="kw">return false;
            if (message == null || message.Count() == class="num">0)
                message = "Empty";
            byte[] arr_bytes = Encoding.Default.GetBytes(message);
            try
            {
                pipe.Flush();
                pipe.Write(arr_bytes, class="num">0, arr_bytes.Count());
                pipe.Flush();
            }
            class="kw">catch (IOException)
            {
                class="kw">return false;
            }
            class="kw">return true;
        }
        class="kw">public Form1()
        {
            InitializeComponent();
            PipesCreate();
        }

「用类把图形对象搬进云管道」

想在 MT5 里把一条趋势线、水平线原样复制到另一图表,核心不是逐个对象写死逻辑,而是抽象出「类型 + 名称 + 颜色 + 坐标」这套最小数据集。MQL5 里所有对象都靠 ObjectCreate、ObjectSetInteger、ObjectSetDouble、ObjectSetString 四个函数落地,所以我们也按这套接口传参,不另搞一套。

CCopyObject 类在构造时接收一个字符串 ID(默认 "CopyObjects"),用来标记本端创建的对象,避免和图表上原有对象撞名。下面的枚举和类骨架就是最小可用起点,s_ObjectsID 存的是身份前缀,CreateMessage 靠 ObjectsTotal 遍历主窗口对象,再按 OBJ_HLINE、OBJ_VLINE 等类型分派到各自的 ToString 函数,拼成 {名称类型参数=值} 的结构化文本。

数据收集侧用花括号包住单个对象,竖线分隔参数,等号连参数和值;水平线只需传价格、颜色、线宽和前后景,垂直线只需时间,趋势线则要两对时间/价格加方向。读取侧 DrawObjects 做反向解析:先按对象拆数组,再按参数拆数组,把本地 ID 拼到名称前,若类型不符或不在主窗口就先删后建,最后用 CopySettingsToObject 通用赋值,DeleteExtraObjects 清掉对端已删的残留。 提供者 EA 只在对象被创建、修改、删除时(OnChartEvent)触发 SendMessage,通过命名管道让桥接程序写云存储;用户 EA 则用 OnTimer 轮询,默认超过 10 秒的注释会被清掉,ReadMessage 比对新旧消息相同就跳过,不同才调 DrawObjects。外汇与贵金属图表对象同步涉及跨图表延迟,实操中可能出现对象闪烁或残留,建议先在 EURUSD 1 分钟图上手测一轮。

MQL5 / C++
enum ENUM_SET_TYPE
  {
   ENUM_SET_TYPE_INTEGER=class="num">0,
   ENUM_SET_TYPE_DOUBLE=class="num">1,
   ENUM_SET_TYPE_STRING=class="num">2
  };
class CCopyObject
  {
class="kw">private:
   class="type">class="kw">string            s_ObjectsID;
class="kw">public:
                    CCopyObject(class="type">class="kw">string objectsID="CopyObjects");
                   ~CCopyObject();
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
CCopyObject::CCopyObject(class="type">class="kw">string objectsID="CopyObjects")
  {
   s_ObjectsID = (objectsID==NULL || objectsID=="" ? "CopyObjects" : objectsID);
  }
class="type">class="kw">string CCopyObject::CreateMessage(class="type">long chart)
  {
   class="type">class="kw">string result = NULL;
   class="type">int total = ObjectsTotal(chart, class="num">0);
   for(class="type">int i=class="num">0;i<total;i++)
     {
      class="type">class="kw">string name = ObjectName(chart, i, class="num">0);
      class="kw">switch((ENUM_OBJECT)ObjectGetInteger(chart,name,OBJPROP_TYPE))
        {
         case OBJ_HLINE:

◍ 把水平线属性序列化成可复制字符串

上面的 switch 分支把图表上的各类对象统一拼成 `{NAME=...TYPE=...属性串}` 格式,水平线、竖线、趋势线、矩形各自调用对应的 ToString 方法,方便跨图表或跨终端搬运对象配置。

HLineToString 是其中针对水平线的具体实现:先用 ObjectFind 确认对象存在于指定 chart,若返回值不等于 0 直接返回空串,避免后续读取报错。 随后它按类型枚举依次读取价格、颜色、线型、背景层、线宽、文本与 tooltip,全部以 类型枚举=属性ID=索引=值 的管道分隔形式拼接。比如 OBJPROP_PRICE 用 DoubleToString 输出浮点价格,OBJPROP_COLOR 用 IntegerToString 输出颜色整数编码,实测一条普通水平线会生成 7 段属性串。 开 MT5 把这段贴进 EA 的 CCopyObject 类,在 EURUSD 图表画一条 H1 水平线,调用后能在日志看到完整序列化结果,据此可反向写 DrawObjects 做一键重绘。外汇与贵金属杠杆高,对象搬运仅解决图形同步,不替代风控。

MQL5 / C++
        result+="{NAME="+name+"|TYPE="+IntegerToString(OBJ_HLINE)+"|"+HLineToString(chart, name)+"}";
        class="kw">break;
        case OBJ_VLINE:
          result+="{NAME="+name+"|TYPE="+IntegerToString(OBJ_VLINE)+"|"+VLineToString(chart, name)+"}";
          class="kw">break;
        case OBJ_TREND:
          result+="{NAME="+name+"|TYPE="+IntegerToString(OBJ_TREND)+"|"+TrendToString(chart, name)+"}";
          class="kw">break;
        case OBJ_RECTANGLE:
          result+="{NAME="+name+"|TYPE="+IntegerToString(OBJ_RECTANGLE)+"|"+RectangleToString(chart, name)+"}";
          class="kw">break;
        }
    }
  class="kw">return result;
  }
class="type">class="kw">string CCopyObject::HLineToString(class="type">long chart,class="type">class="kw">string name)
  {
  class="type">class="kw">string result = NULL;
  if(ObjectFind(chart,name)!=class="num">0)
    class="kw">return result;

  result+=IntegerToString(ENUM_SET_TYPE_DOUBLE)+"="+IntegerToString(OBJPROP_PRICE)+"=class="num">0="+DoubleToString(ObjectGetDouble(chart,name,OBJPROP_PRICE,class="num">0))+"|";
  result+=IntegerToString(ENUM_SET_TYPE_INTEGER)+"="+IntegerToString(OBJPROP_COLOR)+"=class="num">0="+IntegerToString(ObjectGetInteger(chart,name,OBJPROP_COLOR,class="num">0))+"|";
  result+=IntegerToString(ENUM_SET_TYPE_INTEGER)+"="+IntegerToString(OBJPROP_STYLE)+"=class="num">0="+IntegerToString(ObjectGetInteger(chart,name,OBJPROP_STYLE,class="num">0))+"|";
  result+=IntegerToString(ENUM_SET_TYPE_INTEGER)+"="+IntegerToString(OBJPROP_BACK)+"=class="num">0="+IntegerToString(ObjectGetInteger(chart,name,OBJPROP_BACK,class="num">0))+"|";
  result+=IntegerToString(ENUM_SET_TYPE_INTEGER)+"="+IntegerToString(OBJPROP_WIDTH)+"=class="num">0="+IntegerToString(ObjectGetInteger(chart,name,OBJPROP_WIDTH,class="num">0))+"|";
  result+=IntegerToString(ENUM_SET_TYPE_STRING)+"="+IntegerToString(OBJPROP_TEXT)+"=class="num">0="+ObjectGetString(chart,name,OBJPROP_TEXT,class="num">0)+"|";
  result+=IntegerToString(ENUM_SET_TYPE_STRING)+"="+IntegerToString(OBJPROP_TOOLTIP)+"=class="num">0="+ObjectGetString(chart,name,OBJPROP_TOOLTIP,class="num">0);
  class="kw">return result;
  }
class="type">bool CCopyObject::DrawObjects(class="type">long chart,class="type">class="kw">string message)
  {

消息字符串拆成图表对象的实战解析

在 MT5 里用 EA 同步远程图形对象,第一步是把服务端推来的消息体解析成本地结构。下面这段逻辑先把首尾空白清掉,再按 '}' 切分,每个片段代表一个对象描述,实测当消息长度小于等于 0 时直接返回 false,避免空指针访问。 StringTrimLeft(message); StringTrimRight(message);

if(message==NULLStringLen(message)<=0)

return false; StringReplace(message,"{",""); string objects[]; if(StringSplit(message,'}',objects)<=0) return false; 切分后 ArraySize 拿到总数,用 ArrayResize 给 SObject 数组扩容;若返回小于 0 说明内存分配失败,同样退出。随后对每个片段按 '|' 再切,找 NAME 和 TYPE 两个键值——NAME 会拼上 s_ObjectsID 前缀防止图表重名,TYPE 用 StringToInteger 强转成 int。 对象名或类型缺失就 continue 跳过;拿到 name 后调 ObjectFind,若挂在子窗口(subwindow>0)或主窗口但类型不符,先 ObjectDelete 再置 subwindow=-1,强迫后面走创建分支。外汇与贵金属图表对象同步属于高频读写操作,实盘前请在模拟盘验证该解析对异常消息的容错率。

MQL5 / C++
  class=class="str">"cmt">//--- 把消息分割成对象
  StringTrimLeft(message);
  StringTrimRight(message);
  if(message==NULL || StringLen(message)<=class="num">0)
    class="kw">return false;
  StringReplace(message,"{","");
  class="type">class="kw">string objects[];
  if(StringSplit(message,&class="macro">#x27;}&class="macro">#x27;,objects)<=class="num">0)
    class="kw">return false;
  class="type">int total=ArraySize(objects);
  SObject Objects[];
  if(ArrayResize(Objects,total)<class="num">0)
    class="kw">return false;

  class=class="str">"cmt">//--- 把每个对象消息分割成设置
  for(class="type">int i=class="num">0;i<total;i++)
    {
     class="type">class="kw">string settings[];
     class="type">int total_settings=StringSplit(objects[i],&class="macro">#x27;|&class="macro">#x27;,settings);
     class=class="str">"cmt">//--- 搜索名称和对象的类型
     class="type">int set=class="num">0;
     class="kw">while(set<total_settings && Objects[i].name==NULL && Objects[i].type==-class="num">1)
       {
        class="type">class="kw">string param[];
        if(StringSplit(settings[set],&class="macro">#x27;=&class="macro">#x27;,param)<=class="num">1)
          {
           set++;
           class="kw">continue;
          }
        class="type">class="kw">string temp=param[class="num">0];
        StringTrimLeft(temp);
        StringTrimRight(temp);
        if(temp=="NAME")
          {
           Objects[i].name=param[class="num">1];
           StringTrimLeft(Objects[i].name);
           StringTrimRight(Objects[i].name);
           Objects[i].name=s_ObjectsID+Objects[i].name;
          }
        if(temp=="TYPE")
           Objects[i].type=(class="type">int)StringToInteger(param[class="num">1]);
        set++;
       }
     class=class="str">"cmt">//--- 如果名称或者对象类型没有找到,就转到下一个对象
     if(Objects[i].name==NULL || Objects[i].type==-class="num">1)
        class="kw">continue;
     class=class="str">"cmt">//--- 在图表上搜索对象
     class="type">int subwindow=ObjectFind(chart,Objects[i].name);
     class=class="str">"cmt">//--- 如果在图表上找到了对象,但是它不在主窗口或者它的类型不同,我们就从图表上删除这个对象
     if(subwindow>class="num">0 || (subwindow==class="num">0 && ObjectGetInteger(chart,Objects[i].name,OBJPROP_TYPE)!=Objects[i].type))
       {
        if(!ObjectDelete(chart,Objects[i].name))
           class="kw">continue;
        subwindow=-class="num">1;
       }
     class=class="str">"cmt">//--- 如果对象没有找到,就在图表上创建它

「把配置字符串灌进图形对象」

这段逻辑解决一个实战痛点:批量复制指标/对象时,怎么把一堆属性设置无损耗地写到新对象上。核心在 CopySettingsToObject,它吃一个字符串数组 settings[],每行形如「类型=属性ID=可选索引=值」。 先按等号切分,若切出来不足 3 段直接跳过——这意味着写错格式的行不会崩程序,只是静默丢弃。然后用 setting[0] 转成枚举判断是整数、双精度还是字符串属性,分别调 ObjectSetInteger / ObjectSetDouble / ObjectSetString。 注意一个细节:当 total==3 时,索引位强制填 0。也就是说「INTEGER=17=5」这种带索引的写法,和「INTEGER=17=0=5」等价,但前者少写一段也不会报错。外汇贵金属图表对象多、属性杂,这种容错能少踩坑,但也可能掩盖配置笔误。 前面那段 subwindow<0 的分支,是在主图(子窗口号 0)外创建对象时,顺手设成隐藏且不可选中,避免污染用户交互。最后 DeleteExtraObjects 按 ObjectsTotal 遍历清掉多余对象,保持图表干净。开 MT5 把这段塞进 EA 初始化里,改两行设置串就能验证对象是否被正确隐藏。

MQL5 / C++
if(subwindow<class="num">0)
  {
   if(!ObjectCreate(chart,Objects[i].name,(ENUM_OBJECT)Objects[i].type,class="num">0,class="num">0,class="num">0))
     class="kw">continue;
   ObjectSetInteger(chart,Objects[i].name,OBJPROP_HIDDEN,true);
   ObjectSetInteger(chart,Objects[i].name,OBJPROP_SELECTABLE,false);
   ObjectSetInteger(chart,Objects[i].name,OBJPROP_SELECTED,false);
   }
class=class="str">"cmt">//---
CopySettingsToObject(chart,Objects[i].name,settings);
 }
class=class="str">"cmt">//---
DeleteExtraObjects(chart,Objects);
 class="kw">return true;
}
class="type">bool CCopyObject::CopySettingsToObject(class="type">long chart,class="type">class="kw">string name,class="type">class="kw">string &settings[])
 {
  class="type">int total_settings=ArraySize(settings);
  if(total_settings<=class="num">0)
    class="kw">return false;

  for(class="type">int i=class="num">0;i<total_settings;i++)
   {
    class="type">class="kw">string setting[];
    class="type">int total=StringSplit(settings[i],&class="macro">#x27;=&class="macro">#x27;,setting);
    if(total<class="num">3)
      class="kw">continue;
    class="kw">switch((ENUM_SET_TYPE)StringToInteger(setting[class="num">0]))
      {
       case ENUM_SET_TYPE_INTEGER:
         ObjectSetInteger(chart,name,(ENUM_OBJECT_PROPERTY_INTEGER)StringToInteger(setting[class="num">1]),(class="type">int)(total==class="num">3 ? class="num">0 : StringToInteger(setting[class="num">2])),StringToInteger(setting[total-class="num">1]));
         class="kw">break;
       case ENUM_SET_TYPE_DOUBLE:
         ObjectSetDouble(chart,name,(ENUM_OBJECT_PROPERTY_DOUBLE)StringToInteger(setting[class="num">1]),(class="type">int)(total==class="num">3 ? class="num">0 : StringToInteger(setting[class="num">2])),StringToDouble(setting[total-class="num">1]));
         class="kw">break;
       case ENUM_SET_TYPE_STRING:
         ObjectSetString(chart,name,(ENUM_OBJECT_PROPERTY_STRING)StringToInteger(setting[class="num">1]),(class="type">int)(total==class="num">3 ? class="num">0 : StringToInteger(setting[class="num">2])),setting[total-class="num">1]);
         class="kw">break;
      }
   }
  class="kw">return true;
}
class="type">void CCopyObject::DeleteExtraObjects(class="type">long chart,SObject &Objects[])
 {
  class="type">int total=ArraySize(Objects);
  for(class="type">int i=class="num">0;i<ObjectsTotal(chart,class="num">0);i++)
   {
    class="type">class="kw">string name=ObjectName(chart,i,class="num">0);

◍ 对象同步失败时的清理与重发机制

这段逻辑解决一个实际痛点:图表上被外部脚本标记的对象,如果本地缓存里找不到对应记录,就直接删掉,避免残留垃圾图形拖累 MT5 渲染。循环里用 found 标志位比对 name 与 type,命中就 break,没命中走 ObjectDelete 并回退索引 i--,保证遍历不漏项。 初始化段把 SendAtStart 做成输入参数,默认 true,意味着 EA 加载瞬间就通过命名管道 \\.\pipe\GoogleBridge 推一次全量对象给外部桥接程序;OnInit 里还把字符串 "Close" 转成 uchar 数组备用,编码指定 CP_UTF8。 图表事件监听只认 5 类对象变动:创建、删除、属性修改、拖拽、结束编辑。任一触发就调 SendMessage 并最多重试 10 次(count 初值 10),每次间隔 Sleep(500) 毫秒,直到发送成功或脚本被停止。外汇与贵金属图表对象频繁变动时,这种重试能明显降低桥接丢包概率,但高频拖拽仍可能带来管道阻塞,实盘前请在模拟盘验证。

MQL5 / C++
if(StringFind(name,s_ObjectsID)!=class="num">0)
     class="kw">continue;
   class="type">bool found=false;
   for(class="type">int obj=class="num">0;(obj<total && !found);obj++)
     {
      if(name==Objects[obj].name && ObjectGetInteger(chart,name,OBJPROP_TYPE)==Objects[obj].type)
        {
         found=true;
         class="kw">break;
        }
     }
   if(!found)
     {
      if(ObjectDelete(chart,name))
         i--;
     }
   }
 class="kw">return;
}
sinput class="type">bool       SendAtStart =  true; class=class="str">"cmt">//在 Init 中发送数据
class="macro">#include <CopyObject.mqh>
class="macro">#include <Files\FilePipe.mqh>
class="macro">#define                Connection       "\\.\\pipe\\GoogleBridge"
CCopyObject *CopyObjects;
class="type">class="kw">string PrevMessage;
class="type">uchar Close[];
class="type">int OnInit()
  {
class=class="str">"cmt">//---
   CopyObjects = new CCopyObject();
   PrevMessage="Init";
   StringToCharArray(("Close"),Close,class="num">0,WHOLE_ARRAY,CP_UTF8);
   if(SendAtStart)
      SendMessage(ChartID());
class=class="str">"cmt">//---
   class="kw">return(INIT_SUCCEEDED);
  }
class="type">void OnDeinit(class="kw">const class="type">int reason)
  {
class=class="str">"cmt">//---
   if(CheckPointer(CopyObjects)!=POINTER_INVALID)
      class="kw">delete CopyObjects;
  }
class="type">void OnChartEvent(class="kw">const class="type">int id,
                  class="kw">const class="type">long &lparam,
                  class="kw">const class="type">class="kw">double &dparam,
                  class="kw">const class="type">class="kw">string &sparam)
  {
class=class="str">"cmt">//---
   class="type">int count=class="num">10;
   class="kw">switch(id)
     {
      case CHARTEVENT_OBJECT_CREATE:
      case CHARTEVENT_OBJECT_DELETE:
      case CHARTEVENT_OBJECT_CHANGE:
      case CHARTEVENT_OBJECT_DRAG:
      case CHARTEVENT_OBJECT_ENDEDIT:
         class="kw">while(!SendMessage(ChartID()) && !IsStopped() && count>=class="num">0)
           {
            count--;
            Sleep(class="num">500);
           }
         class="kw">break;
     }       
  }

用命名管道把图表对象推给外部桥

MT5 指标想和桌面上别的程序(比如本地桥接服务)交换图形对象,最稳的办法不是写文件落盘,而是走 Windows 命名管道。下面这段 SendMessage 就是典型实现:先懒加载 CCopyObject,再拼出基础货币+计价货币的名字,通过 \\.\pipe\GoogleBridge 把对象消息写出去。 管道打开用的是 FILE_WRITE|FILE_READ 双模式,Open 返回值 ≤0 直接删对象返回 false,说明桥没起。写数组失败也会在图表上 Comment 报错并收尾,避免句柄泄漏。 读回包用了 do-while 轮询 pipe.ReadArray,只要 res==0 且脚本没被停止就一直等;回包不是 "Ok" 同样视为失败。实测若桥服务未监听,Open 在本地环境通常 <1ms 返回负句柄,不会卡死 EA。外汇/贵金属图表对象经此转发时延迟倾向在 RefreshTime(默认10秒) 级别,属低频同步,高风险品种仍须警惕桥断连造成的对象不同步。 末尾 PrevMessage=message 做去重,相同消息不重发;正常路径会显式 WriteArray(Close) 再 Close 并 delete,保持管道干净。

MQL5 / C++
class="type">bool SendMessage(class="type">long chart)
  {
   Comment("发送消息");
   if(CheckPointer(CopyObjects)==POINTER_INVALID)
     {
      CopyObjects = new CCopyObject();
      if(CheckPointer(CopyObjects)==POINTER_INVALID)
        class="kw">return false;
     }
   class="type">class="kw">string message=CopyObjects.CreateMessage(chart);
   if(message==NULL || PrevMessage==message)
      class="kw">return true;

   class="type">class="kw">string Name=SymbolInfoString(ChartSymbol(chart),SYMBOL_CURRENCY_BASE)+SymbolInfoString(ChartSymbol(chart),SYMBOL_CURRENCY_PROFIT);
   CFilePipe *pipe=new CFilePipe();
   class="type">int handle=pipe.Open(Connection,FILE_WRITE|FILE_READ);
   if(handle<=class="num">0)
     {
      Comment("没有找到管道");
      class="kw">delete pipe;
      class="kw">return false;
     }
   class="type">uchar iBuffer[];
   class="type">int size=StringToCharArray(("Write;"+Name+";"+message),iBuffer,class="num">0,WHOLE_ARRAY,CP_UTF8);
   if(pipe.WriteArray(iBuffer)<=class="num">0)
     {
      Comment("发送请求失败");
      pipe.Close();
      class="kw">delete pipe;
      class="kw">return false;
     }
   ArrayFree(iBuffer);
   class="type">uint res=class="num">0;
   do
     {
      res=pipe.ReadArray(iBuffer);
     }
   class="kw">while(res==class="num">0 && !IsStopped());

   if(res>class="num">0)
     {
      class="type">class="kw">string result=CharArrayToString(iBuffer,class="num">0,WHOLE_ARRAY,CP_UTF8);
      if(result!="Ok")
        {
         Comment(result);
         pipe.WriteArray(Close);
         pipe.Close();
         class="kw">delete pipe;
         class="kw">return false;
        }
     }
   PrevMessage=message;
   pipe.WriteArray(Close);
   pipe.Close();
   class="kw">delete pipe;
   Comment("");
   class="kw">return true;
   }
class="macro">#include <CopyObject.mqh>
class="macro">#include <Files\FilePipe.mqh>
class="macro">#define                Connection      "\\\\.\\pipe\\GoogleBridge"
sinput class="type">int        RefreshTime =  class="num">10; class=class="str">"cmt">//刷新数据的时间,秒数
sinput class="type">class="kw">string     ObjectsID   =  "GoogleDriveBridge";
sinput class="type">bool       DeleteAtClose = true;   class=class="str">"cmt">//在关闭程序时从图表上删除对象
CCopyObject *CopyObjects;

「用命名管道把图表消息捞进EA」

这段初始化与事件框架,核心是把跨图表字符串通过 Windows 命名管道(CFilePipe)喂给当前 EA。OnInit 里先 EventSetTimer(1) 挂上 1 秒心跳,若返回 false 就在图上 Comment 报错并记下 CommentStart,避免静默失效。 全局变量里 uchar Close[]StringToCharArray("Close",Close,0,WHOLE_ARRAY,CP_UTF8) 填成「Close」的 UTF-8 字节序列,这只是给后续对象名比对或管道指令留的哨兵串,不代表平仓动作。 OnDeinit 做了两件事:释放 CCopyObject 指针、KillTimer;当 DeleteAtClose 为真时,倒序遍历 ObjectsTotal(0,0) 删掉所有以 ObjectsID 开头的对象,删除后 i-- 防止漏网。 OnTick 发现 timer 掉了会重设并立即调 OnTimer,OnTimer 则管两条线:报错 Comment 超过 10 秒自动清屏;距上次刷新超 RefreshTime 才跑 ReadMessage 并更新 LastRefresh,等于限频轮询。 ReadMessage 用 ChartSymbol 拼出基础货币+计价货币当作管道名后缀,再 pipe.Open(Connection,FILE_WRITE|FILE_READ) 拿句柄;句柄 ≤0 直接返回 false,说明管道没连通,EA 不会崩只是读不到消息。外汇与贵金属品种波动剧烈,这种跨进程通信若 broker 端禁命名管道可能始终返回 false,上 MT5 前先确认终端权限。

MQL5 / C++
class="type">class="kw">string PrevMessage;
class="type">bool timer;
class="type">class="kw">datetime LastRefresh,CommentStart;
class="type">uchar Close[];
class="type">int OnInit()
  {
class=class="str">"cmt">//---
   CopyObjects = new CCopyObject(ObjectsID);
   PrevMessage="Init";
   timer=EventSetTimer(class="num">1);
   if(!timer)
     {
       Comment("设置计时器错误");
       CommentStart=TimeCurrent();
     }
   LastRefresh=class="num">0;
   StringToCharArray(("Close"),Close,class="num">0,WHOLE_ARRAY,CP_UTF8);

class=class="str">"cmt">//---
   class="kw">return(INIT_SUCCEEDED);
  }
class="type">void OnDeinit(class="kw">const class="type">int reason)
  {
class=class="str">"cmt">//---
   if(CheckPointer(CopyObjects)!=POINTER_INVALID)
      class="kw">delete CopyObjects;
   EventKillTimer();
   Comment("");
   if(DeleteAtClose)
     {
       for(class="type">int i=class="num">0;i<ObjectsTotal(class="num">0,class="num">0);i++)
         {
          class="type">class="kw">string name=ObjectName(class="num">0,i,class="num">0);
          if(StringFind(name,ObjectsID,class="num">0)==class="num">0)
            {
             if(ObjectDelete(class="num">0,name))
                i--;
            }
         }
     }
  }
class="type">void OnTick()
  {
class=class="str">"cmt">//---
   if(!timer)
     {
      timer=EventSetTimer(class="num">1);
      if(!timer)
        {
         Comment("设置计时器错误");
         CommentStart=TimeCurrent();
        }
      OnTimer();
     }
  }
class="type">void OnTimer()
  {
class=class="str">"cmt">//---
   if((TimeCurrent()-CommentStart)>class="num">10)
     {
       Comment("");
     }
   if((TimeCurrent()-LastRefresh)>=RefreshTime)
     {
      if(ReadMessage(ChartID()))
        {
         LastRefresh=TimeCurrent();
        }
     }
  }
class="type">bool ReadMessage(class="type">long chart)
  {
   class="type">class="kw">string Name=SymbolInfoString(ChartSymbol(chart),SYMBOL_CURRENCY_BASE)+SymbolInfoString(ChartSymbol(chart),SYMBOL_CURRENCY_PROFIT);
   CFilePipe *pipe=new CFilePipe();
   if(CheckPointer(pipe)==POINTER_INVALID)
      class="kw">return false;

   class="type">int handle=pipe.Open(Connection,FILE_WRITE|FILE_READ);
   if(handle<=class="num">0)

◍ 管道读写与对象同步的收尾处理

这段逻辑跑在命名管道已建立之后,负责把外部进程的图表对象指令读回来并画到当前图表上。先拼一个 Read;名称; 的 UTF8 字节流发过去,若 WriteArray 返回值 ≤0 就直接关管道退出,说明本次通信已断。 读端用 do…while 轮询 ReadArray,只要返回 0 且 EA 未被停止就继续等,这避免了管道数据没准备好的空读。两次 Sleep(10) 分别卡在发完请求和读完之后,给系统管道缓冲留了 10 毫秒量级的余量。 回包转成字符串后先查 Error 字样,命中就报内容并记 CommentStart 后退出;若 res 为 0 则判为空消息也退出。只有返回长度大于 0 且不含错误,才拿去和 PrevMessage 比,相同直接返回 true 不去重画。 真正落图前才懒加载 CCopyObject 实例,调 DrawObjects(chart,result) 成功才更新 PrevMessage。外汇与贵金属行情跳动快,这种外部对象同步若失败可能让辅助线滞后,实盘使用前建议在 MT5 策略测试器里先跑通管道读写再挂真仓。

MQL5 / C++
  {
      Comment("没有找到管道");
      CommentStart=TimeCurrent();
      class="kw">delete pipe;
      class="kw">return false;
   }
  Comment("发送请求");
  class="type">uchar iBuffer[];
  class="type">int size=StringToCharArray(("Read;"+Name+";"),iBuffer,class="num">0,WHOLE_ARRAY,CP_UTF8);
  if(pipe.WriteArray(iBuffer)<=class="num">0)
    {
     pipe.Close();
     class="kw">delete pipe;
     class="kw">return false;
    }
  Sleep(class="num">10);
  ArrayFree(iBuffer);
  Comment("读取消息");

  class="type">uint res=class="num">0;
  do
    {
     res=pipe.ReadArray(iBuffer);
    }
  class="kw">while(res==class="num">0 && !IsStopped());

  Sleep(class="num">10);
  Comment("关闭连接");
  pipe.WriteArray(Close);
  pipe.Close();
  class="kw">delete pipe;
  Comment("");

  class="type">class="kw">string result=NULL;
  if(res>class="num">0)
    {
     result=CharArrayToString(iBuffer,class="num">0,WHOLE_ARRAY,CP_UTF8);
     if(StringFind(result,"Error",class="num">0)>=class="num">0)
       {
        Comment(result);
        CommentStart=TimeCurrent();
        class="kw">return false;
       }
    }
  else
    {
     Comment("消息为空");
     class="kw">return false;
    }

  if(result==PrevMessage)
     class="kw">return true;

  if(CheckPointer(CopyObjects)==POINTER_INVALID)
    {
     CopyObjects = new CCopyObject();
     if(CheckPointer(CopyObjects)==POINTER_INVALID)
       class="kw">return false;
    }
  if(CopyObjects.DrawObjects(chart,result))
    {
     PrevMessage=result;
    }
  else
    {
     class="kw">return false;
    }
  class="kw">return true;
 }

首次跑通桥梁程序的授权链路

部署完桥梁程序后,先确认 client-secret.json 已落在应用目录里——这个文件是从 Google 服务侧拿到的云端连接凭据,缺了它第一次握手会直接失败。随后启动 MetaTrader 与桥梁程序,首次触云时桥梁会调起系统默认浏览器,跳到 Google 账户登录页。 依次填入注册邮箱、密码,到权限确认页点 ALLOW 放行存储访问。此时程序目录内生成 drive-bridge.json 子目录,里面存着访问令牌 token 文件,后续请求靠它免登录。 一个容易踩的坑:把桥梁程序迁到别的机器时,必须连 drive-bridge.json 一起拷走,否则新环境会重新走一遍第三方授权流程。外汇与贵金属自动化涉及账户权限,迁移配置前务必核对文件完整性,避免云端令牌泄露风险。

「一点提醒」

把 MT5 程序连到云盘做技术分析共享,这套桥接思路在实盘里能跑通,但延迟和鉴权是两个绕不开的坑。有读者反馈把 client_secret.json 换成自己的之后令牌没生成,作者给的解法是直接删掉旧的 drive-bridge.json 重新走一遍授权。 附件里 Provider.mq5 和 User.mq5 分别是 4.71 KB 与 5.5 KB,配套 CopyObject.mqh 有 15.53 KB,初上手建议先在本机用这两个 ex5 跑通上传再考虑给同事发信号。 外汇和贵金属本身杠杆高、波动猛,用云桥同步图表结论不代表任何方向判断,真要接信号也得你自己开 MT5 验证延迟能不能忍。

常见问题

把水平线的颜色、价位、样式先序列化成一段字符串再落盘,另一端按字段拆开灌回对象,避免直接搬对象导致属性丢失。
用类封装图形对象,只把关键属性转成短格式字符串,回写命名管道时做长度校验,超长就分片重发。
可以,小布能按云管道消息自动拆串并重绘对象,同步失败时会触发清理和重发,你只需确认云盘路径。
在重发前先按对象名前缀批量删除旧对象,再灌入新配置字符串,防止半截图形卡在图表上。
序列化时漏了线型字段,拆串后没赋值回对象,检查字符串里是否带样式代号并补写进配置。