使用云存储服务来进行终端之间的数据交换·综合运用
命名管道回写与云盘落盘的细节
这段 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 初始化完就能直接连。
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 分钟图上手测一轮。
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 做一键重绘。外汇与贵金属杠杆高,对象搬运仅解决图形同步,不替代风控。
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==NULL | StringLen(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,强迫后面走创建分支。外汇与贵金属图表对象同步属于高频读写操作,实盘前请在模拟盘验证该解析对异常消息的容错率。
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 初始化里,改两行设置串就能验证对象是否被正确隐藏。
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) 毫秒,直到发送成功或脚本被停止。外汇与贵金属图表对象频繁变动时,这种重试能明显降低桥接丢包概率,但高频拖拽仍可能带来管道阻塞,实盘前请在模拟盘验证。
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,保持管道干净。
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 前先确认终端权限。
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 策略测试器里先跑通管道读写再挂真仓。
{
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 验证延迟能不能忍。