一个使用命名管道在 MetaTrader 5 客户端之间进行通信的无 DLL 解决方案·综合运用
(3/3)·从进程间管道机制到 CNamedPipes 类落地,再到流式报价压测的完整收口
◍ 用自定义事件把多图表的报价推到监听端
这段逻辑干的事很直接:遍历终端里已打开的图表,把账户号和买卖价通过 CHARTEVENT_CUSTOM+6666 的自定义事件广播出去,让别的图表或 EA 能接住即时价做处理。 外层先拿 ChartFirst() 拿到第一张图表的句柄,用 chart<100 的计数器兜底,避免极端情况下死循环。每轮用 EventChartCustom 把 account 和 bidask 推给当前图,再用 ChartNext 切到下一张,遇到句柄返回 0 就 break,说明图表清单已经走完。 监听侧在 OnChartEvent 里判断 id==CHARTEVENT_CUSTOM+6666 且 dparam 等于管道标识时才认作报价事件,其余 id 大于基础自定义事件的归为别的自定义消息,小于等于的当作普通用户交互。 想验证就开 MT5 挂上这段,开三张以上图表,在接收端 Print 出 dparam,能看到账户号随 tick 被逐图推送;外汇和贵金属报价波动快、点差跳变频繁,多图表广播会放大终端负载,实盘前先在模拟盘压一遍。
class="type">long currChart=ChartFirst(); class="type">int chart=class="num">0; while(chart<class="num">100) { EventChartCustom(currChart,class="num">6666,class="num">0,(class="type">class="kw">double)account,bidask); currChart=ChartNext(currChart); if(currChart==class="num">0) break; class=class="str">"cmt">// 到达图表清单结尾 chart++; } if(GlobalVariableCheck("gvar0")==true || (kernel32::GetLastError()==ERROR_BROKEN_PIPE)) break; } } while(tickReceived==true); if(i>class="num">0) { Print(IntegerToString(i)+"即时价收到."); i=class="num">0; }; if(GlobalVariableCheck("gvar0")==true || (kernel32::GetLastError()==ERROR_BROKEN_PIPE)) break; Sleep(class="num">100); } } pipe.Close(); } class="type">void OnChartEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam) { if (dparam==(class="type">class="kw">double)incomingPipe) if(id>CHARTEVENT_CUSTOM) { if(id==CHARTEVENT_CUSTOM+class="num">6666) { class=class="str">"cmt">// 处理进入的即时价 } } else { class=class="str">"cmt">// 处理用户时间 } }
共享内存管道的实际吞吐表现
基于共享内存的命名管道在 MT5 客户端之间搬运报价几乎没有协议开销。我在两台 MT5 终端之间跑了批量压测:发送端用 WriteTick() 逐笔推报价,用 GetTickCount() 卡前后时间戳。
- 次样本里,10 万笔报价发送与接收耗时基本对等,集中在 592–702 ms;100 万笔则在 5610–5930 ms 区间。表内第 6–10 行显示收发时间差最大不到 20 ms,说明瓶颈不在跨进程拷贝。
在一台 Windows Vista、2.0GHz T4200、3GB RAM 的老笔记本上,100 万次报价的平均吞吐约 17 万次价格变动/秒。外汇与贵金属行情高频转发属高风险操作,实盘机器若负载拉满可能拖慢 EA 决策。 下面这段发送端脚本是压测核心:Print 打点后,用 GetTickCount 取起始毫秒,循环 10 万次调用 pipe.WriteTick 把报价写进管道,再取 stop 算耗时并关闭管道。 服务端脚本则监听 ReadTick,首 tick 记录 start,断管时跳出循环并打印总接收耗时,逻辑对称。
Print("发送..."); class="type">uint start = GetTickCount(); for (class="type">int i=class="num">0;i<class="num">100000;i++) pipe.WriteTick(outgoing); class="type">uint stop = GetTickCount(); Print("发送时间" + IntegerToString(stop-start) + " [ms]"); pipe.Close(); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| SpeedTestPipeServer.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; class="type">bool tickReceived; class="type">uint start,stop; CNamedPipe pipe; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 交易程序初始函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class="type">int i=class="num">0; if(pipe.Create(account)==true) if(pipe.Connect()==true) Print("管道已连接"); do { tickReceived=pipe.ReadTick(); if(i==class="num">0) start=GetTickCount(); if(tickReceived==class="kw">false) { if(kernel32::GetLastError()==ERROR_BROKEN_PIPE) { Print("客户端从管道断开 "+pipe.GetPipeName()); pipe.Disconnect(); break; } } else i++; } while(tickReceived==true); stop=GetTickCount(); if(i>class="num">0) { Print(IntegerToString(i)+" ticks received."); i=class="num">0; }; pipe.Close(); Print("服务器: 接收时间 "+IntegerToString(stop-start)+" [ms]"); } class=class="str">"cmt">//+------------------------------------------------------------------
「把这条线请下神坛」
命名管道在 MT5 客户端之间搬实时报价是可行的,CNamedPipes 类附带了 mqh 头文件与若干演示源文件,其中 cnamedpipes.mqh 体积 21.65 KB,sendtickpipeindicator.mq5 仅 3.71 KB,足以在两张图表间透传 tick。 但社区反馈很直白:2023 年 6 月起的多个版本在附加指标时抛出「访问违规」,有用户自行修补了类文件并补了 WriteDouble 方法才勉强跑通。跨终端通信不是银弹,实盘里多账户对冲若依赖这套老管道,要先在模拟环境压一遍。 外汇与贵金属本身高杠杆、高风险,任何进程间传价方案都只是工具链一环,别神化它的稳定性。 下面这段是社区补的写双精度方法,直接看 kernel32 的 WriteFile 怎么被包了一层: #import "kernel32.dll" ... 引入 WriteFile(ulong,double&,int,int&,ulong) 系统调用 ... #import bool CNamedPipe::WriteDouble(double data) { int data_size = sizeof(data);
| if(!data | data_size<1) return(false); |
|---|
int written;
| if(!WriteFile(hPipe,data,data_size,written,NULL) | written!=data_size) |
|---|
return(false); return(true); } 逻辑就一句:尺寸不对或写字节数不匹配就失败,匹配才返回真。开 MT5 把这段塞进你自己的管道类,先测本地回环再谈跨账户。
class="macro">#class="kw">import "kernel32.dll" ... class="type">int WriteFile(class="type">class="kw">ulong fileHandle,class="type">class="kw">double &var,class="type">int bytes,class="type">int &numOfBytes,class="type">class="kw">ulong overlapped); ... class="macro">#class="kw">import ... class="type">bool CNamedPipe::WriteDouble(class="type">class="kw">double data) { class="type">int data_size = class="kw">sizeof(data); class=class="str">"cmt">//--- 检查参数 if(!data || data_size<class="num">1) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 发送数据 class="type">int written; if(!WriteFile(hPipe,data,data_size,written,NULL) || written!=data_size) class="kw">return(class="kw">false); class=class="str">"cmt">//--- ok class="kw">return(true); }