我相信很多交易者都经常对他们交易系统优化过的参数感到疑惑,事实上,一个单独的交易算法并不够,我们需要看它被如何运用。无论您使用何种交易策略,是简单还是复杂,使用一种还是多种资产,您都不能避免这样的问题:如何选择参数以保证未来的利润。
我们将使用在优化期间(回归测试)显示良好结果的参数在接下来的时间段(前瞻测试)进行验证测试。前瞻测试事实上没有那么必要,相关的结果可以通过历史数据获得。
这种方法提出了一个很大的而且没有确定答案的问题:到底需要多少历史数据来优化一个交易系统?这件事情有很多选项。它依赖于您想使用的价格波动范围。
回到优化所需历史数量的问题,我们可以认为可用的数据对小时内的交易已经足够,但是对于长时间范围来说这不一定正确。重复模式的数量越多,比如,我们的交易数量越多,所测试的交易系统在未来期待的效率就越可信。
如果某项资产的价格数据不够而无法获得那么多的重复结果和感到那么放心,我们怎么办呢?答案:使用所有可用资产的数据。
在我们继续使用MetaTrader 5编程之前,让我们看一个来自NeuroShell DayTrader 专业版的实例。它提供了强大的功能为使用多个交易品种的交易系统(使用构造函数编译)优化参数,您可以在交易模块设置部分设置所需的参数,为每一个交易品种单独地进行参数的优化或者为全部交易品种找到一个同时最优的参数集合。此选项可以在优化页面找到:
图 1. NeuroShell DayTrader 专业版交易模块的优化页面
在我们的实例中,任何简单的交易系统都可以,因为我们只需要比较两种优化方法的结果,所以系统的选择目前并不重要。
您可以在我的博客(您可以使用搜索或者标签来定位有关信息)上找到如何编译NeuroShell DayTrader 专业版交易策略的信息,我也想向您推荐阅读一篇叫做"怎样为其他应用程序准备MetaTrader5报价信息"的文章,它向您演示怎样使用脚本从MetaTrader 5中下载与NeuroShell DayTrader 专业版兼容的报价信息。
为了做这个测试,我准备了8个交易品种的从2000年到2013年的日图柱数据:
图 2. 用于NeuroShell DayTrader 专业版测试的交易品种列表
下图中显示了两种优化的结果,上面的部分显示了每个交易品种使用自己参数的优化结果,而下图显示了所有交易品种使用通用参数的优化结果。
图 3. 两种参数优化方法的结果比较
结果显示,使用通用参数看起来没有为每个交易品种使用不同参数那么好,然而这让我们更加确信交易系统通过对所有交易品种使用相同参数可以说明它适用于各种不同的价格表现模式(波动,趋势/平盘的数量)。
继续此话题,我们在逻辑上可以找到另外一个参数来使用更多数据进行优化,这样做比较好,因为某个货币对,比如EURUSD,它的价格行为可能前后变化很大(在两年,五年或者十年内)。比如,GBPUSD的价格趋势和之前EURUSD的价格行为将很类似,反之也是。您应该做好准备,因为这对任何资产来说都是真实的。
现在让我们看一下MetaTrader 5中提供了哪些参数优化模式。以下在优化模式的下拉列表中您可以看见使用一个箭头标识的市场报价中选择的全部交易品种优化模式。
图 4. MetaTrader 5 策略测试器中的优化模式
这种模式允许您的EA交易挨个在每个交易品种上只测试当前的参数,测试中使用的交易品种是当前市场报价窗口中所选择的,换句话说,在这种情况下没有进行参数优化。然而,MetaTrader 5 和 MQL5 允许您使用自己的方式来实现这个想法。
现在,我们需要看一下怎样实现这样的EA交易。交易品种列表将在一个文本文件(*.txt)中提供。另外,我们会提供选项保存几组交易品种列表,每一组将有一个独立的区段,使用区段编号分隔拥有自己的抬头部分,数字编号在检查时非常必要。
请注意,在数字之前必须有#,这才能使EA交易在填充交易品种数组的时候取得正确的数据。通常情况下,抬头部分可能包含任何交易品种,但是所有时候它又必须有#符号。数字符号可以用任何其他字符代替,这要看在EA交易中如何对区段进行判断/计数,如果您要替换该符号,代码中必须做对应的改变。
以下您可以看到包含用于测试的三个交易品种集合的SymbolsList.txt文件。这里显示的文件将在未来对这种方法进行测试时使用。
图 5. 测试文本文件中包含的几个交易品种集合
在外部参数中,我们将增加另外一个参数SectionOfSymbolList,它用来指出EA交易在当前测试时应该使用哪种交易品种的集合。这个参数使用定义交易品种集合的数值(大于等于0)如果此数值大于可用的集合数,EA交易会在日志中写入对应内容而测试只是会在当前交易品种下进行。
SymbolsList.txt 必须放在终端的本地目录 Metatrader 5\MQL5\Files之下。它也可以放到通用目录下,但是如果这样就不能使用MQL5 云网络进行参数优化了。另外,为了允许用于测试的相关自定义指标也能访问此文件,我们需要在文件开头加上以下几行代码:
//--- 允许云网络在优化过程中访问外部文件和指标 #property tester_file "SymbolsList.txt" #property tester_indicator "EventsSpy.ex5"
我们的EA交易将基于在文章"MQL5 Cookbook: 使用没有数量限制的参数开发多币种EA交易"中做好的多币种EA交易,它的交易策略非常简单,但是它足够用于测试这种方法的有效性了。我们将删除其中不必要的部分,增加我们需要的代码以及修改已有的相关代码。我们也会使用本系列前文"MQL5 Cookbook: 把交易历史写入文件并在Excel中为每个交易品种创建余额图表"中描述的保存报告功能增强我们的EA交易。我们也会需要所有交易品种的余额图标用来评估方法的效率。
EA交易的外部参数应该如下修改:
//--- EA交易的外部参数 sinput int SectionOfSymbolList = 1; // 交易品种列表中的区域编号 sinput bool UpdateReport = false; // 更新报告 sinput string delimeter_00=""; // -------------------------------- sinput long MagicNumber = 777; // 幻数 sinput int Deviation = 10; // 滑点 sinput string delimeter_01=""; // -------------------------------- input int IndicatorPeriod = 5; // 指标周期数 input double TakeProfit = 100; // 获利 input double StopLoss = 50; // 止损 input double TrailingStop = 10; // 跟踪止损 input bool Reverse = true; // 反向仓位 input double Lot = 0.1; // 手数 input double VolumeIncrease = 0.1; // 仓位交易量增加值 input double VolumeIncreaseStep = 10; // 交易量增加步长
所有与外部参数相关的数组都应该被删除,因为已经不需要它们了,在整个代码中必须使用外部变量来替代它们。我们应该只留下交易品种的动态数组,InputSymbols[],它的大小将依赖于SymbolsList.txt文件中交易品种集合中交易品种的数量。如果EA交易是在策略测试器以外使用的,数组的大小应该等于1,因为在实时模式下EA交易只处理一个交易品种。
对应的修改也必须在数组初始化文件- InitializeArrays.mqh中进行,即所有用于外部变量数组初始化的函数应该被删除。InitializeArraySymbols() 函数现在看起来如下:
//+------------------------------------------------------------------+ //| 填充交易品种数组 | //+------------------------------------------------------------------+ void InitializeArraySymbols() { int strings_count =0; // 交易品种文件的字符串数量 string checked_symbol =""; // 检查交易品种在交易服务器上是否可以访问 //--- 测试模式的信息 string message_01="<--- 在 <- SymbolsList.txt -> 文件中的交易品种名称不正确 ... --->\n" "<--- ... 或者 \"Section of List Symbols\" 参数的值比 " "文件段数大!--->\n" "<--- 这样我们只会测试当前交易品种. --->"; //--- 实时模式下的信息 string message_02="<--- 在实时模式下, 我们只处理当前交易品种. --->"; //--- 如果是实时模式 if(!IsRealtime()) { //--- 从文件中取得指定交易品种区域字符串的数量并填充交易品种的临时数组 strings_count=ReadSymbolsFromFile("SymbolsList.txt"); //--- 从指定的集合中迭代所有交易品种 for(int s=0; s<strings_count; s++) { //--- 如果检查交易品种之后返回了正确的字符串 if((checked_symbol=GetSymbolByName(temporary_symbols[s]))!="") { //--- 增加计数器 SYMBOLS_COUNT++; //--- 设置/增加数组大小 ArrayResize(InputSymbols,SYMBOLS_COUNT); //--- 交易品种名称索引 InputSymbols[SYMBOLS_COUNT-1]=checked_symbol; } } } //--- 如果全部交易品种名称没有正确输入或者目前是工作在实时模式下 if(SYMBOLS_COUNT==0) { //--- 实时模式信息 if(IsRealtime()) Print(message_02); //--- 测试模式信息 if(!IsRealtime()) Print(message_01); //--- 我们只处理当前交易品种 SYMBOLS_COUNT=1; //--- 设置数组大小并且 ArrayResize(InputSymbols,SYMBOLS_COUNT); //--- 记录当前交易品种名称索引 InputSymbols[0]=_Symbol; } }
ReadSymbolsFromFile()函数的代码也需要修改。它曾经用于读取整个交易品种列表,而现在我们只需要它读取指定的交易品种集合。以下是修改过的函数代码:
//+------------------------------------------------------------------+ //| 从指定的集合中返回字符串(交易品种)的数目 | //| 并且填充到交易品种的临时数组中 | //+------------------------------------------------------------------+ //--- 当准备文件的时候,列表中的交易品种应该用行分开 int ReadSymbolsFromFile(string file_name) { ulong offset =0; // 确定文件指针位置的偏移量 string delimeter ="#"; // 区段起始部分的标示符 string read_line =""; // 用于检查读取的字符串 int limit_count =0; // 限制可能开启图表数目的计数器 int strings_count =0; // 字符串计数器 int sections_count =-1; // 区段计数器 //--- Message 01 string message_01="<--- <- "+file_name+" -> 文件还没有正确准备好!--->\n" "<--- 第一个字符串没有包含区段编号标识符 ("+delimeter+")!--->"; //--- Message 02 string message_02="<--- <- "+file_name+" -> 文件还没有正确准备好!--->\n" "<--- 最后的字符串没有换行符号, --->\n" "<--- 所以只测试当前交易品种. --->"; //--- Message 03 string message_03="<--- <- "+file_name+" -> 文件无法找到!--->" "<--- 只测试当前交易品种. --->"; //--- 在终端的本地目录中打开文件 (取得句柄) 用于读取 int file_handle=FileOpen(file_name,FILE_READ|FILE_ANSI,'\n'); //--- 如果文件句柄已经得到 if(file_handle!=INVALID_HANDLE) { //--- 读取直到文件指针的当前位置 // 达到文件末尾或者程序被移除 while(!FileIsEnding(file_handle) || !IsStopped()) { //--- 读取文件直到当前文件指针到达文件末尾或者程序退出 while(!FileIsLineEnding(file_handle) || !IsStopped()) { //--- 读取整个字符串 read_line=FileReadString(file_handle); //--- 如果找到了区段编号标示符 if(StringFind(read_line,delimeter,0)>-1) //--- 增加区段计数器 sections_count++; //--- 如果区段已经读取,退出函数 if(sections_count>SectionOfSymbolList) { FileClose(file_handle); // 关闭文件 return(strings_count); // 返回文件的字符串数量 } //--- 如果这是第一个迭代而且第一个字符串不包含区段编号标识符 if(limit_count==0 && sections_count==-1) { PrepareArrayForOneSymbol(strings_count,message_01); //--- 关闭文件 FileClose(file_handle); //--- 返回文件的字符串数目 return(strings_count); } //--- 增加可能开启图表限制的计数器 limit_count++; //--- 如果达到了限制 if(limit_count>=CHARTS_MAX) { PrepareArrayForOneSymbol(strings_count,message_02); //--- 关闭文件 FileClose(file_handle); //--- 返回文件的字符串数目 return(strings_count); } //--- 取得指针位置 offset=FileTell(file_handle); //--- 如果这是字符串末尾 if(FileIsLineEnding(file_handle)) { //--- 如果还没有到文件末尾,读取下一个字符串 // 为了这个目的,增加文件指针的偏移 if(!FileIsEnding(file_handle)) offset++; //--- 移动到下一个字符串 FileSeek(file_handle,offset,SEEK_SET); //--- 如果我们不在文件的指定区段,退出循环 if(sections_count!=SectionOfSymbolList) break; //--- 否则, else { //--- 如果字符串不为空 if(read_line!="") { //--- 增加字符串计数器 strings_count++; //--- 增加字符串数组的大小, ArrayResize(temporary_symbols,strings_count); //--- 把字符串写到当前索引处 temporary_symbols[strings_count-1]=read_line; } } //--- 退出循环 break; } } //--- 如果这是文件末尾,终止整个循环 if(FileIsEnding(file_handle)) break; } //--- 关闭文件 FileClose(file_handle); } else PrepareArrayForOneSymbol(strings_count,message_03); //--- 返回文件字符串数量 return(strings_count); }
您可以发现以上代码的一些字符串被高亮标记了,那些包含着PrepareArrayForOneSymbol()函数的字符串在出错情况下就简单地为一个(当前)交易品种准备一个数组。
//+------------------------------------------------------------------+ //| 为一个交易品种准备一个数组 | //+------------------------------------------------------------------+ void PrepareArrayForOneSymbol(int &strings_count,string message) { //--- 打印信息到日志 Print(message); //--- 数组大小 strings_count=1; //--- 设置交易品种数组大小 ArrayResize(temporary_symbols,strings_count); //--- 在当前索引位置写下当前交易品种的字符串 temporary_symbols[0]=_Symbol; }
现在所有事情都准备好了,可以测试参数优化方法了。但是在我们继续测试之前,我们先在报告中增加另一个数据序列。之前,除了所有交易品种的余额,报告文件还包含所有局部最大值的回撤百分率。现在,报告还要包含所有回撤的钱数。同时我们需要修改生成报告的 CreateSymbolBalanceReport() 函数。
CreateSymbolBalanceReport() 函数代码提供如下:
//+------------------------------------------------------------------+ //| 创建 .csv 格式的交易测试报告 | //+------------------------------------------------------------------+ void CreateSymbolBalanceReport() { int file_handle =INVALID_HANDLE; // 文件句柄 string path =""; // 文件路径 //---如果在创建/读取文件夹时出错,退出 if((path=CreateInputParametersFolder())=="") return; //--- 在终端通用文件夹下创建文件用于写入数据 file_handle=FileOpen(path+"\\LastTest.csv",FILE_CSV|FILE_WRITE|FILE_ANSI|FILE_COMMON); //--- 如果句柄有效 (文件已创建/打开) if(file_handle>0) { int digits =0; // 价格中的小数点位数 int deals_total =0; // 指定历史的交易数量 ulong ticket =0; // 交易订单号 double drawdown_max =0.0; // 回撤 double balance =0.0; // 余额 string delimeter =","; // 分隔符 string string_to_write =""; // 生成写入的字符串 static double percent_drawdown =0.0; // 回撤表现为百分率 static double money_drawdown =0.0; // 回撤钱数 //--- 生成抬头字符串 string headers="TIME,SYMBOL,DEAL TYPE,ENTRY TYPE,VOLUME," "PRICE,SWAP($),PROFIT($),DRAWDOWN(%),DRAWDOWN($),BALANCE"; //--- 如果选了多个交易品种, 修改抬头字符串 if(SYMBOLS_COUNT>1) { for(int s=0; s<SYMBOLS_COUNT; s++) StringAdd(headers,","+InputSymbols[s]); } //--- 写报告抬头 FileWrite(file_handle,headers); //--- 取得完整历史 HistorySelect(0,TimeCurrent()); //--- 取得交易数量 deals_total=HistoryDealsTotal(); //---根据交易品种数量修改余额数组大小 ArrayResize(symbol_balance,SYMBOLS_COUNT); //--- 修改每个交易品种的交易数组大小 for(int s=0; s<SYMBOLS_COUNT; s++) ArrayResize(symbol_balance[s].balance,deals_total); //--- 在循环中迭代并写入数据 for(int i=0; i<deals_total; i++) { //--- 取得交易订单号码 ticket=HistoryDealGetTicket(i); //--- 取得所有的交易属性 GetHistoryDealProperties(ticket,D_ALL); //--- 取得价格中的小数位数 digits=(int)SymbolInfoInteger(deal.symbol,SYMBOL_DIGITS); //--- 计算总体余额 balance+=deal.profit+deal.swap+deal.commission; //--- 计算从局部最大值的最大回撤 TesterDrawdownMaximum(i,balance,percent_drawdown,money_drawdown); //--- 使用连接生成用于写入的字符串 StringConcatenate(string_to_write, deal.time,delimeter, DealSymbolToString(deal.symbol),delimeter, DealTypeToString(deal.type),delimeter, DealEntryToString(deal.entry),delimeter, DealVolumeToString(deal.volume),delimeter, DealPriceToString(deal.price,digits),delimeter, DealSwapToString(deal.swap),delimeter, DealProfitToString(deal.symbol,deal.profit),delimeter, DrawdownToString(percent_drawdown),delimeter, DrawdownToString(money_drawdown),delimeter, DoubleToString(balance,2)); //--- 如果选择了多个交易品种,写下它们的余额 if(SYMBOLS_COUNT>1) { //--- 迭代所有交易品种 for(int s=0; s<SYMBOLS_COUNT; s++) { //--- 如果交易品种相同而交易结果不等于零 if(deal.symbol==InputSymbols[s] && deal.profit!=0) { //--- 显示对应交易品种的交易余额 // 考虑库存费和手续费 symbol_balance[s].balance[i]=symbol_balance[s].balance[i-1]+ deal.profit+ deal.swap+ deal.commission; //--- 加到字符串上 StringAdd(string_to_write,","+DoubleToString(symbol_balance[s].balance[i],2)); } //--- 否则写下前值 else { //--- 如果交易类型为 "余额" (第一笔交易) if(deal.type==DEAL_TYPE_BALANCE) { //--- 余额对所有交易品种都相同 symbol_balance[s].balance[i]=balance; StringAdd(string_to_write,","+DoubleToString(symbol_balance[s].balance[i],2)); } //--- 否则在当前索引写下前值 else { symbol_balance[s].balance[i]=symbol_balance[s].balance[i-1]; StringAdd(string_to_write,","+DoubleToString(symbol_balance[s].balance[i],2)); } } } } //--- 写下生成的字符串 FileWrite(file_handle,string_to_write); //--- 强制把变量清零,为下个字符串做准备 string_to_write=""; } //--- 关闭文件 FileClose(file_handle); } //--- 如果文件无法创建/打开,打印相关信息 else Print("创建文件出错!错误: "+IntegerToString(GetLastError())+""); }
我们曾经在DrawdownMaximumToString()函数中计算回撤。现在这在TesterDrawdownMaximum()函数中进行,而使用DrawdownToString()函数把回撤值转换为字符串。
TesterDrawdownMaximum()函数的代码如下:
//+------------------------------------------------------------------+ //| 返回局部最大值的最大回撤 | //+------------------------------------------------------------------+ void TesterDrawdownMaximum(int deal_number, double balance, double &percent_drawdown, double &money_drawdown) { ulong ticket =0; // 交易订单编号 string str =""; // 在报告中显示的字符串 //--- 计算局部最大值和回撤 static double max =0.0; static double min =0.0; //--- 如果是第一笔交易 if(deal_number==0) { //--- 还没有回撤 percent_drawdown =0.0; money_drawdown =0.0; //--- 设置起点为局部最大值 max=balance; min=balance; } else { //--- 如果当前余额高于存储的, 那么... if(balance>max) { //--- 使用前值计算回撤: // 钱数 money_drawdown=max-min; // 表示为百分率 percent_drawdown=100-((min/max)*100); //--- 更新局部最大值 max=balance; min=balance; } //--- 否则 else { //--- 返回0值做为回撤 money_drawdown=0.0; percent_drawdown=0.0; //--- 更新最小值 min=fmin(min,balance); //--- 如果仓位的交易订单编号已经获得,那么... if((ticket=HistoryDealGetTicket(deal_number))>0) { //--- ...取得交易注释 GetHistoryDealProperties(ticket,D_COMMENT); //--- 最后一笔交易的标志 static bool last_deal=false; //--- 测试中的最后一笔交易可以根据"测试结束"注释来找到 if(deal.comment=="测试结束" && !last_deal) { //--- 设置标志 last_deal=true; //--- 更新回撤值: // 钱数 money_drawdown=max-min; // 表示为百分率 percent_drawdown+=100-((min/max)*100); } } } } }
DrawdownToString()函数的代码提供如下:
//+------------------------------------------------------------------+ //| 把回撤转换为字符串 | //+------------------------------------------------------------------+ string DrawdownToString(double drawdown) { return((drawdown<=0) ? "" : DoubleToString(drawdown,2)); }
现在所有事情都已经准备好了,可以进行EA交易的测试和分析了。在文章前面的部分,我们看见了一个准备好的文件实例,让我们这样做:为第二个交易品种集合做参数优化(有3个交易品种: EURUSD, AUDUSD and USDCHF),优化以后使用第三个交易品种集合的所有交易品种(共7个交易品种)做测试, 来看一下没有进行参数优化的交易品种的测试结果。
策略测试器需要如下设置:
图 6. 策略测试器用于优化的设置
用于参数优化的EA交易设置提供如下:
图 7. 用于参数优化的EA交易设置
因为优化包含了3个交易品种,每个都启用了仓位交易量增加,我们把开启仓位交易量设为最小手数并且增加了仓位的交易增加量。在我们的实例中,其数值为0.01。
在优化之后,我们从最大采收率结果中选取最好结果,并且把VolumeIncrease参数设置为0.1手数。结果显示如下:
图 8. MetaTrader 5 中的测试结果
以下,您可以看到显示于 Excel 2010 中的结果:
图 9. 在 Excel 2010 中显示的三个交易品种的测试结果
在底下的图表中,回撤的钱数用绿色标出做为第二(辅助)尺度。
您也应该注意Excel 2010中的图表限制 (完整的规格和限制列表可以在 Microsoft Office 网站的Excel 规格与限制页面找到)。
图 10. Excel 2010 图标的规格和限制
表格中显示我们可以同时运行255个交易品种的数据而在图表中显示所有的结果!我们的限制只有计算机资源。
让我们现在使用当前参数运行第3个集合中的7个交易品种来检查结果:
图 11. Excel 2010 中显示的7个交易品种的测试结果
在处理的7个交易品种中,我们共有 6901 笔交易。Excel 2010 中图表数据的更新非常快。
我相信这里介绍的方法非常值得关注,因为我们即使使用这样简单的交易策略也显示了很好的结果。在此,我们应该知道,优化只是在3个交易品种上进行而不是7个,我们可以为所有交易品种做参数优化而进一步提高结果数据,但是,我们首要的还是提高交易系统本身,或者更好一点,拥有多个不同的交易系统,晚些时候我们会回到这个想法上。
就是这样,我们已经有了研究多币种交易策略的相对有用的工具,以下是可供下载的zip文件,包含了您可以使用的EA交易。
在解开这些文件以后,把ReduceOverfittingEA文件夹放到MetaTrader 5\MQL5\Experts目录之下;还有,EventsSpy.mq5指标必须放在MetaTrader 5\MQL5\Indicators目录下。而SymbolsList.txt 必须放在MetaTrader 5\MQL5\Files目录。
本社区仅针对特定人员开放
查看需注册登录并通过风险意识测评
5秒后跳转登录页面...
移动端课程