创建多币种多系统 EA 交易·进阶篇
EA 启动先把账户和品种摸清
多策略 EA 在 OnInit 里第一件事是拿账户级数值,比如杠杆率。它跟具体品种和策略无关,直接 Leverage=AccountInfo.Leverage() 存下来就行,没必要塞进数组里反复传。 外部参数建议统一倒进数组。像 BBPeriod_A0 这种用 uint 声明是为了堵住用户填负数,但指标函数 iBands 要的是 int,不转类型编译器会警告,所以写成 (int)BBPeriod_A0 再存数组最省事。 品种合法性必须查。下面这段代码遍历策略数组,若 IsTrade_A[i] 为 false 就跳过,否则调 IsSymbolInMarketWatch 确认该品种在“市场报价”里;找不到就 Print 报错并 ExpertRemove 终止 EA。 别让同一品种在多个策略里重复挂。Strategy_A>1 时会跑双重循环比对 Symbol_A[i]==Symbol_A[j],撞车就直接移除 EA。外汇和贵金属波动剧烈,这种初始化冲突若漏检,实盘可能倾向出现重复开仓和保证金占用翻倍。 通用动作放在 for 循环里做:校验输入参数、建指标句柄(例如 iBands 取 PRICE_HIGH)、算批量数据,最后用 CTrade 的 Trade_A 对象设交易参数。每套策略重复同一套流程即可。
class=class="str">"cmt">//--- Get the leverage for the account Leverage=AccountInfo.Leverage(); class=class="str">"cmt">//--- Copy external variables to arrays Symbol_A[class="num">0] =Symbol_A0; IsTrade_A[class="num">0] =IsTrade_A0; Period_A[class="num">0] =Period_A0; BBPeriod_A[class="num">0] =(class="type">int)BBPeriod_A0; BBShift_A[class="num">0] =BBShift_A0; BBDeviation_A[class="num">0]=BBDeviation_A0; class=class="str">"cmt">//--- Check for the symbol in the Market Watch for(class="type">int i=class="num">0; i<Strategy_A; i++) { if(IsTrade_A[i]==class="kw">false) class="kw">continue; if(IsSymbolInMarketWatch(Symbol_A[i])==class="kw">false) { Print(Symbol_A[i]," could not be found on the server!"); ExpertRemove(); } } class=class="str">"cmt">//--- Check whether the symbol is used more than once if(Strategy_A>class="num">1) { for(class="type">int i=class="num">0; i<Strategy_A-class="num">1; i++) { if(IsTrade_A[i]==class="kw">false) class="kw">continue; for(class="type">int j=i+class="num">1; j<Strategy_A; j++) { if(IsTrade_A[j]==class="kw">false) class="kw">continue; if(Symbol_A[i]==Symbol_A[j]) { Print(Symbol_A[i]," is used more than once!"); ExpertRemove(); } } } } class=class="str">"cmt">//--- The IsSymbolInMarketWatch() function class="type">bool IsSymbolInMarketWatch(class="type">class="kw">string f_Symbol) { for(class="type">int s=class="num">0; s<SymbolsTotal(class="kw">false); s++) { if(f_Symbol==SymbolName(s,class="kw">false)) class="kw">return(true); } class="kw">return(class="kw">false); } class=class="str">"cmt">//--- General actions for(class="type">int i=class="num">0; i<Strategy_A; i++) { if(IsTrade_A[i]==class="kw">false) class="kw">continue; class=class="str">"cmt">//--- Check for errors in input parameters class=class="str">"cmt">//... class=class="str">"cmt">//--- Set indicator handles BB_handle_high_A[i]=iBands(Symbol_A[i],Period_A[i],BBPeriod_A[i],BBShift_A[i],BBDeviation_A[i], PRICE_HIGH); if(BB_handle_high_A[i]<class="num">0) {
「多品种EA的句柄失败与交易参数装配」
在把布林带指标挂到多个交易品种上时,若某个品种的高价轨句柄返回 INVALID_HANDLE,说明该品种可能不在当前经纪商可见品种列表里。代码里用 Print 打出 Symbol_A[i] 和 GetLastError(),随后直接 ExpertRemove() 卸载EA,避免后续对空句柄计算仓位。 对每个允许交易的品种,要先把 SymbolInfo 指向具体符号,再取 LotsMin、LotsMax、Point、ContractSize 存进数组。外汇与贵金属杠杆品种的最小手数常是 0.01,最大手数随经纪商从 10 到 1000 不等,Point 在 XAUUSD 常见为 0.01,这些数值开 MT5 品种规格窗口就能核对。 Trade_A 对象初始化时,SetExpertMagicNumber 区分策略,SetDeviationInPoints 给滑点容差,SetTypeFilling 用 ORDER_FILLING_RETURN 适配服务器,SetAsyncMode(true) 走 OrderSendAsync 非阻塞发单。LogLevel(1) 只保留关键日志,类内部其实会自选最优模式。 最后一段双重循环比对 Strategy_A 与 Strategy_B 里启用的符号,若同一品种出现在两套策略就 Print 报警并卸载。多策略共锁一个品种可能在同根账户里自相成交,概率上会增加无故回撤,实盘前应在策略配置里手动排重。
Print("Failed to create a handle for Bollinger Bands based on High prices for ",Symbol_A[i]," . Handle=",INVALID_HANDLE, "\n Error=",GetLastError()); ExpertRemove(); } class=class="str">"cmt">//... class=class="str">"cmt">//--- Calculate data for the Lot class=class="str">"cmt">//--- set the name of the symbol for which the information will be obtained SymbolInfo.Name(Symbol_A[i]); class=class="str">"cmt">//--- minimum and maximum volume size in trading operations MinLot_A[i]=SymbolInfo.LotsMin(); MaxLot_A[i]=SymbolInfo.LotsMax(); class=class="str">"cmt">//--- point value Point_A[i]=SymbolInfo.Point(); class=class="str">"cmt">//--- contract size ContractSize_A[i]=SymbolInfo.ContractSize(); class=class="str">"cmt">//--- Set some additional parameters } class=class="str">"cmt">//--- Set parameters for trading operations class=class="str">"cmt">//--- set the magic number Trade_A.SetExpertMagicNumber(MagicNumber_A); class=class="str">"cmt">//--- set the permissible slippage in points upon deal execution Trade_A.SetDeviationInPoints(Slippage_A); class=class="str">"cmt">//--- order filling mode, use the mode that is allowed by the server Trade_A.SetTypeFilling(ORDER_FILLING_RETURN); class=class="str">"cmt">//--- logging mode, it is advisable not to call this method as the class will set the optimal mode by itself Trade_A.LogLevel(class="num">1); class=class="str">"cmt">//--- the function to be used for trading: true - OrderSendAsync(), class="kw">false - OrderSend(). Trade_A.SetAsyncMode(true); class=class="str">"cmt">//--- Check whether one and the same symbol is used in several strategies for(class="type">int i=class="num">0; i<Strategy_A; i++) { if(IsTrade_A[i]==class="kw">false) class="kw">continue; for(class="type">int j=class="num">0; j<Strategy_B; j++) { if(IsTrade_B[j]==class="kw">false) class="kw">continue; if(Symbol_A[i]==Symbol_B[j]) { Print(Symbol_A[i]," is used in several strategies!"); ExpertRemove(); } } }
◍ 多品种循环里 continue 与 break 的分工
在 OnTimer() 里跑多交易品种扫描时,单品种 EA 常用 return 直接掐掉后续计算;但多品种框架下这么做会连累其他品种,正确做法是当前品种不满足条件就 continue,只跳过这一轮迭代。 想给多策略 EA 加新策略,且新策略内部本身带“终止全部计算”的 for 循环,可以套一层标志位:内层 break 置 IsInterrupt=true,出内层循环后判断若为真就 continue,从而只放弃当前策略的本次迭代而非整个定时器逻辑。 变量替换是移植旧 EA 的关键一步:把 _Symbol 换成 Symbol_A[i]、_Point 换成 Point_A[i],这些品种特有数值在初始化阶段写进数组即可。下面这段框架演示了连接检测、A/B 策略双循环以及内层中断模式。 平仓逻辑里有个可验证细节:当 Bid_price>=布林下轨[0] 或 DealNumber_A[A]==0 时倾向平掉 BUY 仓,CopyBuffer 返回值<=0 则 continue 跳过,避免脏数据下单。外汇与贵金属品种波动剧烈,这类定时器轮询策略仍存在滑点及断连风险。
class="type">void OnTimer() { class=class="str">"cmt">//--- Check if the terminal is connected to the trade server if(TerminalInfoInteger(TERMINAL_CONNECTED)==class="kw">false) class="kw">return; class=class="str">"cmt">//--- Section A: Main loop of the FOR class="kw">operator for strategy A ----------- for(class="type">int A=class="num">0; A<Strategy_A; A++) { class=class="str">"cmt">//--- A.class="num">1: Check whether the symbol is allowed to be traded if(IsTrade_A[A]==class="kw">false) class="kw">continue; class=class="str">"cmt">// terminate the current FOR iteration } class=class="str">"cmt">//--- Section В: Main loop of the FOR class="kw">operator for strategy В ----------- for(class="type">int B=class="num">0; B<Strategy_B; B++) { class=class="str">"cmt">//--- B.class="num">1: Check whether the symbol is allowed to be traded if(IsTrade_B[B]==class="kw">false) class="kw">continue; class=class="str">"cmt">// terminate the current FOR iteration } } class=class="str">"cmt">//--- Section N: Main loop of the FOR class="kw">operator for strategy N ----------- for(class="type">int N=class="num">0; N<Strategy_N; N++) { class=class="str">"cmt">//... class="type">bool IsInterrupt=class="kw">false; for(class="type">int i=class="num">0; i<Number; i++) { if(...) class=class="str">"cmt">// terminate all calculations { IsInterrupt=true; class="kw">break; } } if(IsInterrupt=true) class="kw">continue; class=class="str">"cmt">// terminate the current FOR iteration class=class="str">"cmt">//... } class=class="str">"cmt">//--- A.class="num">3: Lower band of BB calculated based on High prices if(CopyBuffer(BB_handle_high_A[A],LOWER_BAND,BBShift_A[A],class="num">1,BB_lower_band_high)<=class="num">0) class="kw">continue; class=class="str">"cmt">// terminate the current FOR iteration ArraySetAsSeries(BB_lower_band_high,true); class=class="str">"cmt">//--- A.class="num">7.1: Calculate the current Ask and Bid prices SymbolInfo.Name(Symbol_A[A]); SymbolInfo.RefreshRates(); class="type">class="kw">double Ask_price=SymbolInfo.Ask(); class="type">class="kw">double Bid_price=SymbolInfo.Bid(); if(PositionSelect(Symbol_A[A])) { class=class="str">"cmt">//--- A.class="num">7.2: Closing a BUY position if(PositionInfo.PositionType()==POSITION_TYPE_BUY) { if(Bid_price>=BB_lower_band_high[class="num">0] || DealNumber_A[A]==class="num">0) { if(!Trade_A.PositionClose(Symbol_A[A])) { Print("Failed to close the Buy ",Symbol_A[A]," position. Code=",Trade_A.ResultRetcode(), " (",Trade_A.ResultRetcodeDescription(),")"); class="kw">continue; class=class="str">"cmt">// terminate the current FOR iteration } else {
下穿下轨后的买入执行与日志
当 Ask 价格小于等于当前 K 线的布林带下轨(BB_lower_band_low[0])时,策略触发 A.9.1 的买入分支。这里先用 Trade_A.Buy(OrderLot, Symbol_A[A]) 尝试以计算手数在下标为 A 的交易品种上开多,返回布尔值决定后续流向。 若 Buy 返回 false,说明下单被拒绝或执行失败,代码会打印品种名、ResultRetcode 错误码及 ResultRetcodeDescription 文字说明,然后 continue 跳出本次 FOR 迭代,避免对同一品种重复尝试。外汇与贵金属杠杆高,实盘里这类拒单常源于保证金不足或报价过期,需结合终端‘专家’标签页核对。 若 Buy 返回 true,则打印成功日志并同样 continue 结束本轮循环。注意两路分支都靠 continue 终止当前迭代,意味着每个品种每根满足条件 K 线最多只下一单,不会在循环内连续加仓。 下面这段是原文里买卖平仓与开仓日志的拼接片段,可直接贴进 MT5 脚本看输出:
Print("The Buy ",Symbol_A[A]," position closed successfully. Code=",Trade_A.ResultRetcode(), " (",Trade_A.ResultRetcodeDescription(),")"); class="kw">continue; class=class="str">"cmt">// terminate the current FOR iteration } } } class=class="str">"cmt">//... } class=class="str">"cmt">//--- A.class="num">9.1: for a Buy if(Ask_price<=BB_lower_band_low[class="num">0]) { class=class="str">"cmt">//... class=class="str">"cmt">//--- A.class="num">9.1.class="num">3: Execute a deal if(!Trade_A.Buy(OrderLot,Symbol_A[A])) { Print("The Buy ",Symbol_A[A]," has been unsuccessful. Code=",Trade_A.ResultRetcode(), " (",Trade_A.ResultRetcodeDescription(),")"); class="kw">continue; class=class="str">"cmt">// terminate the current FOR iteration } else { Print("The Buy ",Symbol_A[A]," has been successful. Code=",Trade_A.ResultRetcode(), " (",Trade_A.ResultRetcodeDescription(),")"); class="kw">continue; class=class="str">"cmt">// terminate the current FOR iteration } }
「分项跑完再叠总盘」
EA 成型以后,先把策略与品种拆开单独回测,再跑一遍全策略全品种的混合盘,两边对照才有意义。前提是你已经把输入参数调到各自的最优值,否则对比结论会失真。 策略测试器里按图 2 的设定走:单策略单品种各自出一份报告。策略 A 在 EURUSD 上的权益曲线见图表 3,换到 GBPUSD 见图表 4;策略 B 在 AUDUSD 见图表 5,在 EURJPY 见图表 6。 把四个组合并成「所有策略与交易品种」统一跑,得到图表 7 的总结果。外汇与贵金属杠杆高、滑点跳空频繁,回测盈利不代表实盘概率同构,MT5 里用真实点差重跑一遍才作数。
◍ 一点提醒
多币种多系统 EA 的结构并不复杂,把不同品种和策略塞进同一个 mq5 文件里,就能在单一账户里统算整体效益。实测中即便券商只允许挂一个 EA,这种聚合思路也跑得通,附带的 2multi_en.mq5 源码约 30 KB,直接拖进 MT5 就能编译研读。 外汇和贵金属杠杆高、滑点诡异,多系统同跑会放大连带回撤,参数没验之前别直接上真仓。 想改定时器节奏的,看 OnTimer 实际耗时:若平均超 1 秒,OnInit 里的 EventSetTimer(1) 就该调大,否则事件堆积可能拖慢整体。 剩下要做的只是打开策略测试器,挨个品种跑一遍,看哪些组合在样本内不互蚀。
class="type">int OnInit() { class=class="str">"cmt">//--- 设置事件发生频率 EventSetTimer(class="num">1); class=class="str">"cmt">// class="num">1 秒 class=class="str">"cmt">// ... class="kw">return(class="num">0); }