
MQL5 Cookbook: 多币种EA交易 - 简洁而快速的途径
简介
本文将介绍一个针对多币种EA交易的简单而合适的实现。意思是说您可以设置EA交易,针对多币种在同一条件下测试/交易,但是为每个交易品种设置不同的参数。作为例子,我们将创建针对两个交易品种的模式,但是通过这种方法可以还增加另外的交易品种,如有必要只需修改很少的代码。
一个多币种模式可以用多种方法以MQL5实现:
我们可以使用这样的模式:EA交易根据时间,在OnTimer()函数中按照指定的时间间隔做精确的检查。
或者, 也可以像本文章系列之前所有的EA交易那样, 在OnTick()函数中做检查,这种情况下EA依赖于它所运行的当前交易品种的订单事件,所以如果另外一个交易品种上有完成的柱,而当前交易品种上还没有订单时,EA只有在当前交易品种有了订单才能做检查。
还有另外一个有趣的选项,来自它的作者 Konstantin Gruzdev (Lizar). 它使用事件模型: 使用OnChartEvent()函数, EA交易可以从位于交易/测试中的交易品种图表的指标代理中获得重现的事件,指标代理可以重现它们所在图表的新柱事件和订单事件,这类指标 (EventsSpy.mq5) 可以在本文末尾下载。我们在EA交易的操作中将需要它。
EA 交易开发
在文章 "MQL5 Cookbook: 在EA交易中使用指标设置交易条件" 中的EA交易将做为模板,我已经把前一篇文章"MQL5 Cookbook: 基于三重滤网策略开发一个交易系统框架"中处理信息面板部分删除,同时也简化了仓位开启条件部分,因为我们将要创建一个支持两个交易品种的EA交易,它们中的每一个都将需要它们自己的外部参数:
//--- EA交易的外部参数 sinput long MagicNumber = 777; // 幻数 sinput int Deviation = 10; // 滑点 //--- sinput string delimeter_00=""; // -------------------------------- sinput string Symbol_01 = "EURUSD"; // 交易品种 1 input int IndicatorPeriod_01 = 5; // | 指标周期数 input double TakeProfit_01 = 100; // | 获利 input double StopLoss_01 = 50; // | 止损 input double TrailingStop_01 = 10; // | 跟踪止损 input bool Reverse_01 = true; // | 仓位反转 input double Lot_01 = 0.1; // | 手数 input double VolumeIncrease_01 = 0.1; // | 仓位交易量增加量 input double VolumeIncreaseStep_01 = 10; // | 交易量增加步长 //--- sinput string delimeter_01=""; // -------------------------------- sinput string Symbol_02 = "NZDUSD"; // 交易品种 2 input int IndicatorPeriod_02 = 5; // | 指标周期 input double TakeProfit_02 = 100; // | 获利 input double StopLoss_02 = 50; // | 止损 input double TrailingStop_02 = 10; // | 跟踪止损 input bool Reverse_02 = true; // | 仓位反转 input double Lot_02 = 0.1; // | 手数 input double VolumeIncrease_02 = 0.1; // | 仓位交易量增加量 input double VolumeIncreaseStep_02 = 10; // | 交易量增加步长
外部参数将被放置到数组中,数组的大小将依赖于使用的交易品种的数目,EA交易中使用的交易品种的数目由NUMBER_OF_SYMBOLS常量值决定,我们在文件开头创建它:
//--- 交易品种数量 #define NUMBER_OF_SYMBOLS 2 //--- EA交易名称 #define EXPERT_NAME MQL5InfoString(MQL5_PROGRAM_NAME)
让我们创建用于保存外部参数的数组:
//--- 用于保存外部参数的数组 string Symbols[NUMBER_OF_SYMBOLS]; // 交易品种 int IndicatorPeriod[NUMBER_OF_SYMBOLS]; // 指标周期数 double TakeProfit[NUMBER_OF_SYMBOLS]; // 获利 double StopLoss[NUMBER_OF_SYMBOLS]; // 止损 double TrailingStop[NUMBER_OF_SYMBOLS]; // 跟踪止损 bool Reverse[NUMBER_OF_SYMBOLS]; // 仓位反转 double Lot[NUMBER_OF_SYMBOLS]; // 手数 double VolumeIncrease[NUMBER_OF_SYMBOLS]; // 仓位交易量增加量 double VolumeIncreaseStep[NUMBER_OF_SYMBOLS]; // 交易量增加步长
数组初始化函数将放在InitArrays.mqh包含文件中,为了初始化Symbols[]数组,我们将创建GetSymbol()函数,它将从外部参数中获得交易品种名称,如果这种交易品种在服务器的交易品种列表中存在,它将被选择到市场报价窗口中。相反,如果所需的交易品种在服务器上没有找到,这个函数会返回一个空字符串,EA交易的日志中将有对应更新。
以下是GetSymbol()函数的代码:
//+------------------------------------------------------------------+ //| 把指定的交易品种增加到市场报价窗口 | //+------------------------------------------------------------------+ string GetSymbolByName(string symbol) { string symbol_name=""; // 服务器上的交易品种名称 //--- 如果传入空字符串,则返回空字符串 if(symbol=="") return(""); //--- 在服务器上所有交易品种列表上迭代 for(int s=0; s<SymbolsTotal(false); s++) { //--- 取得交易品种名称 symbol_name=SymbolName(s,false); //--- 如果请求的交易品种在服务器上存在 if(symbol==symbol_name) { //--- 在市场报价窗口选择它 SymbolSelect(symbol,true); //--- 返回交易品种名称 return(symbol); } } //--- 如果所需的交易品种没有找到,返回空字符串 Print(symbol+" 交易品种无法在服务器上找到!"); return(""); }
Symbols[]数组将在GetSymbols()函数中被初始化:
//+------------------------------------------------------------------+ //| 填充交易品种数组 | //+------------------------------------------------------------------+ void GetSymbols() { Symbols[0]=GetSymbolByName(Symbol_01); Symbols[1]=GetSymbolByName(Symbol_02); }
另外,我们还将以这种方式实现:如果某种交易品种的外部参数设置为空值,对应的区块将不在测试/交易中涉及,这很必要,因为这样就可以为每个交易品种分开做优化而完全把其他部分抛开了。
所有外部参数的其他数组都使用相同方式初始化,换句话说,我们需要为每个数组创建单独的函数,所有这些函数的代码如下:
//+------------------------------------------------------------------+ //| 填充指标周期数组 | //+------------------------------------------------------------------+ void GetIndicatorPeriod() { IndicatorPeriod[0]=IndicatorPeriod_01; IndicatorPeriod[1]=IndicatorPeriod_02; } //+------------------------------------------------------------------+ //| 填充获利数组 | //+------------------------------------------------------------------+ void GetTakeProfit() { TakeProfit[0]=TakeProfit_01; TakeProfit[1]=TakeProfit_02; } //+------------------------------------------------------------------+ //| 填充止损数组 | //+------------------------------------------------------------------+ void GetStopLoss() { StopLoss[0]=StopLoss_01; StopLoss[1]=StopLoss_02; } //+------------------------------------------------------------------+ //| 填充跟踪止损数组 | //+------------------------------------------------------------------+ void GetTrailingStop() { TrailingStop[0]=TrailingStop_01; TrailingStop[1]=TrailingStop_02; } //+------------------------------------------------------------------+ //| 填充反向数组 | //+------------------------------------------------------------------+ void GetReverse() { Reverse[0]=Reverse_01; Reverse[1]=Reverse_02; } //+------------------------------------------------------------------+ //| 填充手数数组 | //+------------------------------------------------------------------+ void GetLot() { Lot[0]=Lot_01; Lot[1]=Lot_02; } //+------------------------------------------------------------------+ //| 填充VolumeIncrease数组 | //+------------------------------------------------------------------+ void GetVolumeIncrease() { VolumeIncrease[0]=VolumeIncrease_01; VolumeIncrease[1]=VolumeIncrease_02; } //+------------------------------------------------------------------+ //| 填充VolumeIncreaseStep数组 | //+------------------------------------------------------------------+ void GetVolumeIncreaseStep() { VolumeIncreaseStep[0]=VolumeIncreaseStep_01; VolumeIncreaseStep[1]=VolumeIncreaseStep_02; }
现在让我们创建一个函数来帮我们方便地一次初始化所有外部参数数组 - InitializeInputParameters() 函数:
//+------------------------------------------------------------------+ //| 初始化外部参数数组 | //+------------------------------------------------------------------+ void InitializeInputParameters() { GetSymbols(); GetIndicatorPeriod(); GetTakeProfit(); GetStopLoss(); GetTrailingStop(); GetReverse(); GetLot(); GetVolumeIncrease(); GetVolumeIncreaseStep(); }
在初始化外部参数数组之后,我们可以前进到主要部分了。一些过程,比如取得指标句柄,它们的值以及价格信息,检查新柱等,将在循环中按照每个交易品种顺序依次进行。这就是为什么要把外部参数值放到数组中,这样就可以在循环中如下进行了:
//--- 迭代所有交易品种 for(int s=0; s<NUMBER_OF_SYMBOLS; s++) { //--- 如果允许此交易品种交易 if(Symbols[s]!="") { //--- 其他代码 } }
但是在我们开始修改已有函数和创建新函数之前,让我们也创建这种模式所需要的数组,
我们需要为指标句柄创建两个数组:
//--- 指标代理句柄的数组 int spy_indicator_handles[NUMBER_OF_SYMBOLS]; //--- 信号指标句柄的数组 int signal_indicator_handles[NUMBER_OF_SYMBOLS];
这两个数组首先应该使用无效值进行初始化:
//+------------------------------------------------------------------+ //| 初始化指标句柄数组 | //+------------------------------------------------------------------+ void InitializeArrayHandles() { ArrayInitialize(spy_indicator_handles,INVALID_HANDLE); ArrayInitialize(signal_indicator_handles,INVALID_HANDLE); }
价格数据以及指标值的数组将使用结构来访问:
//--- 用于检查交易条件的数据数组 struct PriceData { double value[]; }; PriceData open[NUMBER_OF_SYMBOLS]; // 柱的开盘价 PriceData high[NUMBER_OF_SYMBOLS]; // 柱的最高价 PriceData low[NUMBER_OF_SYMBOLS]; // 柱的最低价 PriceData close[NUMBER_OF_SYMBOLS]; // 柱的收盘价 PriceData indicator[NUMBER_OF_SYMBOLS]; // 指标值数组
现在,如果您需要获取列表中第一个交易品种的最新的已完成柱的指标值,您应该如下书写:
double indicator_value=indicator[0].value[1];
我们也需要创建数组,而不是之前用于CheckNewBar()函数中的变量:
//--- 用于取得当前柱开始时间的数组 struct Datetime { datetime time[]; }; Datetime lastbar_time[NUMBER_OF_SYMBOLS]; //--- 用于检查每个交易品种新柱的数组 datetime new_bar[NUMBER_OF_SYMBOLS];
这样我们就把数组设置好了,现在我们需要根据以上的改动来修改一系列函数,让我们从GetIndicatorHandles()函数开始:
//+------------------------------------------------------------------+ //| 取得指标句柄 | //+------------------------------------------------------------------+ void GetIndicatorHandles() { //--- 迭代所有交易品种 for(int s=0; s<NUMBER_OF_SYMBOLS; s++) { //--- 如果允许交易此交易品种 if(Symbols[s]!="") { //--- 如果还没有获得句柄 if(signal_indicator_handles[s]==INVALID_HANDLE) { //--- 取得指标句柄 signal_indicator_handles[s]=iMA(Symbols[s],_Period,IndicatorPeriod[s],0,MODE_SMA,PRICE_CLOSE); //--- 如果无法获得指标句柄 if(signal_indicator_handles[s]==INVALID_HANDLE) Print("获得指标句柄失败,交易品种为 "+Symbols[s]+"!"); } } } }
现在,不论在测试/交易中使用多少个交易品种,函数代码都不用改变了。
同样地,我们还创建另外一个函数, GetSpyHandles(), 用于获得从其他交易品种传递订单信息的指标代理的句柄。但是在那之前,我们将再为各交易品种的事件增加一个枚举,ENUM_CHART_EVENT_SYMBOL, 该枚举位于Enums.mqh文件中:
//+------------------------------------------------------------------+ //| 所有交易品种和时间范围的新柱和订单事件 | //+------------------------------------------------------------------+ enum ENUM_CHART_EVENT_SYMBOL { CHARTEVENT_NO = 0, // 事件被禁用- 0 CHARTEVENT_INIT = 0, // 初始化事件 - 0 //--- CHARTEVENT_NEWBAR_M1 = 0x00000001, // 分钟图的新柱事件 (1) CHARTEVENT_NEWBAR_M2 = 0x00000002, // 2分钟图的新柱事件 (2) CHARTEVENT_NEWBAR_M3 = 0x00000004, // 3分钟图的新柱事件 (4) CHARTEVENT_NEWBAR_M4 = 0x00000008, // 4分钟图的新柱事件 (8) //--- CHARTEVENT_NEWBAR_M5 = 0x00000010, // 5分钟图的新柱事件 (16) CHARTEVENT_NEWBAR_M6 = 0x00000020, // 6分钟图的新柱事件 (32) CHARTEVENT_NEWBAR_M10 = 0x00000040, // 10分钟图的新柱事件 (64) CHARTEVENT_NEWBAR_M12 = 0x00000080, // 12分钟图的新柱事件 (128) //--- CHARTEVENT_NEWBAR_M15 = 0x00000100, // 15分钟图的新柱事件 (256) CHARTEVENT_NEWBAR_M20 = 0x00000200, // 20分钟图的新柱事件 (512) CHARTEVENT_NEWBAR_M30 = 0x00000400, // 30分钟图的新柱事件 (1024) CHARTEVENT_NEWBAR_H1 = 0x00000800, // 1小时图的新柱事件 (2048) //--- CHARTEVENT_NEWBAR_H2 = 0x00001000, // 2小时图的新柱事件 (4096) CHARTEVENT_NEWBAR_H3 = 0x00002000, // 3小时图的新柱事件 (8192) CHARTEVENT_NEWBAR_H4 = 0x00004000, // 4小时图的新柱事件 (16384) CHARTEVENT_NEWBAR_H6 = 0x00008000, // 6小时图的新柱事件 (32768) //--- CHARTEVENT_NEWBAR_H8 = 0x00010000, // 8小时图的新柱事件 (65536) CHARTEVENT_NEWBAR_H12 = 0x00020000, // 12小时图的新柱事件 (131072) CHARTEVENT_NEWBAR_D1 = 0x00040000, // 日图的新柱事件 (262144) CHARTEVENT_NEWBAR_W1 = 0x00080000, // 周图的新柱事件 (524288) //--- CHARTEVENT_NEWBAR_MN1 = 0x00100000, // 月图的新柱事件 (1048576) CHARTEVENT_TICK = 0x00200000, // 新订单事件 (2097152) //--- CHARTEVENT_ALL = 0xFFFFFFFF // 所有事件都被启用 (-1) };
这个枚举对于在GetSpyHandles()函数使用自定义指标EventsSpy.mq5 (文件在本文附件中)是必需的,函数代码提供如下:
//+------------------------------------------------------------------+ //| 根据指定的交易品种获取代理的句柄 | //+------------------------------------------------------------------+ void GetSpyHandles() { //--- 迭代所有交易品种 for(int s=0; s<NUMBER_OF_SYMBOLS; s++) { //--- 如果允许交易此交易品种 if(Symbols[s]!="") { //--- 如果还没有获得句柄 if(spy_indicator_handles[s]==INVALID_HANDLE) { //--- 取得指标句柄 spy_indicator_handles[s]=iCustom(Symbols[s],_Period,"EventsSpy.ex5",ChartID(),0,CHARTEVENT_TICK); //--- 如果无法获得指标句柄 if(spy_indicator_handles[s]==INVALID_HANDLE) Print("安装代理失败,交易品种为"+Symbols[s]+""); } } } }
请注意iCustom()函数最后的参数: 在本例中, 使用CHARTEVENT_TICK标识符来取得订单事件,但是如有必要,它也可以改为取得新柱事件。比如,如果您使用以下代码,EA交易就可以获得在分钟(M1)范围和小时(H1) 范围的新柱事件:
handle_event_indicator[s]=iCustom(Symbols[s],_Period,"EventsSpy.ex5",ChartID(),0,CHARTEVENT_NEWBAR_M1|CHARTEVENT_NEWBAR_H1);
为了获得所有事件(所有事件范围内的订单和柱事件),您需要指定CHARTEVENT_ALL标识符。
所有数组都在 OnInit() 函数中被初始化:
//+------------------------------------------------------------------+ //| EA初始化函数 | //+------------------------------------------------------------------+ void OnInit() { //--- 初始化外部参数的数组 InitializeInputParameters(); //--- 初始化指标句柄的数组 InitializeArrayHandles(); //--- 取得代理句柄 GetSpyHandles(); //--- 取得指标句柄 GetIndicatorHandles(); //--- 初始化新柱 InitializeArrayNewBar(); }
如我们在文章开头所说,从指标代理收到的事件是在OnChartEvent()函数中收到的,以下是将要用于此函数中的代码:
//+------------------------------------------------------------------+ //| 图表事件处理函数 | //+------------------------------------------------------------------+ void OnChartEvent(const int id, // 事件编号 const long &lparam, // 长整型事件参数 const double &dparam, // 双精度型事件参数 const string &sparam) // 字符串型事件参数 { //--- 如果是自定义事件 if(id>=CHARTEVENT_CUSTOM) { //--- 如果不允许交易则退出 if(CheckTradingPermission()>0) return; //--- 如果是一个订单事件 if(lparam==CHARTEVENT_TICK) { //--- 检查信号并交易 CheckSignalsAndTrade(); return; } } }
在CheckSignalAndTrade()函数中 (以上高亮标记的代码), 我们将有一个循环,在其中所有的交易品种都依次检查新柱事件以及之前在OnTick()函数中所实现的交易信号:
//+------------------------------------------------------------------+ //| 根据新柱事件检查信号和交易 | //+------------------------------------------------------------------+ void CheckSignalsAndTrade() { //--- 迭代所有指定的交易品种 for(int s=0; s<NUMBER_OF_SYMBOLS; s++) { //--- 如果允许交易此交易品种 if(Symbols[s]!="") { //--- 如果不是新的柱,继续下一个交易品种 if(!CheckNewBar(s)) continue; //--- 如果有新柱 else { //--- 取得指标数据。如果没有数据,继续下一个交易品种 if(!GetIndicatorsData(s)) continue; //--- 取得柱数据 GetBarsData(s); //--- 检查条件和交易 TradingBlock(s); //--- 跟踪止损 ModifyTrailingStop(s); } } } }
所有使用外部参数,以及交易品种和指标数据的函数,都需要根据以上改变做修改,为了这个目标,我们需要增加交易品种编号做为第一个参数,并且把函数内的变量和数组都替换为以上所述的新数组。
为了更好说明,在此提供了修改过的CheckNewBar(), TradingBlock() 和OpenPosition() 函数的代码。
CheckNewBar() 函数的代码:
//+------------------------------------------------------------------+ //| 检查新柱 | //+------------------------------------------------------------------+ bool CheckNewBar(int number_symbol) { //--- 读取当前柱的开始时间 // 如果读取时间出错,打印相关信息 if(CopyTime(Symbols[number_symbol],Period(),0,1,lastbar_time[number_symbol].time)==-1) Print(__FUNCTION__,": 复制柱的开始事件出错: "+IntegerToString(GetLastError())); //--- 如果是第一次函数调用 if(new_bar[number_symbol]==NULL) { //--- 设置时间 new_bar[number_symbol]=lastbar_time[number_symbol].time[0]; Print(__FUNCTION__,": Initialization ["+Symbols[number_symbol]+"][TF: "+TimeframeToString(Period())+"][" +TimeToString(lastbar_time[number_symbol].time[0],TIME_DATE|TIME_MINUTES|TIME_SECONDS)+"]"); return(false); } //--- 如果时间不同了 if(new_bar[number_symbol]!=lastbar_time[number_symbol].time[0]) { //--- 设置时间并退出 new_bar[number_symbol]=lastbar_time[number_symbol].time[0]; return(true); } //--- 如果我们到达了这一行,说明柱不是新的,返回false return(false); }
TradingBlock() 函数代码:
//+------------------------------------------------------------------+ //| 交易模块 | //+------------------------------------------------------------------+ void TradingBlock(int symbol_number) { ENUM_ORDER_TYPE signal=WRONG_VALUE; // 用于取得信号的变量 string comment="hello :)"; // 仓位注释 double tp=0.0; // 获利 double sl=0.0; // 止损 double lot=0.0; // 用于仓位反转时仓位交易量计算 double position_open_price=0.0; // 仓位开盘价 ENUM_ORDER_TYPE order_type=WRONG_VALUE; // 建仓的订单类型 ENUM_POSITION_TYPE opposite_position_type=WRONG_VALUE; // 反向仓位类型 //--- 查找已有持仓 pos.exists=PositionSelect(Symbols[symbol_number]); //--- 取得信号 signal=GetTradingSignal(symbol_number); //--- 如果没有信号,则退出 if(signal==WRONG_VALUE) return; //--- 取得交易品种属性 GetSymbolProperties(symbol_number,S_ALL); //--- 确定交易变量值 switch(signal) { //--- 给买入变量赋值 case ORDER_TYPE_BUY : position_open_price=symb.ask; order_type=ORDER_TYPE_BUY; opposite_position_type=POSITION_TYPE_SELL; break; //--- 给卖出变量赋值 case ORDER_TYPE_SELL : position_open_price=symb.bid; order_type=ORDER_TYPE_SELL; opposite_position_type=POSITION_TYPE_BUY; break; } //--- 取得获利和止损水平 sl=CalculateStopLoss(symbol_number,order_type); tp=CalculateTakeProfit(symbol_number,order_type); //--- 如果没有持仓 if(!pos.exists) { //--- 调整交易量 lot=CalculateLot(symbol_number,Lot[symbol_number]); //--- 开启仓位 OpenPosition(symbol_number,lot,order_type,position_open_price,sl,tp,comment); } //--- 如果有持仓 else { //--- 读取仓位类型 GetPositionProperties(symbol_number,P_TYPE); //--- 如果仓位和信号方向相反并且反向仓位被启用 if(pos.type==opposite_position_type && Reverse[symbol_number]) { //--- 取得仓位交易量 GetPositionProperties(symbol_number,P_VOLUME); //--- 调整交易量 lot=pos.volume+CalculateLot(symbol_number,Lot[symbol_number]); //--- 反转仓位 OpenPosition(symbol_number,lot,order_type,position_open_price,sl,tp,comment); return; } //--- 如果信号与仓位方向相同并且启用了增加交易量,则增加仓位交易量 if(!(pos.type==opposite_position_type) && VolumeIncrease[symbol_number]>0) { //--- 取得当前仓位的止损 GetPositionProperties(symbol_number,P_SL); //--- 取得当前仓位的获利 GetPositionProperties(symbol_number,P_TP); //--- 调整交易量 lot=CalculateLot(symbol_number,VolumeIncrease[symbol_number]); //--- 增加仓位交易量 OpenPosition(symbol_number,lot,order_type,position_open_price,pos.sl,pos.tp,comment); return; } } }
OpenPosition()函数的代码:
//+------------------------------------------------------------------+ //| 开启一个仓位 | //+------------------------------------------------------------------+ void OpenPosition(int symbol_number, double lot, ENUM_ORDER_TYPE order_type, double price, double sl, double tp, string comment) { //--- 设置交易结构的幻数 trade.SetExpertMagicNumber(MagicNumber); //--- 设置滑点点数 trade.SetDeviationInPoints(CorrectValueBySymbolDigits(Deviation)); //--- 即时执行和市场执行模式 // *** 从 build 803 开始, 止损和获利 *** // *** 可以在以 SYMBOL_TRADE_EXECUTION_MARKET 模式下设置 *** if(symb.execution_mode==SYMBOL_TRADE_EXECUTION_INSTANT || symb.execution_mode==SYMBOL_TRADE_EXECUTION_MARKET) { //--- 如果仓位开始失败,打印相关信息 if(!trade.PositionOpen(Symbols[symbol_number],order_type,lot,price,sl,tp,comment)) Print("开始仓位错误: ",GetLastError()," - ",ErrorDescription(GetLastError())); } }
这样,每个函数现在都能接收交易品种编号了(symbol_number)。也请注意build 803中做的改变:
其他修改过代码的函数都可以在附件的文件中找到。我们现在需要做的就是优化参数和进行测试了。
优化参数和测试EA交易
我们首先优化第一个交易品种的参数,然后再优化第二个。让我们从EURUSD开始.
以下是策略测试器的设置:
图 1. 策略测试器设置.
EA交易的设置应该如下(为了更加方便, 包含每个交易品种设置的 .set 文件也附在文章的附件中了). 为了在优化中排除某个交易品种,您只需要把交易品种名称参数栏位设为空,为每个交易品种独立地优化参数也会使优化过程加快。
图 2. 用于参数优化的EA交易设置: EURUSD.
优化在一个双核处理器上大约需要1个小时。最大采收率测试结果显示如下:
图 3. EURUSD的最大采收率测试结果.
现在把NZDUSD设为第二个交易品种. 为了优化,把第一个参数块部分的交易品种名称设为空,
NZDUSD 的优化结果如下:
图 4. NZDUSD的最大采收率测试结果.
现在我们可以两个交易品种一起测试了,在策略测试器设置中,您可以使EA交易启动在任何交易品种上,因为结果都是一样的,它甚至可以是一个并不参与交易/测试的交易品种。
以下是两个交易品种一起测试的结果:
图 5. 两个交易品种的测试结果: EURUSD 和 NZDUSD.
结论
就是这样,源代码部分都在附件中可供下载和进一步学习。做为练习,您可以尝试选则一个或多个交易品种或者使用其他指标改变仓位开启的条件。
在从存档中展开文件以后,把MultiSymbolExpert 文件夹放到MetaTrader 5\MQL5\Experts目录下。进而, EventsSpy.mq5 指标必须放到 MetaTrader 5\MQL5\Indicators 目录下。
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/648
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.



文章简洁易懂,并提供了 .set 文件中的设置。我在符号执行模式 方面遇到了一个问题,特别是检查是即时还是市场的条件,只有在检查后才允许打开订单,我不得不将其删除,但一切正常。
我有一个问题。我看到过很多不同的高级方法,比如这个方法,它们从一开始就忽略了所提供的 Expert、ExpertSignal、ExpertTrade.... 结构,尽管它们花了很多代码(我想也花了很多时间)复制其中的一些功能。有谁能向我解释一下吗?
大家好!
我正试着测试这个 EA,但在 Strategy Tester 中得到了以下信息:"开仓出错:4753 - 1 未找到头寸"。我不明白这是为什么。当执行文件 "TradeFunctions.mqh "第 159 行中的 "trade.PositionOpen "函数时,就会出现这种情况。谁能帮帮我?
您能告诉我作者描述的位置反转 发生在哪里吗?以下是他的代码和描述
查看OpenPosition(OpenPosition_symbol_number,lot,order_type,position_open_price,sl,tp,comment) 函数;
这只是一个锁定!没有反转......手数递增也是如此!您能解释一下吗,也许我错了?您能告诉我作者描述的位置反转 发生在哪里吗?以下是他的代码和描述
查看OpenPosition(OpenPosition_symbol_number,lot,order_type,position_open_price,sl,tp,comment) 函数;
这只是一个锁定!没有反转......手数递增也是如此!您能解释一下吗,也许我说错了?有两种类型的交易账户:净额结算和对冲。
交易账户有两种类型:净额结算和套期保值。
买入谢谢