「用 WinInet 在 MT5 终端间传数据」
「用 WinInet 在 MT5 终端间传数据」
MT5 借助 wininet.dll 可以直接发起 HTTP 请求,把终端变成能互发数据的节点。比起单纯拉一个网页,更有用的是让服务器端 PHP 接收并暂存数据,再转发给别的终端——这样多个 MT5 之间就能双向交换报价或消息。 核心只依赖 7 个 WinInet 函数:InternetAttemptConnect 探连、InternetOpenW 初始化库、InternetConnectW 开连接、HttpOpenRequestW 建请求描述符、HttpSendRequest 发请求、InternetReadFile 读回数据、InternetCloseHandle 释放句柄。会话和连接描述符在 OnInit 里建一次即可,千万别在 OnTimer 里反复初始化,否则频繁建结构体只会拖慢终端。 一个可验证的现象:MQL5 代码库里 ErrorDescription.mqh 的真实路径是 [MQL5官方文档] p.mql5.com、请求 /data/18/79/5/ErrorDescription.mqh、文件名 ErrorDescription.mqh——ParseURL 就是按这种线性拆分干的。 实盘外汇/贵金属走这套机制有高风险:很多站点防爬,机器批量下载若触发封 IP 就断服务;跨经纪商报价也只是「可能」反映真实价差,需自行过滤模拟服务器噪声。
class="macro">#class="kw">import "wininet.dll" class="type">int InternetAttemptConnect(class="type">int x); class="type">int InternetOpenW(class="type">class="kw">string &sAgent,class="type">int lAccessType,class="type">class="kw">string &sProxyName,class="type">class="kw">string &sProxyBypass,class="type">int lFlags); class="type">int InternetConnectW(class="type">int hInternet,class="type">class="kw">string &szServerName,class="type">int nServerPort,class="type">class="kw">string &lpszUsername,class="type">class="kw">string &lpszPassword,class="type">int dwService,class="type">int dwFlags,class="type">int dwContext); class="type">int HttpOpenRequestW(class="type">int hConnect,class="type">class="kw">string &Verb,class="type">class="kw">string &ObjectName,class="type">class="kw">string &Version,class="type">class="kw">string &Referer,class="type">class="kw">string &AcceptTypes,class="type">uint dwFlags,class="type">int dwContext);
◍ 在 MT5 里用 WinINet 拉起 HTTP 会话
想在 MT5 里直接抓外部行情或接口数据,靠的是对系统 wininet.dll 的封装调用。上面这组函数声明把 HttpSendRequestW、HttpQueryInfoW、InternetReadFile、InternetCloseHandle 全部 import 进来,相当于把 Windows 原生的 HTTP 客户端能力接到了 MQL5 侧。 常量定义里几个十六进制值值得记一下:FLAG_KEEP_CONNECTION 是 0x00400000,FLAG_PRAGMA_NOCACHE 是 0x00000100,FLAG_RELOAD 是 0x80000000。它们直接控制连接是否保活、是否避开本地缓存,写错一位都可能拿到旧数据。 MqlNet::Open 这段是建立会话的主入口。它先判空主机名,再用 TerminalInfoInteger(TERMINAL_DLLS_ALLOWED) 确认终端放行了 DLL——很多自己写的 EA 在默认设置下跑不起来,就是卡在这一关。 随后 InternetAttemptConnect(0) 探一下网络通不通,过了才用 InternetOpenW 开 Session,再用 InternetConnectW 连到指定 Host 和 Port(SERVICE_HTTP 写死为 3)。任一环节返回描述符 ≤0 就打印错误并 Close 收尾。开 MT5 后把这段代码塞进类里,改个 Host 就能验证你的 broker 环境是否允许出网。
class="type">int HttpSendRequestW(class="type">int hRequest,class="type">class="kw">string &lpszHeaders,class="type">int dwHeadersLength,class="type">uchar &lpOptional[],class="type">int dwOptionalLength); class="type">int HttpQueryInfoW(class="type">int hRequest,class="type">int dwInfoLevel,class="type">int &lpvBuffer[],class="type">int &lpdwBufferLength,class="type">int &lpdwIndex); class="type">int InternetReadFile(class="type">int hFile,class="type">uchar &sBuffer[],class="type">int lNumBytesToRead,class="type">int &lNumberOfBytesRead); class="type">int InternetCloseHandle(class="type">int hInet); class="macro">#class="kw">import class=class="str">"cmt">//为了清晰起见,我们将使用wininet.h中的常量名 class="macro">#define OPEN_TYPE_PRECONFIG class="num">0 class=class="str">"cmt">// 使用默认配置 class="macro">#define FLAG_KEEP_CONNECTION 0x00400000 class=class="str">"cmt">// 保持连接 class="macro">#define FLAG_PRAGMA_NOCACHE 0x00000100 class=class="str">"cmt">// 页面不缓存 class="macro">#define FLAG_RELOAD 0x80000000 class=class="str">"cmt">// 当连接时从服务器接收页面 class="macro">#define SERVICE_HTTP class="num">3 class=class="str">"cmt">// 所需的协议 class="type">class="kw">string Host; class=class="str">"cmt">// 主机名 class="type">int Port; class=class="str">"cmt">// 端口 class="type">int Session; class=class="str">"cmt">// 会话描述符 class="type">int Connect; class=class="str">"cmt">// 连接描述符 class="type">bool MqlNet::Open(class="type">class="kw">string aHost,class="type">int aPort) { if(aHost=="") { Print("-Host is not specified"); class="kw">return(false); } class=class="str">"cmt">// 检查终端是否允许使用DLL if(!TerminalInfoInteger(TERMINAL_DLLS_ALLOWED)) { Print("-DLL is not allowed"); class="kw">return(false); } class=class="str">"cmt">// 如果会话已经存在,关闭 if(Session>class="num">0 || Connect>class="num">0) Close(); class=class="str">"cmt">// 记录尝试打开的日志 Print("+Open Inet..."); class=class="str">"cmt">// 如果我们没能检查到网络连接的存在,那么退出 if(InternetAttemptConnect(class="num">0)!=class="num">0) { Print("-Err AttemptConnect"); class="kw">return(false); } class="type">class="kw">string UserAgent="Mozilla"; class="type">class="kw">string nill=""; class=class="str">"cmt">// 打开一个会话 Session=InternetOpenW(UserAgent,OPEN_TYPE_PRECONFIG,nill,nill,class="num">0); class=class="str">"cmt">// 如果我们没能打开会话,则退出 if(Session<=class="num">0) { Print("-Err create Session"); Close(); class="kw">return(false); } Connect=InternetConnectW(Session,aHost,aPort,nill,nill,SERVICE_HTTP,class="num">0,class="num">0); if(Connect<=class="num">0) { Print("-Err create Connect"); Close(); class="kw">return(false); } Host=aHost; Port=aPort; class=class="str">"cmt">// 否则所有尝试都成功了 class="kw">return(true); }
用 MQL5 直接发 HTTP 请求抓数据
在 MT5 里做价格行为分析时,常需要把外部信号(比如 AIGC 生成的支撑阻力评分)拉进 EA。下面这段封装好的网络请求方法,能让你在 MQL5 里直接走 HTTP/1.1 协议取页面或写文件,不用借助 DLL。
| Request() 先判断会话句柄是否还活着,Session 或 Connect 小于等于 0 就自动 Close() 再 Open(Host,Port) 重连;创建请求时写死用 FLAG_KEEP_CONNECTION | FLAG_RELOAD | FLAG_PRAGMA_NOCACHE,等于强制服务端别拿缓存糊弄你。 |
|---|
发送体头部固定为 Content-Type: application/x-www-form-urlencoded,数据来源分两条路:fromFile 为真就从文件读进 uchar 数组,否则把字符串转字符数组。注意 HttpSendRequestW 传的是 ArraySize(data)-1,把结尾的 \0 截掉,不然服务端可能收脏数据。 ReadPage() 用 100 字节的小缓冲循环 InternetReadFile,一段段拼成字符串;toFile 为真就二进制模式写盘。外汇和贵金属行情接口调用存在断连、超时风险,实盘前务必在策略测试器用模拟 host 跑通再上。
class="type">void MqlNet::CloseInet() { Print("-Close Inet..."); if(Session>class="num">0) InternetCloseHandle(Session); Session=-class="num">1; if(Connect>class="num">0) InternetCloseHandle(Connect); Connect=-class="num">1; } class="type">bool MqlNet::Request(class="type">class="kw">string Verb,class="type">class="kw">string Object,class="type">class="kw">string &Out,class="type">bool toFile=false,class="type">class="kw">string addData="",class="type">bool fromFile=false) { if(toFile && Out=="") { Print("-File is not specified "); class="kw">return(false); } class="type">uchar data[]; class="type">int hRequest,hSend,h; class="type">class="kw">string Vers="HTTP/class="num">1.1"; class="type">class="kw">string nill=""; if(fromFile) { if(FileToArray(addData,data)<class="num">0) { Print("-Err reading file "+addData); class="kw">return(false); } } class=class="str">"cmt">// 读取数组中的文件内容 else StringToCharArray(addData,data); if(Session<=class="num">0 || Connect<=class="num">0) { Close(); if(!Open(Host,Port)) { Print("-Err Connect"); Close(); class="kw">return(false); } } class=class="str">"cmt">// 创建一个请求描述符 hRequest=HttpOpenRequestW(Connect,Verb,Object,Vers,nill,nill,FLAG_KEEP_CONNECTION|FLAG_RELOAD|FLAG_PRAGMA_NOCACHE,class="num">0); if(hRequest<=class="num">0) { Print("-Err OpenRequest"); InternetCloseHandle(Connect); class="kw">return(false); } class=class="str">"cmt">// 发送请求 class=class="str">"cmt">// 请求的标题 class="type">class="kw">string head="Content-Type: application/x-www-form-urlencoded"; class=class="str">"cmt">// 发送文件 hSend=HttpSendRequestW(hRequest,head,StringLen(head),data,ArraySize(data)-class="num">1); if(hSend<=class="num">0) { Print("-Err SendRequest"); InternetCloseHandle(hRequest); Close(); } class=class="str">"cmt">// 读取页面 ReadPage(hRequest,Out,toFile); class=class="str">"cmt">// 关闭所有句柄 InternetCloseHandle(hRequest); InternetCloseHandle(hSend); class="kw">return(true); } class="type">void MqlNet::ReadPage(class="type">int hRequest,class="type">class="kw">string &Out,class="type">bool toFile) { class=class="str">"cmt">// 读取页面 class="type">uchar ch[class="num">100]; class="type">class="kw">string toStr=""; class="type">int dwBytes,h; while(InternetReadFile(hRequest,ch,class="num">100,dwBytes)) { if(dwBytes<=class="num">0) break; toStr=toStr+CharArrayToString(ch,class="num">0,dwBytes); } if(toFile) { h=FileOpen(Out,FILE_BIN|FILE_WRITE); FileWriteString(h,toStr); FileClose(h); }
「用类封装 HTTP 会话与 URL 拆解」
在 MT5 里做网页抓取或外联 API,先把网络会话包成一个类最省事。下面这段 MqlNet 类定义了主机、端口、会话与连接四个私有成员,对外只暴露 Open、Close、Request、OpenURL 等几个方法,调用方不必关心 socket 细节。 ParseURL 函数负责把一个完整 http 链接拆成 host、request 和 filename 三段。它先跳过前 7 个字符(即 'http://'),再用 StringFind 找第一个斜杠切出主机与请求路径;随后从字符串末尾反向扫描拿到末级文件名。注意它假设链接一定带 http:// 前缀,否则 StringSubstr(URL,7) 会直接截掉正常字符。 类里还 import 了 Kernel32.dll 的 MoveFileExW,用来在下载完成后做文件重命名或移动。外汇与贵金属行情接口若走这套抓取逻辑,需意识到网络延迟与连接失败属高风险,回测外数据可能随时中断。
else Out=toStr; } class MqlNet { class="type">class="kw">string Host; class=class="str">"cmt">// 主机名 class="type">int Port; class=class="str">"cmt">// 端口 class="type">int Session; class=class="str">"cmt">// 会话描述符 class="type">int Connect; class=class="str">"cmt">// 连接描述符 class="kw">public: MqlNet(); class=class="str">"cmt">// 类的构造函数 ~MqlNet(); class=class="str">"cmt">// 析构函数 class="type">bool Open(class="type">class="kw">string aHost,class="type">int aPort); class=class="str">"cmt">// 创建一个会话并打开连接 class="type">void Close(); class=class="str">"cmt">// 关闭会话和连接 class="type">bool Request(class="type">class="kw">string Verb,class="type">class="kw">string Request,class="type">class="kw">string &Out,class="type">bool toFile=false,class="type">class="kw">string addData="",class="type">bool fromFile=false); class=class="str">"cmt">// 发送请求 class="type">bool OpenURL(class="type">class="kw">string URL,class="type">class="kw">string &Out,class="type">bool toFile); class=class="str">"cmt">// 读取页面到文件或变量中 class="type">void ReadPage(class="type">int hRequest,class="type">class="kw">string &Out,class="type">bool toFile); class=class="str">"cmt">// 读取页面 class="type">int FileToArray(class="type">class="kw">string FileName,class="type">uchar &data[]); class=class="str">"cmt">// 将文件复制到数组中用于发送 }; class="macro">#class="kw">import "Kernel32.dll" class="type">bool MoveFileExW(class="type">class="kw">string &lpExistingFileName, class="type">class="kw">string &lpNewFileName, class="type">int dwFlags); class="macro">#class="kw">import "Kernel32.dll" class="type">void ParseURL(class="type">class="kw">string path,class="type">class="kw">string &host,class="type">class="kw">string &request,class="type">class="kw">string &filename) { host=StringSubstr(URL,class="num">7); class=class="str">"cmt">// 移除 class="type">int i=StringFind(host,"/"); request=StringSubstr(host,i); host=StringSubstr(host,class="num">0,i); class="type">class="kw">string file=""; for(i=StringLen(URL)-class="num">1; i>=class="num">0; i--) if(StringSubstr(URL,i,class="num">1)=="/") { file=StringSubstr(URL,i+class="num">1); break; } if(file!="") filename=file; }
◍ 用脚本从网络拉取并归位 MQ5 文件
MT5 脚本可以直接从指定 URL 把 .mq5 文件抓回终端,并按目录类型自动挪到 Experts、Indicators、Scripts 等位置。核心依赖是自定义的 InternetLib.mqh 里的 MqlNet 类,以及 Windows 的 Kernel32.dll 导出的 MoveFileExW。 代码里先通过 ParseURL 拆出主机名和请求路径,默认用 80 端口开会话;若 Open(Host,80) 失败直接退出,成功才打印「+Copy」日志。下载落盘文件名被硬编码为 Recieve_ 加 TimeToString(TimeCurrent()) 再加 .mq5,也就是说同一秒多次运行会覆盖同名文件,这点调试时要注意。 FolderType 枚举把目标目录映射成 0~7 的数字,Files 类型(值 5)是个特例:下载完不移动,直接留在 MQL5\Files 下返回。其余类型会拼出 from(Files 目录源路径)和 to(对应 MQL5 子目录或 Templates/TesterSet 根路径),调用 MoveFileExW 并带 MOVEFILE_REPLACE_EXISTING 标记强制替换。 外汇与贵金属交易终端跑这类自动下载脚本存在较高安全风险,远程 URL 内容不可控,可能引入恶意代码;实盘环境使用前建议在离线或模拟账户终端验证。
class="macro">#class="kw">property copyright "www.fxmaster.de © class="num">2010" class="macro">#class="kw">property link "www.fxmaster.de" class="macro">#class="kw">property version "class="num">1.00" class="macro">#class="kw">property description "Download files from internet" class="macro">#class="kw">property script_show_inputs class="macro">#include <InternetLib.mqh> class="macro">#class="kw">import "Kernel32.dll" class="type">bool MoveFileExW(class="type">class="kw">string &lpExistingFileName,class="type">class="kw">string &lpNewFileName,class="type">int dwFlags); class="macro">#class="kw">import class="macro">#define MOVEFILE_REPLACE_EXISTING 0x1 enum _FolderType { Experts=class="num">0, Indicators=class="num">1, Scripts=class="num">2, Include=class="num">3, Libraries=class="num">4, Files=class="num">5, Templates=class="num">6, TesterSet=class="num">7 }; input class="type">class="kw">string URL=""; input _FolderType FolderType=class="num">0; class=class="str">"cmt">//------------------------------------------------------------------ OnStart class="type">int OnStart() { MqlNet INet; class=class="str">"cmt">// 在因特网中执行操作的变量 class="type">class="kw">string Host,Request,FileName="Recieve_"+TimeToString(TimeCurrent())+".mq5"; class=class="str">"cmt">// 解析url ParseURL(URL,Host,Request,FileName); class=class="str">"cmt">// 打开会话 if(!INet.Open(Host,class="num">80)) class="kw">return(class="num">0); Print("+Copy "+FileName+" from http:class=class="str">"cmt">//"+Host+" to "+GetFolder(FolderType)); class=class="str">"cmt">// 获取文件 if(!INet.Request("GET",Request,FileName,true)) { Print("-Err download "+URL); class="kw">return(class="num">0); } Print("+Ok download "+FileName); class=class="str">"cmt">// 移动到目标文件夹 class="type">class="kw">string to,from,dir; class=class="str">"cmt">// 如果没有必要移动到其他地方 if(FolderType==Files) class="kw">return(class="num">0); class=class="str">"cmt">// 来自 from=TerminalInfoString(TERMINAL_DATA_PATH)+"\\MQL5\\Files\\"+FileName; class=class="str">"cmt">// 去到 to=TerminalInfoString(TERMINAL_DATA_PATH)+"\\"; if(FolderType!=Templates && FolderType!=TesterSet) to+="MQL5\\"; to+=GetFolder(FolderType)+"\\"+FileName; class=class="str">"cmt">// 移动文件 if(!MoveFileExW(from,to,MOVEFILE_REPLACE_EXISTING)) { Print("-Err move to "+to); class="kw">return(class="num">0); } Print("+Ok move "+FileName+" to "+GetFolder(FolderType)); class="kw">return(class="num">0); } class=class="str">"cmt">//------------------------------------------------------------------ GetFolder class="type">class="kw">string GetFolder(_FolderType foldertype) { if(foldertype==Experts) class="kw">return("Experts"); if(foldertype==Indicators) class="kw">return("Indicators"); if(foldertype==Scripts) class="kw">return("Scripts"); if(foldertype==Include) class="kw">return("Include"); if(foldertype==Libraries) class="kw">return("Libraries");
从 URL 里抠出主机名和文件名
做 MT5 下的网页拉取类 EA 时,经常要把一个完整 URL 拆成 host、请求路径和落地文件名三块。上面这段 ParseURL 就是干这个活的:它先从 URL 第 7 个字符起截掉「http://」前缀,再用首个斜杠把主机和请求串切开。 后面那段反向遍历很有用——从字符串末尾往前找第一个「/」,它后面的部分就是远端文件名。像 www.fxmaster.de/metaarbitr.php?server=Metaquotes&pair=EURUSD&bid=1.4512&time=13286794 这种地址,能稳定解析出 host=f xmaster.de、request=/metaarbitr.php?...、filename=metaarbitr.php。 注意原代码里 host 赋值写的是 StringSubstr(URL,7),若你实际传的是带 https 的链接,偏移量得改成 8,否则会把「s」带进主机名。外汇与贵金属接口调用涉及实时报价,网络异常和 broker 限制都是高风险点,解析逻辑先在脚本里跑通再进 EA。
if(foldertype==Files) class="kw">return("Files"); if(foldertype==Templates) class="kw">return("Profiles\\Templates"); if(foldertype==TesterSet) class="kw">return("Tester"); class="kw">return(""); } class=class="str">"cmt">//------------------------------------------------------------------ ParseURL class="type">void ParseURL(class="type">class="kw">string path,class="type">class="kw">string &host,class="type">class="kw">string &request,class="type">class="kw">string &filename) { host=StringSubstr(URL,class="num">7); class=class="str">"cmt">// 移除 class="type">int i=StringFind(host,"/"); request=StringSubstr(host,i); host=StringSubstr(host,class="num">0,i); class="type">class="kw">string file=""; for(i=StringLen(URL)-class="num">1; i>=class="num">0; i--) if(StringSubstr(URL,i,class="num">1)=="/") { file=StringSubstr(URL,i+class="num">1); break; } if(file!="") filename=file; } class=class="str">"cmt">//+------------------------------------------------------------------+ www.fxmaster.de/metaarbitr.php?server=Metaquotes&pair=EURUSD&bid=class="num">1.4512&time=class="num">13286794
「把工具请下神坛」
GET 请求在 MT5 里已经能跑通网络收发,但别把它当万能钥匙。直接把新闻喂给 EA、远程控单、自动更新指标、跨终端复制信号——这些方向都只是潜在扩展点,实际落地前要自己压测。 本文示例全程用 GET 拿文件或发少量参数,够用在多数轻量场景;下一阶段该看 POST,尤其是传文件和服务端双向同步。 有个真实崩溃样本值得记:2021.12.26 01:06:05 在 EURUSD H1 上 metaarbitrage 打日志 '+Open Inet...',3 秒后 wininet.dll 抛出 Access violation read to 0xFFFFFFFFFFFFFFFF,栈顶停在 cmp word [rcx+rax*2+0x2], 0x0。说明网络库句柄未就绪就进循环,外盘贵金属和外汇这种高波动品种更容易撞上,实盘前务必在策略测试器多跑几轮。 网络功能只是 EA 的辅助手脚,不是印钞插件。开 MT5 把 internetlib.mqh 拖进 include,先复现上面那段日志,确认你的环境不会在 Open Inet 后三秒内崩,再谈别的。
class="num">2021.12.class="num">26 class="num">01:class="num">06:class="num">05.840 metaarbitrage(EURUSD,H1) +Open Inet... class="num">2021.12.class="num">26 class="num">01:class="num">06:class="num">08.537 metaarbitrage(EURUSD,H1) Access violation at 0x00007FF93AAE2740 read to 0xFFFFFFFFFFFFFFFF in &class="macro">#x27;wininet.dll&class="macro">#x27; class="num">2021.12.class="num">26 class="num">01:class="num">06:class="num">08.539 metaarbitrage(EURUSD,H1) crash --> 00007FF93AAE2740 66837C410200 cmp word [rcx+rax*class="num">2+0x2], 0x0 class="num">2021.12.class="num">26 class="num">01:class="num">06:class="num">08.539 metaarbitrage(EURUSD,H1) 00007FF93AAE2746 488D4001 lea rax, [rax+0x1] class="num">2021.12.class="num">26 class="num">01:class="num">06:class="num">08.539 metaarbitrage(EURUSD,H1) 00007FF93AAE274A 75F4 jnz 0x7ff93aae2740 class="num">2021.12.class="num">26 class="num">01:class="num">06:class="num">08.539 metaarbitrage(EURUSD,H1) class="num">2021.12.class="num">26 class="num">01:class="num">06:class="num">08.539 metaarbitrage(EURUSD,H1) 00007FF93AAE274C 488B0D8D5D3D00 mov rcx, [rip+0x3d5d8d] class="num">2021.12.class="num">26 class="num">01:class="num">06:class="num">08.539 metaarbitrage(EURUSD,H1) 00007FF93AAE2753 8D044502000000 lea eax, [rax*class="num">2+0x2] class="num">2021.12.class="num">26 class="num">01:class="num">06:class="num">08.539 metaarbitrage(EURUSD,H1) 00007FF93AAE275A 448BC0 mov r8d, eax class="num">2021.12.class="num">26 class="num">01:class="num">06:class="num">08.539 metaarbitrage(EURUSD,H1) 00007FF93AAE275D 33D2 xor edx, edx class="num">2021.12.class="num">26 class="num">01:class="num">06:class="num">08.539 metaarbitrage(EURUSD,H1) class="num">2021.12.class="num">26 class="num">01:class="num">06:class="num">08.539 metaarbitrage(EURUSD,H1) class="num">00: 0x00007FF93AAE2740 class="num">2021.12.class="num">26 class="num">01:class="num">06:class="num">08.539 metaarbitrage(EURUSD,H1) class="num">01: 0x000002C546534171 class="num">2021.12.class="num">26 class="num">01:class="num">06:class="num">08.539 metaarbitrage(EURUSD,H1) class="num">02: 0x0000000000CC0038 class="num">2021.12.class="num">26 class="num">01:class="num">06:class="num">08.539 metaarbitrage(EURUSD,H1) class="num">03: 0x000002C500000000