MetaTrader 中的多机器人:从单图表中启动多个机器人·进阶篇
◍ 多品种虚拟图表模板的输入与数组骨架
做一套能在单一 EA 里驱动多个图表交易的通用骨架,第一步是把品种、手数、周期这些外部参数用字符串喂进来,再靠分隔符拆成动态数组。下面这段输入定义在 MQL4 和 MQL5 里写法一致,直接复用就行。 核心输入里 SymbolsE 用冒号隔开了 7 个主要外汇对,LotsE 和 TimeframesE 必须一一对应;LastBars=10 控制回看柱数,MagicE=156 是首个魔术码,后续实例会顺延。外汇与贵金属杠杆高,实盘前请在策略测试器用可视化跑一遍确认隔离逻辑。 数组拆分靠 ConstructArrays 数冒号,SCount 初始为 1,每遇一个 ':' 加一,从而算出总品种数,再据此给 S[]、L[]、T[] 定大小。这样后面虚拟图表类和虚拟 EA 类就能按索引 j 绑定到自己那张图,互不串单。 别把正态当圣经:模板本身是从更复杂的工程化版本裁剪来的,多余字段(如 DepositForRepurchaseLotE 留 0 即固定手数)可按需删,但删前先确认没被 VirtualEA 的 OptimalLot 引用。
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 清理。外汇与贵金属品种解析后下单或读价均属高风险操作,品种拼错可能指向不存在的标的。
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 拥堵导致的拷贝延迟。
} 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,观察复制耗时与内存占用变化。
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 验一遍再考虑真金。
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 {