使用MetaTrader 5作为MetaTrader 4的信号提供者·进阶篇
🔗

使用MetaTrader 5作为MetaTrader 4的信号提供者·进阶篇

(2/3)· 当经纪商迟迟不开 MT5 实盘,怎样把 5 的仓位活动无损喂给 4 的账户

偏理论 第 2/3 篇
很多人以为 MT5 只读模式只能看不能动,就放弃了用它给 MT4 当信号源的念头。结果要么手动抄单慢半拍,要么靠社区页面五分钟刷一次,最佳进场早溜了。跨终端跟单若卡在采集层,后面全链路都是延迟的。

「用脚本而非EA持续抓信号」

在 MT4 里做信号采集,关键是不能只等新订单触发才去读数据,得靠脚本常驻轮询,而不是写成 EA。这样才能把行情端的原始状态不间断地往外抛。 要和 MT5 端的信号源打通,就得走套接字。MQL4 代码库里有个 WinSock.mqh 包含文件,封装得很薄,几行就能建连。有人吐槽过它不稳,但我实测没掉过链子,轻量需求够用。 下面这段引用就是当时挂的包含头,路径指向公开代码库 9296 号资源,文章与下载页都在同一编号下。 别把正态当圣经 套接口稳定性不在库本身,而在你轮询间隔和重连逻辑。脚本里没写心跳,MT4 掉线时连接悄无声息断掉,信号就漏了。

MQL5 / C++
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 前建议先用模拟账户跑通三步平仓。

MQL5 / C++
<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-&lt;小时数&gt;)</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&lt;<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>)&gt;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 的边界。

MQL5 / C++
         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 出来就能拿去验证了。

MQL5 / C++
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>() &nbsp;&nbsp;{ &nbsp;&nbsp; PrintLogs(); &nbsp;&nbsp;} <span class="keyword">enum</span> ENUM_STARTUP_TYPE &nbsp;&nbsp;{ &nbsp;&nbsp; STARTUP_TYPE_CLEAR,<span class="comment"> // CLEAR - 发送每一个账户中出现的新交易.</span> &nbsp;&nbsp; STARTUP_TYPE_CONTINUE <span class="comment">// CONTINUE - 在现有仓位没有关闭时不发送交易.</span> &nbsp;&nbsp;}; <span class="comment">//--- 输入参数</span> <span class="keyword">input</span> ENUM_STARTUP_TYPE InpStartupType=STARTUP_TYPE_CONTINUE; <span class="comment">// 启动类型</span> <span class="comment">//--- 服务器正在运行批开始数据收集和处理</span> &nbsp;&nbsp; <span class="keyword">while</span>(!<span class="functions">IsStopped</span>()) &nbsp;&nbsp;&nbsp;&nbsp; { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">Print</span>(<span class="string">"客户端:等待交易..."</span>); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">ArrayInitialize</span>(iBuffer,<span class="number">0</span>); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;iRetVal=recv(iSocketHandle,iBuffer,<span class="functions">ArraySize</span>(iBuffer)&lt;&lt;<span class="number">2</span>,<span class="number">0</span>); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(iRetVal&gt;<span class="number">0</span>) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">string</span> sRawData=struct2str(iBuffer,iRetVal&lt;&lt;<span class="number">18</span>); &nbsp;&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。

MQL5 / C++
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;
让小布替你跑这套
这些跨终端信号采集与转发的逻辑,小布盯盘的 AIGC 已内置多终端监控,打开对应品种页即可看到 MT5 到 MT4 的跟单状态,你只管核对风控参数。

常见问题

在禁止交易权限下运行 EA,仍可获取仓位、订单与交易三类数据,其中订单和交易含进场类型等细节,是跨终端跟随的主要来源。
仓位只反映净敞口,不暴露每笔挂单与成交动作;订单和交易能逐笔还原账户行为,避免每秒比对仓位状态的低效。
公开页面轮询约五分钟一次,且集中访问给服务器加压;用 MT5 本地 EA 直连只读账户可把延迟降到事件触发级。
小布内置多终端监控与信号诊断,可呈现跟单链路状态,但实盘信号转发涉及外汇贵金属高风险,具体执行须自行校验经纪商规则。
它区分进场是市价、挂单触发还是对冲了结,帮助接收端 MT4 判别该复制的是新开还是平仓动作,降低误跟概率。