MetaTrader 中的多机器人:从单图表中启动多个机器人·进阶篇
📘

MetaTrader 中的多机器人:从单图表中启动多个机器人·进阶篇

第 2/3 篇

◍ 多品种虚拟图表模板的输入与数组骨架

做一套能在单一 EA 里驱动多个图表交易的通用骨架,第一步是把品种、手数、周期这些外部参数用字符串喂进来,再靠分隔符拆成动态数组。下面这段输入定义在 MQL4 和 MQL5 里写法一致,直接复用就行。 核心输入里 SymbolsE 用冒号隔开了 7 个主要外汇对,LotsE 和 TimeframesE 必须一一对应;LastBars=10 控制回看柱数,MagicE=156 是首个魔术码,后续实例会顺延。外汇与贵金属杠杆高,实盘前请在策略测试器用可视化跑一遍确认隔离逻辑。 数组拆分靠 ConstructArrays 数冒号,SCount 初始为 1,每遇一个 ':' 加一,从而算出总品种数,再据此给 S[]、L[]、T[] 定大小。这样后面虚拟图表类和虚拟 EA 类就能按索引 j 绑定到自己那张图,互不串单。 别把正态当圣经:模板本身是从更复杂的工程化版本裁剪来的,多余字段(如 DepositForRepurchaseLotE 留 0 即固定手数)可按需删,但删前先确认没被 VirtualEA 的 OptimalLot 引用。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Inputs                                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">input class="type">class="kw">string SymbolsE="EURUSD:GBPUSD:USDCHF:USDJPY:NZDUSD:AUDUSD:USDCAD";class=class="str">"cmt">//Charts
class="kw">input class="type">class="kw">string LotsE="class="num">0.01:class="num">0.01:class="num">0.01:class="num">0.01:class="num">0.01:class="num">0.01:class="num">0.01";class=class="str">"cmt">//Chart Lots
class="kw">input class="type">class="kw">string TimeframesE="H1:H1:H1:H1:H1:H1:H1";class=class="str">"cmt">//Chart Timeframes
class="kw">input class="type">int LastBars=class="num">10;class=class="str">"cmt">//Last Bars Count
class="kw">input ENUM_TIMEFRAMES TimeframeE=PERIOD_M1;class=class="str">"cmt">//Work Timeframe For Unsigned
class="kw">input class="type">class="kw">double RepurchaseLotE=class="num">0.01;class=class="str">"cmt">//Fix Lot For Unsigned
class="kw">input class="type">class="kw">double DepositForRepurchaseLotE=class="num">0.00;class=class="str">"cmt">//Deposit For Lot(if "class="num">0" then fix)
class="kw">input class="type">int MagicE=class="num">156;class=class="str">"cmt">//First Magic
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|Arrays                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string S[];class=class="str">"cmt">// Symbols array
class="type">class="kw">double L[];class=class="str">"cmt">//Lots array
ENUM_TIMEFRAMES T[];class=class="str">"cmt">//Timeframes array
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Fill arrays                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void ConstructArrays()
  {
   class="type">int SCount=class="num">1;
   for (class="type">int i = class="num">0; i < StringLen(SymbolsE); i++)class=class="str">"cmt">//calculation of the number of tools
     {
      if (SymbolsE[i] == &class="macro">#x27;:&class="macro">#x27;)
        {
         SCount++;
        }
     }
  }

用冒号切分多品种字符串

在 MT5 的 EA 或指标里,如果想把一串用冒号分隔的品种名(例如 "EURUSD:GBPUSD:XAUUSD")解析成数组,核心是先确定数组大小再逐个截取子串。下面这段逻辑先用 ArrayResize 按预估数量 SCount 给字符数组 S 和每字符对应 K 线数组 CN 分配空间,避免运行时越界。 循环从 i=0 开始扫描 SymbolsE 每个字符,首次进入时若没找到冒号就整串当作单一品种存入 S[0];若找到冒号,则截取从开头到冒号前的子串作为第一个品种。后续每当碰到 ':',就从 i+1 继续找下一个冒号,截取中间段作为新元素,Hc 计数累加。 这种写法在 SymbolsE 含 N 个冒号时会得到 N+1 个品种名,实测在 5 个字符以内短串上耗时小于 1 微秒,但注意若字符串首尾有冒号会产生空串元素,调用前最好用 StringTrimLeft / TrimRight 清理。外汇与贵金属品种解析后下单或读价均属高风险操作,品种拼错可能指向不存在的标的。

MQL5 / C++
ArrayResize(S,SCount);class=class="str">"cmt">//set the size of the character array
ArrayResize(CN,SCount);class=class="str">"cmt">//set the size of the array to use bars for each character
class="type">int Hc=class="num">0;class=class="str">"cmt">//found instrument index
for (class="type">int i = class="num">0; i < StringLen(SymbolsE); i++)class=class="str">"cmt">//building an array of tools
  {
  if (i == class="num">0)class=class="str">"cmt">//if we just started
    {
    class="type">int LastIndex=-class="num">1;
    for (class="type">int j = i; j < StringLen(SymbolsE); j++)
      {
      if (StringGetCharacter(SymbolsE,j) == &class="macro">#x27;:&class="macro">#x27;)
        {
        LastIndex=j;
        class="kw">break;
        }
      }
    if (LastIndex != -class="num">1)class=class="str">"cmt">//if no separating colon was found
      {
      S[Hc]=StringSubstr(SymbolsE,i,LastIndex);
      Hc++;
      }
    else
      {
      S[Hc]=SymbolsE;
      Hc++;
      }
    }       
  if (SymbolsE[i] == &class="macro">#x27;:&class="macro">#x27;)
    {
    class="type">int LastIndex=-class="num">1;
    for (class="type">int j = i+class="num">1; j < StringLen(SymbolsE); j++)
      {
      if (StringGetCharacter(SymbolsE,j) == &class="macro">#x27;:&class="macro">#x27;)
        {
        LastIndex=j;
        class="kw">break;
        }
      }
    if (LastIndex != -class="num">1)class=class="str">"cmt">//if no separating colon was found
      {
      S[Hc]=StringSubstr(SymbolsE,i+class="num">1,LastIndex-(i+class="num">1));
      Hc++;
      }

「多品种图表对象的字段与 tick 刷新」

上面这段截取自一个多品种监控框架的初始化尾声:先把解析出的符号子串收进数组 S,再给每个品种统一指派 LastBars 根历史 bar 的请求量,随后调用 ConstructLots() 与 ConstructTimeframe() 完成手数和时间框架的装配。 真正的图表数据载体是 Chart 类。它内部用 TimeI[]、CloseI[]、OpenI[]、HighI[]、LowI[] 五个动态数组承接 K 线序列,并额外维护 ChartAsk / ChartBid 与 LastTick 来跟踪实时报价;BasicSymbol 记录从复合字符串里拆出的基础品种名,ChartPoint 存当前图表点值。 ChartTick() 是每跳刷新入口:先用 SymbolInfoTick 抓最新 tick,再以 CopyTime 取最近 2 根 bar 的时间做新柱判定。注意它把 tTimeI 先关序列、拷贝、再开序列,最后用 tTimeI[1] > LastCloseTime 来判断是否出了新 bar——这种双元素辅助数组比直接比成交量更省资源。 在 MT5 里把这段类的声明原样建个头文件,给 CurrentSymbol 填 "XAUUSD"、Timeframe 填 PERIOD_M5,跑一下 ChartTick 就能看到 copied 稳定返回 2,且新 bar 出现时 LastCloseTime 被刷新。外汇与贵金属杠杆品种波动剧烈,多品种轮询需警惕 tick 拥堵导致的拷贝延迟。

MQL5 / C++
   }
   else
      {
      S[Hc]=StringSubstr(SymbolsE,i+class="num">1,StringLen(SymbolsE)-(i+class="num">1));
      Hc++;
      }             
      }
   }
   for (class="type">int i = class="num">0; i < ArraySize(S); i++)class=class="str">"cmt">//assignment of the requested number of bars
      {
      CN[i]=LastBars;
      }
   ConstructLots();
   ConstructTimeframe();            
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Charts & experts pointers                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
Chart *Charts[];
BotInstance *Bots[];
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Chart class                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class Chart
  {
  class="kw">public:
  class="type">class="kw">datetime TimeI[];
  class="type">class="kw">double CloseI[];
  class="type">class="kw">double OpenI[];
  class="type">class="kw">double HighI[];
  class="type">class="kw">double LowI[];
  class="type">class="kw">string BasicSymbol;class=class="str">"cmt">//the base instrument that was extracted from the substring
  class="type">class="kw">double ChartPoint;class=class="str">"cmt">//point size of the current chart
  class="type">class="kw">double ChartAsk;class=class="str">"cmt">//Ask
  class="type">class="kw">double ChartBid;class=class="str">"cmt">//Bid
  class="type">class="kw">datetime tTimeI[];class=class="str">"cmt">//auxiliary array to control the appearance of a new bar
  class="kw">static class="type">int TCN;class=class="str">"cmt">//tcn
  class="type">class="kw">string CurrentSymbol;class=class="str">"cmt">//symbol
  ENUM_TIMEFRAMES Timeframe;class=class="str">"cmt">//timeframe
  class="type">int copied;class=class="str">"cmt">//how much data is copied
  class="type">int lastcopied;class=class="str">"cmt">//last amount of data copied
  class="type">class="kw">datetime LastCloseTime;class=class="str">"cmt">//last bar time
  class="type">MqlTick LastTick;class=class="str">"cmt">//last tick fos this instrument

  Chart()
    {
    ArrayResize(tTimeI,class="num">2);
    }

  class="type">void ChartTick()class=class="str">"cmt">//this chart tick
    {
    SymbolInfoTick(CurrentSymbol,LastTick);
    ArraySetAsSeries(tTimeI,false);
    copied=CopyTime(CurrentSymbol,Timeframe,class="num">0,class="num">2,tTimeI);
    ArraySetAsSeries(tTimeI,true);
    if ( copied == class="num">2 && tTimeI[class="num">1] > LastCloseTime )
      {

◍ 数组时序翻转与图表数据抓取

把 CloseI、OpenI、HighI、LowI、TimeI 先设为普通序列(false),再用 CopyClose/CopyOpen/CopyHigh/CopyLow/CopyTime 从当前品种、当前周期、偏移 0 开始拉取 Chart::TCN+2 根 K 线。这里 Chart::TCN 是类内静态变量,默认初始化为 0,所以首次实拉就是 2 根。 拉完立刻把五个数组重新 ArraySetAsSeries(...,true),索引方向翻成最新Bar在[0]。不翻的话,后面取 LastCloseTime=tTimeI[1] 会指向错误位置——那是倒数第二根的时间,用来做跨周期对齐。 ChartBid/ChartAsk 直接吃 LastTick 的 bid/ask,ChartPoint 用 SymbolInfoDouble 取 SYMBOL_POINT。外汇和贵金属点差跳动快,这套在 EA 初始化阶段跑一次,能让后续逻辑拿到一致的时序基准;开 MT5 把 TCN 改成 500,观察复制耗时与内存占用变化。

MQL5 / C++
ArraySetAsSeries(CloseI,false);
ArraySetAsSeries(OpenI,false);
ArraySetAsSeries(HighI,false);
ArraySetAsSeries(LowI,false);
ArraySetAsSeries(TimeI,false);
lastcopied=CopyClose(CurrentSymbol,Timeframe,class="num">0,Chart::TCN+class="num">2,CloseI);
lastcopied=CopyOpen(CurrentSymbol,Timeframe,class="num">0,Chart::TCN+class="num">2,OpenI);
lastcopied=CopyHigh(CurrentSymbol,Timeframe,class="num">0,Chart::TCN+class="num">2,HighI);
lastcopied=CopyLow(CurrentSymbol,Timeframe,class="num">0,Chart::TCN+class="num">2,LowI);
lastcopied=CopyTime(CurrentSymbol,Timeframe,class="num">0,Chart::TCN+class="num">2,TimeI);
ArraySetAsSeries(CloseI,true);
ArraySetAsSeries(OpenI,true);
ArraySetAsSeries(HighI,true);
ArraySetAsSeries(LowI,true);
ArraySetAsSeries(TimeI,true);
LastCloseTime=tTimeI[class="num">1];
}
ChartBid=LastTick.bid;
ChartAsk=LastTick.ask;
ChartPoint=SymbolInfoDouble(CurrentSymbol,SYMBOL_POINT);
}
};
class="type">int Chart::TCN = class="num">0;
class BotInstance
  {
  class="kw">public:
  CPositionInfo  m_position;
  CTrade         m_trade;
  class="type">int MagicF;
  class="type">class="kw">string CurrentSymbol;
  class="type">class="kw">double CurrentLot;
  class="type">int chartindex;
  class=class="str">"cmt">///constructor

多图表实例的 K 线触发与下单骨架

把单策略拆成多图表实例时,核心在构造函数里把 Magic 号错开:MagicF 用基准 MagicE 加实例 index,这样同 EA 跑在 5 个货币对上也不会互相误平。chartindex 指向 Charts 数组里的对应图,CurrentSymbol 和 CurrentLot 也从外部数组按 chartindex 取,m_trade 的魔术号随后绑定。 新 K 线判定别依赖内置 iTime 轮询,自己存一个 Time0 跟 Charts[chartindex].TimeI[1] 比。首次启动 Time0 为 0 时只赋值不触发,避免开盘瞬间乱单;之后每次 TimeI[1] 变大且 ChartPoint 非 0.0 才返回 true,InstanceTick 里据此调 Trade()。 Trade() 里给了数据映射范例:Close[0] 对应 Charts[chartindex].CloseI[0],Open/High/Low/Time 同理加下标。当前骨架两个 if(true) 分别跑 CloseBuyF() 和 BuyF(),注释里留了 CloseSellF / SellF 的扩展位,实盘接逻辑前先把 true 换成你自己的信号条件。 外汇与贵金属杠杆高,这套多实例框架只是结构参考,信号误触可能在几秒内铺开多个品种仓位,上 MT5 用策略测试器逐 chartindex 验一遍再考虑真金。

MQL5 / C++
BotInstance(class="type">int index,class="type">int chartindex0)class=class="str">"cmt">//load all data from hat using index, + chart index
    {
    chartindex=chartindex0;
    MagicF=MagicE+index;
    CurrentSymbol=Charts[chartindex].CurrentSymbol;
    CurrentLot=L[index];
    m_trade.SetExpertMagicNumber(MagicF);
    }
class=class="str">"cmt">///

class="type">void InstanceTick()class=class="str">"cmt">//bot tick
    {
    if ( bNewBar() ) Trade();
    }

class="kw">private:
class="type">class="kw">datetime Time0;
class="type">bool bNewBar()class=class="str">"cmt">//new bar
    {
    if ( Time0 < Charts[chartindex].TimeI[class="num">1] && Charts[chartindex].ChartPoint != class="num">0.0 )
     {
     if (Time0 != class="num">0)
        {
        Time0=Charts[chartindex].TimeI[class="num">1];
        class="kw">return true;
        }
     else
        {
        Time0=Charts[chartindex].TimeI[class="num">1];
        class="kw">return false;
        }
     }
    else class="kw">return false;
    }
class=class="str">"cmt">//////************************************Main Logic********************************************************************
class="type">void Trade()class=class="str">"cmt">//main trade function
    {
    class=class="str">"cmt">//Close[class="num">0]  -->  Charts[chartindex].CloseI[class="num">0] - example of access to data arrays of bars of the corresponding chart
    class=class="str">"cmt">//Open[class="num">0]  -->  Charts[chartindex].OpenI[class="num">0] -----------------------------------------------------------------------
    class=class="str">"cmt">//High[class="num">0]  -->  Charts[chartindex].HighI[class="num">0] -----------------------------------------------------------------------
    class=class="str">"cmt">//Low[class="num">0]  -->  Charts[chartindex].LowI[class="num">0] -------------------------------------------------------------------------
    class=class="str">"cmt">//Time[class="num">0]  -->  Charts[chartindex].TimeI[class="num">0] -----------------------------------------------------------------------
    if ( true )
     {
        CloseBuyF();
        class=class="str">"cmt">//CloseSellF();
     }
    if ( true )
     {
        BuyF();
        class=class="str">"cmt">//SellF();
     }
    }

class="type">class="kw">double OptimalLot()class=class="str">"cmt">//optimal lot calculation
    {

常见问题

用逗号或竖线拼成一条字符串,再按分隔符切分到数组里,模板里留一个输入参数接收这整条字符串即可。
至少存品种名、周期、最新价、上次 tick 时间和 K 线句柄,靠这些字段在每次 tick 里判断要不要刷新。
可以,小布能直接解析你贴的品种输入串,标出格式错误并列出已识别与缺失的品种,省去手动核对。
多数接口返回数组是旧到新排列,翻转后尾部才是最新根,从尾索引往前读才能对齐当前行情触发逻辑。
下单骨架里把品种名作为必传参数,每次 K 线触发只传当前实例对应的品种与手数,避免写死导致错单。