一个使用命名管道在 MetaTrader 5 客户端之间进行通信的无 DLL 解决方案·进阶篇
(2/3)· 不用 DLL、不写文件,靠命名管道在多个 MT5 终端间跑通报价流
很多交易者想让不同 MT5 客户端共享实时报价,却还在用文件互写那套笨办法,延迟高还容易锁文件。其实系统自带的命名管道能直接打通进程,只是多数人被 DLL 门槛劝退。本篇接着把无 DLL 的管道通信落到可跑的代码结构上。
用命名管道在 MT5 里搬运字符串与报价
把 EA 和外部程序打通,命名管道是最直接的办法之一。下面这组方法封装了 ANSI 字符串和 MqlTick 报价的双向读写,开 MT5 建个 CNamedPipe 类就能直接抄。 WriteANSI 先调用 StringToCharArray 把 MQL5 的 Unicode 字符串压成 uchar 数组,再分两次 WriteFile:第一次写 int 类型的字节长度,第二次写真实数据,返回实际写入字节数。 ReadANSI 反过来,先读一个 int 拿到待收字节数,再按长度读 uchar 数组,最后 CharArrayToString 还原成 MQL5 字符串;若本次没读到数据(bytesRead 为 0)就返回空串。 WriteTick 和 ReadTick 专门搬 MqlTick 结构,用 MQLTICK_SIZE 常量控制传输体量,省去自己算字段偏移。外汇与贵金属市场高杠杆、报价跳变频繁,管道读写若不同步可能丢 tick,实盘前务必在模拟环境跑通。
class="type">int CNamedPipe::WriteANSI(class="type">class="kw">string message) { class="type">int bytesToWrite, bytesWritten; class="type">uchar ANSIarray[]; bytesToWrite = StringToCharArray(message, ANSIarray); WriteFile(hPipe,bytesToWrite,class="kw">sizeof(class="type">int),bytesWritten,class="num">0); WriteFile(hPipe,ANSIarray,bytesToWrite,bytesWritten,class="num">0); class="kw">return bytesWritten; } class="type">class="kw">string CNamedPipe::ReadANSI(class="type">void) { class="type">class="kw">string ret; class="type">uchar ANSIarray[STR_SIZE]; class="type">int bytesRead, bytesToRead; ReadFile(hPipe,bytesToRead,class="kw">sizeof(class="type">int),bytesRead,class="num">0); ReadFile(hPipe,ANSIarray,bytesToRead,bytesRead,class="num">0); if(bytesRead!=class="num">0) ret = CharArrayToString(ANSIarray); class="kw">return ret; } class="type">int CNamedPipe::WriteTick(class="type">MqlTick &outgoing) { class="type">int bytesWritten; WriteFile(hPipe,outgoing,MQLTICK_SIZE,bytesWritten,class="num">0); class="kw">return bytesWritten; } class="type">int CNamedPipe::ReadTick(class="type">MqlTick &incoming) { class="type">int bytesRead; ReadFile(hPipe,incoming,MQLTICK_SIZE,bytesRead,NULL); class="kw">return bytesRead; }
◍ 用命名管道把报价推给外部客户端
在 MT5 里做跨进程报价中转,最轻量的办法是命名管道。服务端脚本先建好管道并卡在连接等待上,客户端没连进来之前它只在日志里刷“等待客户端连接.”,不占行情线程。 客户端一旦连上,服务端进入内层循环不断 ReadTick(),每收到一次 tick 就把计数器 i 加 1 并 Print 出“N即时价收到.”。实测在 EURUSD 正常波动时段,一分钟内管道可能推几百到上千个 tick,具体量级取决于品种流动性和客户端消费速度。 客户端断开时,ReadTick 返回 false 且 GetError() 拿到 ERROR_BROKEN_PIPE,服务端打印断开日志、调用 Disconnect() 并 break 回外层,重新等下一个客户端。若外层循环发现全局变量 gvar0 存在就整体退出;没有 gvar0 时,只能去图表右键 Expert List 手动停掉脚本。 别把 gvar0 当摆设 想让服务端干净退出,在另一个脚本或终端里 GlobalVariableSet("gvar0",1) 比手动去 EA 列表找它更稳,尤其多图表挂着的时候。
class=class="str">"cmt">//+------------------------------------------------------------------ class=class="str">"cmt">//| NamedPipeServer.mq5 | class=class="str">"cmt">//| Copyright class="num">2010, Investeo.pl | class=class="str">"cmt">//| http:/Investeo.pl | class=class="str">"cmt">//+------------------------------------------------------------------ class="macro">#class="kw">property copyright "Copyright class="num">2010, Investeo.pl" class="macro">#class="kw">property link "http:/Investeo.pl" class="macro">#class="kw">property version "class="num">1.00" class="macro">#include <CNamedPipes.mqh> CNamedPipe pipe; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 交易程序初始函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class="type">bool tickReceived; class="type">int i=class="num">0; if(pipe.Create()==true) while (GlobalVariableCheck("gvar0")==class="kw">false) { Print("等待客户端连接."); if (pipe.Connect()==true) Print("管道已连接"); while(true) { do { tickReceived=pipe.ReadTick(); if(tickReceived==class="kw">false) { if(GetError()==ERROR_BROKEN_PIPE) { Print("客户端从管道断开 "+pipe.Name()); pipe.Disconnect(); break; } } else i++; Print(IntegerToString(i) + "即时价收到."); } while(tickReceived==true); if (i>class="num">0) { Print(IntegerToString(i) + "即时价收到."); i=class="num">0; };
「管道断开时的退出判定」
在 MQL5 里用命名管道做跨程序通信时,最怕终端一侧突然退出。若服务端崩了,客户端再读就会触发 ERROR_BROKEN_PIPE,这时必须立刻 break,否则会卡死在循环里空耗 CPU。 下面这一行是实战里常用的兜底判断:只要全局变量 gvar0 存在,或者系统返回的错误码等于 ERROR_BROKEN_PIPE,就直接跳出读取循环,随后 pipe.Close() 释放句柄。 GlobalVariableCheck 返回 true 说明外部程序已经置了退出标志,而 ERROR_BROKEN_PIPE 是管道断裂的内核级报错,两者任一成立都代表链路不可用了。外汇与贵金属品种波动大、EA 常需长开,这种判定能避免 MT5 终端在断线重连时挂起。
if(GlobalVariableCheck("gvar0")==true || (GetError()==ERROR_BROKEN_PIPE)) break;
用命名管道把逐笔报价甩出 MT5
想让外部程序实时吃到 MT5 的逐笔报价,最轻量的做法是在指标里开一条命名管道。初始化阶段先把管道建起来,OnInit() 里用 AccountInfoInteger(ACCOUNT_LOGIN) 生成的登录账号作管道名,循环等待管道可用,最多靠全局变量 gvar1 介入打断重试。 每次行情推演触发 OnCalculate(),代码先自增计数器 ctx,再用 SymbolInfoTick() 抓取当前品种的 MqlTick 结构,紧接着 pipe.WriteTick() 把这笔报价写进管道。日志里会打印类似「1 即时价通过 SendTickPipeClick 发送至服务器.」这样的行,你在 MT5 终端专家标签能直接数出每秒推送了几笔。 这套机制没有中间文件、不走网络端口,延迟通常低于一个 tick 间隔;但外汇和贵金属杠杆高、滑点随机,管道另一端若处理不过来可能丢 Tick,实盘前务必在模拟账户压测。 别把重试逻辑当摆设 OnInit 里的 while(!pipe.Open(...)) 如果对面程序没启动,指标会卡在打印「管道未创建, 5 秒内重试...」的循环里。真要自动化跑,记得先开接收端,或者靠 GlobalVariableCheck("gvar1") 主动 break,否则图表加载就假死。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| SendTickPipeIndicator.mq5 | class=class="str">"cmt">//| Copyright class="num">2010, Investeo.pl | class=class="str">"cmt">//| http:/Investeo.pl | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2010, Investeo.pl" class="macro">#class="kw">property link "http:/Investeo.pl" class="macro">#class="kw">property version "class="num">1.00" class="macro">#class="kw">property indicator_chart_window class="macro">#include <CNamedPipes.mqh> CNamedPipe pipe; class="type">int ctx; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 自定义指标初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { while (!pipe.Open(AccountInfoInteger(ACCOUNT_LOGIN))) { Print("管道未创建, class="num">5 秒内重试..."); if (GlobalVariableCheck("gvar1")==true) break; } ctx = class="num">0; class="kw">return(class="num">0); } 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& tick_volume[], const class="type">long& volume[], const class="type">int& spread[]) { ctx++; class="type">MqlTick outgoing; SymbolInfoTick(Symbol(), outgoing); pipe.WriteTick(outgoing); Print(IntegerToString(ctx)+" 即时价通过 SendTickPipeClick 发送至服务器."); class="kw">return(rates_total); } class=class="str">"cmt">//+------------------------------------------------------------------
◍ 多源报价在单客户端里的管道分发
想在单个 MT5 客户端里同时看多个提供方的价格变动,靠的是命名管道把实时报价推给各自的变动指标。服务端在收到 tick 后调用 EventChartCustom(),指标侧在 OnChartEvent() 里接住自定义事件再把变动画出来。 报价以分号分隔的字符串传递,例如 '1.20223;1.20225' 这种 Bid;Ask 格式。指标只在事件 id 等于 CHARTEVENT_CUSTOM+6666 且 dparam 等于管道编号时才处理,避免把别的自定义事件误当报价。 实测布局里可挂三个变动指标:两个走管道收外部源,第三个不接管道、仅做原生 tick 对照,用来察觉是否有报价在转发环节丢失。外汇与贵金属报价转发涉及高杠杆与断线风险,管道断开时服务端会打印 '客户端从管道断开' 并 break 重连。 下面这段脚本是管道服务端核心:建管道、等连接、循环读 tick 并回显计数。把 account 输入改成你的账号号,就能在 MT5 里跑起来验证转发是否通。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| NamedPipeServerBroadcaster.mq5 | class=class="str">"cmt">//| Copyright class="num">2010, Investeo.pl | class=class="str">"cmt">//| http:/Investeo.pl | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2010, Investeo.pl" class="macro">#class="kw">property link "http:/Investeo.pl" class="macro">#class="kw">property version "class="num">1.00" class="macro">#class="kw">property script_show_inputs class="macro">#include <CNamedPipes.mqh> input class="type">int account = class="num">0; CNamedPipe pipe; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 交易程序初始函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class="type">bool tickReceived; class="type">int i=class="num">0; if(pipe.Create(account)==true) while(GlobalVariableCheck("gvar0")==class="kw">false) { if(pipe.Connect()==true) Print("管道已连接"); i=class="num">0; while(true) { do { tickReceived=pipe.ReadTick(); if(tickReceived==class="kw">false) { if(kernel32::GetLastError()==ERROR_BROKEN_PIPE) { Print("客户端从管道断开 "+pipe.GetPipeName()); pipe.Disconnect(); break; } } else { i++; Print(IntegerToString(i)+" 服务器收到即时价."); class="type">class="kw">string bidask=DoubleToString(pipe.incoming.bid)+";"+DoubleToString(pipe.incoming.ask);