使用MetaTrader 5作为MetaTrader 4的信号提供者·进阶篇
(2/3)· 当经纪商迟迟不开 MT5 实盘,怎样把 5 的仓位活动无损喂给 4 的账户
「用脚本而非EA持续抓信号」
在 MT4 里做信号采集,关键是不能只等新订单触发才去读数据,得靠脚本常驻轮询,而不是写成 EA。这样才能把行情端的原始状态不间断地往外抛。 要和 MT5 端的信号源打通,就得走套接字。MQL4 代码库里有个 WinSock.mqh 包含文件,封装得很薄,几行就能建连。有人吐槽过它不稳,但我实测没掉过链子,轻量需求够用。 下面这段引用就是当时挂的包含头,路径指向公开代码库 9296 号资源,文章与下载页都在同一编号下。 别把正态当圣经 套接口稳定性不在库本身,而在你轮询间隔和重连逻辑。脚本里没写心跳,MT4 掉线时连接悄无声息断掉,信号就漏了。
class="macro">#include <winsock.mqh> class=class="str">"cmt">// 从MQL4主页下载 class=class="str">"cmt">// 下载连接: [MQL5官方文档] class=class="str">"cmt">// 文章: [MQL5官方文档]
信号中转的数据处理与跨平台仓位对齐
服务端本质是一个不挑图表的 EA,启动即拉起监听线程等连接。这版实现不区分客户端是否在线,只要账户发生交易就群发通知;因此删掉 OnTick(),只留 OnTrade() 去翻最近历史找值得推送的成交。 推送文本用 '<' 开头、'>' 收尾包裹每笔交易,这是为了对抗 TCP/IP 一次 recv 可能黏好几笔数据的现实。代码里出现了一个自造枚举 DEAL_ENTRY_OUTALL,等讲到 MT4 端手数拆分时你就明白它为什么必须存在。 OnTimer() 里只干一件事:EventSetTimer(1) 让每秒打印一次服务端状态,每次调用服务器函数库功能后顺手 PrintLogs() 把错误码甩出来。输入参数 StartupType 决定「跟新仓还是连老仓一起跟」——把提供者挂到已有敞口的账户(比如跟锦标赛信号)时,设错会误导客户端把历史单当新单发。 客户端用脚本死循环收套接字数据。接收函数本身是阻塞的,脚本卡住等数据反而省 CPU,但点「删除脚本」第一次往往删不掉,得点第二次等超时。想要干净退出得给 recv 加超时,这里直接沿用了代码库旧例没改。 收到串流先按 '<''>' 切片再校验,然后过一道 GetLotSize() 重算手数。服务端做了资金管理的映射,客户端也要做:用户设最小/最大手数区间,脚本把信号手数线性映射到自己账户,同时兼容 4/5 位报价与 0.1、0.01 两种合约规格。小账户映射后若尾差手数关不干净,可能留零点零几的残仓。 MT5 同品种单仓、MT4 多订单的冲突是绕不开的坑。MT4 端对同品种每笔「入」开新单;遇到「出」走三步:先找手数完全相同的开单平掉,不够就平比所需小的单凑数,再不够才平大单并回开剩余手数。第三步实战里很少触发,纯属防极端的保护逻辑。外汇与贵金属杠杆高,这类跨平台跟单映射在滑点扩大时残仓风险会显著上升,上 MT5 前建议先用模拟账户跑通三步平仓。
<span class="keyword">class="type">int</span> <span class="functions">OnInit</span>() { <span class="keyword">class="type">class="kw">string</span> str=<span class="class="type">class="kw">string">""</span>; <span class="functions">Print</span>(UTF8_to_ASCII(About())); <span class="comment">class=class="str">"cmt">//--- 启动服务器</span> <span class="functions">Print</span>(<span class="class="type">class="kw">string">"启动服务器,端口 "</span>,InpPort,<span class="class="type">class="kw">string">"..."</span>); <span class="keyword">if</span>(!StartListen(InpPort)) { PrintLogs(); <span class="functions">Print</span>(<span class="class="type">class="kw">string">"OnInit() - FAILED"</span>); <span class="keyword">class="kw">return</span> -<span class="number">class="num">1</span>; } <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| OnTrade() - 每次有交易相关活动发生 |</span> <span class="comment">class=class="str">"cmt">//| |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">void</span> <span class="functions">OnTrade</span>() { <span class="comment">class=class="str">"cmt">//--- 查找所有的新交易并报告给所有客户端</span> <span class="comment">class=class="str">"cmt">//--- class="num">24 小时之前.</span> <span class="keyword">class="type">class="kw">datetime</span> dtStart=<span class="functions">TimeCurrent</span>()-<span class="number">class="num">60</span>*<span class="number">class="num">60</span>*<span class="number">class="num">24</span>; <span class="comment">class=class="str">"cmt">//--- class="num">24 小时之后 (加入您生活在 GMT-<小时数>)</span> <span class="keyword">class="type">class="kw">datetime</span> dtEnd=<span class="functions">TimeCurrent</span>()+<span class="number">class="num">60</span>*<span class="number">class="num">60</span>*<span class="number">class="num">24</span>; <span class="comment">class=class="str">"cmt">//--- 选择最后24小时历史.</span> <span class="keyword">if</span>(<span class="functions">HistorySelect</span>(dtStart,dtEnd)) { <span class="comment">class=class="str">"cmt">//--- 遍历所有交易 (从最老到最新).</span> <span class="keyword">for</span>(<span class="keyword">class="type">int</span> i=<span class="number">class="num">0</span>;i<<span class="functions">HistoryDealsTotal</span>();i++) { <span class="comment">class=class="str">"cmt">//--- 获得交易订单.</span> <span class="keyword">class="type">class="kw">ulong</span> ticket=<span class="functions">HistoryDealGetTicket</span>(i); <span class="comment">class=class="str">"cmt">//--- 如果是我们感兴趣的交易.</span> <span class="keyword">if</span>(<span class="functions">HistoryDealGetInteger</span>(ticket,<span class="keyword">DEAL_ENTRY</span>)!=<span class="keyword">DEAL_ENTRY_STATE</span>) { <span class="comment">class=class="str">"cmt">//Print("Entry type ok.");</span> <span class="comment">class=class="str">"cmt">//--- 检查交易是否比先前报告的更新</span> <span class="keyword">if</span>(<span class="functions">HistoryDealGetInteger</span>(ticket,<span class="keyword">DEAL_TIME</span>)>g_dtLastDealTime) { <span class="comment">class=class="str">"cmt">//--- 如果仓位被部分关闭则检查我们是否需要重新开启</span> <span class="keyword">if</span>(<span class="functions">HistoryDealGetInteger</span>(ticket,<span class="keyword">DEAL_ENTRY</span>)==<span class="keyword">DEAL_ENTRY_OUT</span>) {
◍ 按成交方向切换品种推送权限
在本地信号中继逻辑里,每一笔历史成交都要先判定入口类型再决定要不要广播。若检测到 DEAL_ENTRY_INOUT(即反手/双向平仓再开),说明之前被禁用的品种可能重新需要交易,这时调用 vEnableSymbol 把该品种重新拉回可推送名单,否则容易漏掉后续信号。 品种启用状态由 bIsThisSymbolEnabled 实时校验。只有返回 true,才用 sBuildDealString 拼出成交报文并 SendToAll 群发;返回值 cnt<0 代表服务器通道异常,会在日志打「发送新交易失败!」,而 cnt>=0 则把 g_dtLastDealTime 更新为这笔成交的 DEAL_TIME,作为下次扫描游标。 被禁用品种即便产生了成交也不发通知,但同样要刷新 g_dtLastDealTime,避免扫描器卡在旧时间点反复读取。外汇与贵金属杠杆高,这类自动中继若误推禁用品种,可能诱导跟单账户在失控敞口下建仓,实盘前务必在 MT5 策略测试器用历史成交回放验证 cnt 与 g_dtLastDealTime 的边界。
vUpdateEnabledSymbols(); } class=class="str">"cmt">//--- 如果相反仓位开启,我们需要重启被禁用的交易品种. else if(HistoryDealGetInteger(ticket,DEAL_ENTRY)==DEAL_ENTRY_INOUT) { class=class="str">"cmt">//--- 启用指定交易品种 vEnableSymbol(HistoryDealGetString(ticket,DEAL_SYMBOL)); } class=class="str">"cmt">//--- 检查交易品种是否被启用 if(bIsThisSymbolEnabled(HistoryDealGetString(ticket,DEAL_SYMBOL))) { class=class="str">"cmt">//--- 创建交易字符串并发送到所有连接的客户端 class="type">int cnt=SendToAll(sBuildDealString(ticket)); class=class="str">"cmt">//--- 服务器出现技术问题 if(cnt<class="num">0) { Print("发送新交易失败!"); } class=class="str">"cmt">//--- 如果没有发给任何人(cnt==class="num">0) 或者发给了某些人(cnt>class="num">0) else { class=class="str">"cmt">//--- 更新最后成功发送交易时间. g_dtLastDealTime=(class="type">class="kw">datetime)HistoryDealGetInteger(ticket,DEAL_TIME); } } class=class="str">"cmt">//--- 不做通知因为交易品种被禁用. else { class=class="str">"cmt">//--- 更新我们不做通知的最后交易时间. g_dtLastDealTime=(class="type">class="kw">datetime)HistoryDealGetInteger(ticket,DEAL_TIME); } } } } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 这个函数返回交易数据字符串 | class=class="str">"cmt">//| 例子: |
「把成交记录拼成跨端同步串」
做信号中转时,MT5 本地成交必须被序列化成一段固定格式的文本,才能推到远端账户去复现。上面注释里的四行就是实际落地的样例:EURUSD 买开 0.01 手于 1.37294,卖平于 1.37310,卖开于 1.37320,以及一笔买向 INOUT 双边 0.02 手于 1.37294。 sBuildDealString 这个函数干的就是拼串的活。它先判断成交是 IN、OUT 还是 INOUT:碰到 INOUT 不能直接取历史成交量,而是用 PositionSelect 抓当前持仓量,避免把双向手数算重;普通 IN/OUT 则直接读 DEAL_VOLUME。 一个容易踩的坑是 OUT 之后仓位已平。代码里若发现 PositionSelect 失败,就把 entry 改成自定义的 OUTALL,防止远端因手数映射微小差异而残留碎仓。外汇与贵金属杠杆高,这类同步缝隙可能直接放大成真实滑点亏损,实盘前务必在策略测试器跑一遍历史成交回放。 最后 StringConcatenate 把登录号、品种、买卖类型、方向、两位小数手数和对应小数位价格包进 <> 里,Print 出来就能拿去验证了。
class="type">class="kw">string sBuildDealString(class="type">class="kw">ulong ticket) { class="type">class="kw">string deal=""; class="type">class="kw">double volume=class="num">0; class="type">bool bFirstInOut=true; class=class="str">"cmt">//--- 查找交易量. class=class="str">"cmt">//--- 如果是 INOUT 必须只包含&class="macro">#x27;IN&class="macro">#x27;的交易量. if(HistoryDealGetInteger(ticket,DEAL_ENTRY)==DEAL_ENTRY_INOUT) { if(PositionSelect(HistoryDealGetString(ticket,DEAL_SYMBOL))) { volume=PositionGetDouble(POSITION_VOLUME); } else { Print("读取交易量失败!"); } } class=class="str">"cmt">//--- 如果是 &class="macro">#x27;IN&class="macro">#x27; 或者 &class="macro">#x27;OUT&class="macro">#x27; 交易则使用指定交易量.. else { volume=HistoryDealGetDouble(ticket,DEAL_VOLUME); } class=class="str">"cmt">//--- 生成交易字符串(格式例子: "<EURUSD;BUY;IN;class="num">0.01;class="num">1.37294>"). class="type">int iDealEntry=(class="type">int)HistoryDealGetInteger(ticket,DEAL_ENTRY); class=class="str">"cmt">//--- 如果是 OUT 交易, 而且没有开启仓位了. if(iDealEntry==DEAL_ENTRY_OUT && !PositionSelect(HistoryDealGetString(ticket,DEAL_SYMBOL))) { iDealEntry=DEAL_ENTRY_OUTALL; class=class="str">"cmt">// My own predefined value(this value should not colide with EMUN_DEAL_ENTRY values). } StringConcatenate(deal,"<",AccountInfoInteger(ACCOUNT_LOGIN),";", HistoryDealGetString(ticket,DEAL_SYMBOL),";", Type2String((ENUM_DEAL_TYPE)HistoryDealGetInteger(ticket,DEAL_TYPE)),";", Entry2String(iDealEntry),";",DoubleToString(volume,class="num">2),";", DoubleToString(HistoryDealGetDouble(ticket,DEAL_PRICE), (class="type">int)SymbolInfoInteger(HistoryDealGetString(ticket,DEAL_SYMBOL),SYMBOL_DIGITS)),">"); Print("DEAL:",deal); class="kw">return deal; }
正文
<span class="keyword">void</span> <span class="functions">OnTimer</span>() { PrintLogs(); } <span class="keyword">enum</span> ENUM_STARTUP_TYPE { STARTUP_TYPE_CLEAR,<span class="comment"> // CLEAR - 发送每一个账户中出现的新交易.</span> STARTUP_TYPE_CONTINUE <span class="comment">// CONTINUE - 在现有仓位没有关闭时不发送交易.</span> }; <span class="comment">//--- 输入参数</span> <span class="keyword">input</span> ENUM_STARTUP_TYPE InpStartupType=STARTUP_TYPE_CONTINUE; <span class="comment">// 启动类型</span> <span class="comment">//--- 服务器正在运行批开始数据收集和处理</span> <span class="keyword">while</span>(!<span class="functions">IsStopped</span>()) { <span class="functions">Print</span>(<span class="string">"客户端:等待交易..."</span>); <span class="functions">ArrayInitialize</span>(iBuffer,<span class="number">0</span>); iRetVal=recv(iSocketHandle,iBuffer,<span class="functions">ArraySize</span>(iBuffer)<<<span class="number">2</span>,<span class="number">0</span>); <span class="keyword">if</span>(iRetVal><span class="number">0</span>) { <span class="keyword">string</span> sRawData=struct2str(iBuffer,iRetVal<<<span class="number">18</span>); &nbs
◍ 原始成交文本落库前的清洗与手数换算
把信号源吐出的纯文本成交记录接进 EA 时,第一道关是去空格和字段校验。ProcessOrderRaw 里对 account、symbol、type、entry、volume、price 六个字段先 Trim 再各自 Validate,任何一项不通过就 Print 报错并 return false,这样脏数据不会污染本地账户的下单逻辑。 校验通过后做类型转换,其中价格必须用 NormalizeDouble 按品种小数位对齐:MarketInfo(ssymbol,MODE_DIGITS) 取到的位数直接作为归一化精度,避免跨平台报价小数位不一致导致的 reject。 手数映射是这套桥接的核心风险点。外部输入 remote_lots 范围建议 0.01~15.00,本地受限 InpMinLocalLotSize=0.01、InpMaxLocalLotSize=1.00;GetLotSize 用 dLocalLotDifference = InpMaxLocalLotSize - InpMinLocalLotSize 算出本地可下空间,再按比例把远端仓位缩放到本地账户能承受的手数。外汇与贵金属杠杆高,缩放比例设错可能瞬间超仓,上线前务必在 MT5 策略测试器用 15.00 远端手数跑一遍确认本地不超过 1.00。 末尾的 ProcessOrder 调用才是真正触发本地下单,前面所有转换只为喂给它干净的 account/symbol/type/entry/volume/price。
if(j==iDealsReceived-class="num">1) { class=class="str">"cmt">//--- 这可能是下一个交易不完整的开头部分. sLeftOver=arrDeals[j]; } } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 处理收到的原始数据(文本格式) | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool ProcessOrderRaw(class="type">class="kw">string saccount,class="type">class="kw">string ssymbol,class="type">class="kw">string stype,class="type">class="kw">string sentry,class="type">class="kw">string svolume,class="type">class="kw">string sprice) { class=class="str">"cmt">//--- 清理 saccount= Trim(saccount); ssymbol = Trim(ssymbol); stype=Trim(stype); sentry=Trim(sentry); svolume= Trim(svolume); sprice = Trim(sprice); class=class="str">"cmt">//--- 验证 if(!ValidateAccountNumber(saccount)){Print("Invalid account:",saccount);class="kw">return(false);} if(!ValidateSymbol(ssymbol)){Print("Invalid symbol:",ssymbol);class="kw">return(false);} if(!ValidateType(stype)){Print("Invalid type:",stype);class="kw">return(false);} if(!ValidateEntry(sentry)){Print("Invalid entry:",sentry);class="kw">return(false);} if(!ValidateVolume(svolume)){Print("Invalid volume:",svolume);class="kw">return(false);} if(!ValidatePrice(sprice)){Print("Invalid price:",sprice);class="kw">return(false);} class=class="str">"cmt">//--- 转换 class="type">int account=StrToInteger(saccount); class="type">class="kw">string symbol=ssymbol; class="type">int type=String2Type(stype); class="type">int entry=String2Entry(sentry); class="type">class="kw">double volume= GetLotSize(StrToDouble(svolume),symbol); class="type">class="kw">double price = NormalizeDouble(StrToDouble(sprice),(class="type">int)MarketInfo(ssymbol,MODE_DIGITS)); Print("DEAL[",account,"|",symbol,"|",Type2String(type),"|", Entry2String(entry),"|",volume,"|",price,"]"); class=class="str">"cmt">//--- 执行 ProcessOrder(account,symbol,type,entry,volume,price); class="kw">return(true); } class="kw">extern class="type">class="kw">string _1 = "--- LOT MAPPING ---"; class="kw">extern class="type">class="kw">double InpMinLocalLotSize = class="num">0.01; class="kw">extern class="type">class="kw">double InpMaxLocalLotSize = class="num">1.00; class=class="str">"cmt">// 建议比这个数值大 class="kw">extern class="type">class="kw">double InpMinRemoteLotSize = class="num">0.01; class="kw">extern class="type">class="kw">double InpMaxRemoteLotSize = class="num">15.00; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 计算手数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double GetLotSize(class="type">class="kw">string remote_lots, class="type">class="kw">string symbol) { class="type">class="kw">double dRemoteLots = StrToDouble(remote_lots); class="type">class="kw">double dLocalLotDifference = InpMaxLocalLotSize - InpMinLocalLotSize;