
在MQL5中置换价格柱
概述
MetaTrader 5的策略测试器是许多人用来评估专家顾问(EA)潜力的主要工具。虽然它的功能足够,但经验丰富的开发人员可以使用它来制作能够伪装出非凡性能的“特技”EA。我们都看到了那些净值曲线的屏幕截图,显示了EA卖家令人难以置信的表现。乍一看,这一切都令人印象深刻,但当该策略应用于现实世界时,往往会产生完全不同的净值曲线。我们怎样才能避免上当这些廉价把戏的后果呢?在本文中,我们将研究这样一个系统,并演示如何使用置换测试来消除误导性净值曲线的烟幕弹,从而更准确地了解策略性能。此外,在前一篇文章中,我们看到了一种置换分时数据的算法的实现。这一次,我们将描述一种置换价格柱的方法。
置换 OHLC 数据
由于涉及多个系列,置换价格柱并不难实现。与置换分时数据类似,在处理价格柱时,我们努力保持原始价格序列的总体趋势。同样重要的是,我们绝不允许柱的打开或关闭分别超过或低于上限或下限的界限。目标是获得一系列具有与原始数据完全相同的特征分布的柱形图。
除了趋势之外,随着系列从开盘到收盘的发展,我们必须保持价格变化的分散性。开盘和收盘之间的价格变化幅度在排列的柱形图中应与原始柱形图相同。在柱形之外,我们必须确保柱间价格变化的分布也是相同的。具体地说,一个柱的关闭和下一柱的开始之间的差异。
这一点非常重要,以免对正在测试的策略造成不利影响。该系列的一般特征应该相似,唯一的区别应该是第一柱和最后一个柱之间每个开盘价、最高价、最低价、收盘价(OHLC)的绝对值。实现这一点的代码与在 MetaTrader 5 的蒙特卡罗置换测试一文中介绍的CPermuteTicks类中使用的代码非常相似。价格柱置换代码将封装在PermuteRates.mqh中包含的CPermuteRates类中。
CPermuteRates 类
//+------------------------------------------------------------------+ //| struct to handle relative values of rate data to be worked on | //+------------------------------------------------------------------+ struct CRelRates { double rel_open; double rel_high; double rel_low; double rel_close; }; //+------------------------------------------------------------------+ //| Class to enable permuation of a collection of ticks in an array | //+------------------------------------------------------------------+ class CPermuteRates { private : MqlRates m_rates[]; //original rates to be shuffled CRelRates m_differenced[]; //log difference of rates bool m_initialized; //flag to signal state of random number object CUniFrand *m_random; //random number generator public : //constructor CPermuteRates(void); //desctructor ~CPermuteRates(void); bool Initialize(MqlRates &in_rates[]); bool Permute(MqlRates &out_rates[]); };
PermuteRate.mqh从一个简单结构的定义开始,该结构将存储原始价格的差异记录。
- rel_open 将保存当前柱开盘价和上一个柱收盘价之间的对数差
- rel_high 表示当前柱形最高价和开盘价之间的对数差。
- rel_low 是指当前柱最低价和开盘价之间的对数差
- rel_close 又是当前柱收盘价和开盘价之间的对数差
自定义 CRelRates 结构表示从将被置换的MqlRates中提取的数据。MqlRates的其他结构成员将不会被更改。置换价格的最终结果将使这些结构成员从原始价格序列中复制。正如已经提到的,将改变的只是OHLC值。
//+------------------------------------------------------------------+ //| Permute the bars | //+------------------------------------------------------------------+ bool CPermuteRates::Permute(MqlRates &out_rates[]) { //--- if(!m_initialized) { Print("Initialization error"); ZeroMemory(out_rates); return false; } //--- int i,j; double temp=0.0; //--- i=ArraySize(m_rates)-2; //--- while(i > 1 && !IsStopped()) { j = (int)(m_random.RandomDouble() * i) ; if(j >= i) j = i - 1 ; --i ; temp = m_differenced[i+1].rel_open ; m_differenced[i+1].rel_open = m_differenced[j+1].rel_open ; m_differenced[j+1].rel_open = temp ; } //--- i =ArraySize(m_rates)-2; //--- while(i > 1 && !IsStopped()) { j = (int)(m_random.RandomDouble() * i) ; if(j >= i) j = i - 1 ; --i ; temp = m_differenced[i].rel_high; m_differenced[i].rel_high = m_differenced[j].rel_high ; m_differenced[j].rel_high = temp ; temp = m_differenced[i].rel_low ; m_differenced[i].rel_low = m_differenced[j].rel_low ; m_differenced[j].rel_low = temp ; temp = m_differenced[i].rel_close ; m_differenced[i].rel_close = m_differenced[j].rel_close ; m_differenced[j].rel_close = temp ; } //--- if(ArrayCopy(out_rates,m_rates)!=int(m_rates.Size())) { ZeroMemory(out_rates); Print("Copy error ", GetLastError()); return false; } //--- for(i=1 ; i<ArraySize(out_rates) && !IsStopped() ; i++) { out_rates[i].open = MathExp(((MathLog(out_rates[i-1].close)) + m_differenced[i-1].rel_open)) ; out_rates[i].high = MathExp(((MathLog(out_rates[i].open)) + m_differenced[i-1].rel_high)) ; out_rates[i].low = MathExp(((MathLog(out_rates[i].open)) + m_differenced[i-1].rel_low)) ; out_rates[i].close = MathExp(((MathLog(out_rates[i].open)) + m_differenced[i-1].rel_close)) ; } //--- if(IsStopped()) return false; //--- return true; //--- }
置换是在Permute()方法中完成的。CRelRates结构将柱形图数据分为两种类型的描述符。rel_open 系列值表示从一个柱形到下一个柱形的变化,而rel_high、rel_low和rel_close表示柱形内的变化。为了置换柱形图,我们首先对 rel_open 的价格序列进行打乱,这些是柱形图之间的差异。从那里开始,再打乱内部柱的变化。新的OHLC系列是从打乱的柱间数据构建的,以获得由打乱的柱内变化构建的具有相应最高价、最低价和收盘价的新开盘价。
CPermuteTicks 的更改
CPermuteRates 和旧的 CPermuteTicks 类之间存在许多差异。其中之一是使用自定义随机数生成器,我发现它比使用MQL5的内置函数快一点。
//+------------------------------------------------------------------+ //| UniformRandom.mqh | //| Copyright 2023, MetaQuotes Software Corp. | //| https://www.MQL5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2023, MetaQuotes Software Corp." #property link "https://www.MQL5.com" //+----------------------------------------------------------------------+ //| CUniFrand class: Uniformly distributed random 0 - 1 number generator| //+----------------------------------------------------------------------+ class CUniFrand { private : uint m_m[256]; int m_mwc_initialized; int m_mwc_seed; uint m_carry; uint random(void); public : //constructor CUniFrand(void); //desctructor ~CUniFrand(void); //optionally set a seed for number generator void SetSeed(const int iseed); //get random number between 0 and 1 double RandomDouble(void); }; //+------------------------------------------------------------------+ //| Default constructor | //+------------------------------------------------------------------+ CUniFrand::CUniFrand(void) { m_mwc_initialized=0; m_mwc_seed=123456789; m_carry=362436; } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CUniFrand::~CUniFrand(void) { } //+------------------------------------------------------------------+ //| creates and returns random integer number | //+------------------------------------------------------------------+ uint CUniFrand::random(void) { uint t,a=809430660; static uchar i; if(!m_mwc_initialized) { uint k,j=m_mwc_seed; m_mwc_initialized=1; for(k=0; k<256; k++) { j = 69069 * j + 12345; m_m[k]=j; } } t=a*m_m[++i] + m_carry; m_carry = (uint)(t>>32); m_m[i] = (uint)(t&UINT_MAX); return m_m[i]; } //+------------------------------------------------------------------+ //| Optionally set the seed for random number generator | //+------------------------------------------------------------------+ void CUniFrand::SetSeed(const int iseed) { m_mwc_seed=iseed; m_mwc_initialized=0; } //+------------------------------------------------------------------+ //| returns a random number between 0 and 1 | //+------------------------------------------------------------------+ double CUniFrand::RandomDouble(void) { double mult =1.0/UINT_MAX; return mult * random(); } //+------------------------------------------------------------------+
它也应用于新的CPermuteTicks类。为了提高效率,已经取消了不必要的中间操作。只有买家价格(bid)被打乱了。通过从原始分时系列复制其他分时属性,这解决了一个问题,该问题有时会导致置换分时的点差不现实。新的 CPermuteTick 系列如下所示。
//+------------------------------------------------------------------+ //| PermuteTicks.mqh | //| Copyright 2023, MetaQuotes Ltd. | //| https://www.MQL5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2023, MetaQuotes Ltd." #property link "https://www.MQL5.com" #include<UniformRandom.mqh> //+------------------------------------------------------------------+ //| Class to enable permuation of a collection of ticks in an array | //+------------------------------------------------------------------+ class CPermuteTicks { private : MqlTick m_ticks[]; //original tick data to be shuffled double m_differenced[]; //log difference of tick data bool m_initialized; //flag representing proper preparation of a dataset CUniFrand *m_random; public : //constructor CPermuteTicks(void); //desctrucotr ~CPermuteTicks(void); bool Initialize(MqlTick &in_ticks[]); bool Permute(MqlTick &out_ticks[]); }; //+------------------------------------------------------------------+ //| constructor | //+------------------------------------------------------------------+ CPermuteTicks::CPermuteTicks(void):m_initialized(false) { m_random = new CUniFrand(); m_random.SetSeed(MathRand()); } //+------------------------------------------------------------------+ //| destructor | //+------------------------------------------------------------------+ CPermuteTicks::~CPermuteTicks(void) { delete m_random; //---clean up ArrayFree(m_ticks); //--- ArrayFree(m_differenced); //--- } //+--------------------------------------------------------------------+ //|Initialize the permutation process by supplying ticks to be permuted| //+--------------------------------------------------------------------+ bool CPermuteTicks::Initialize(MqlTick &in_ticks[]) { //---check the random number object if(m_random==NULL) { Print("Critical internal error, failed to initialize random number generator"); return false; } //---set or reset initialization flag m_initialized=false; //---check arraysize if(in_ticks.Size()<5) { Print("Insufficient amount of data supplied "); return false; } //---copy ticks to local array if(ArrayCopy(m_ticks,in_ticks)!=int(in_ticks.Size())) { Print("Error copying ticks ", GetLastError()); return false; } //---ensure the size of m_differenced array if(m_differenced.Size()!=m_ticks.Size()-1) ArrayResize(m_differenced,m_ticks.Size()-1); //---fill m_differenced with differenced values, excluding the first tick for(uint i=1; i<m_ticks.Size() && !IsStopped(); i++) { m_differenced[i-1]=MathLog(m_ticks[i].bid/m_ticks[i-1].bid);//(m_logticks[i])-(m_logticks[i-1]); } //---set the initilization flag m_initialized=true; //--- return true; } //+------------------------------------------------------------------+ //|Public method which applies permutation and gets permuted ticks | //+------------------------------------------------------------------+ bool CPermuteTicks::Permute(MqlTick &out_ticks[]) { //---ensure required data already supplied through initialization if(!m_initialized) { Print("not initialized"); return false; } //--- int i,j; double tempvalue; i=(int)m_ticks.Size()-1; while(i>1 && !IsStopped()) { j=(int)(m_random.RandomDouble()*i); if(j>=i) j=i-1; --i; //---swap tick data randomly tempvalue=m_differenced[i]; m_differenced[i]=m_differenced[j]; m_differenced[j]=tempvalue; } //---- if(IsStopped()) return false; //---copy the first tick if(ArrayCopy(out_ticks,m_ticks)!=int(m_ticks.Size())) { Print(__FUNCTION__," array copy failure ", GetLastError()); return false; } //---apply exponential transform to data and copy original tick data member info //---not involved in permutation operations for(uint k = 1; k<m_ticks.Size() && !IsStopped(); k++) { out_ticks[k].bid=MathExp((MathLog(out_ticks[k-1].bid) + m_differenced[k-1]));//MathExp(m_logticks[k]); out_ticks[k].ask=out_ticks[k].bid + (m_ticks[k].ask - m_ticks[k].bid); } //--- if(IsStopped()) return false; else return true; } //+------------------------------------------------------------------+
CPermuteTicks 仍然以与以前版本相同的方式工作,CPermuteRates 也以类似的方式工作。两者之间的区别在于,一个适用于分时,而另一个则适用于报价
CPermutedSymbolData 类
脚本 PrepareSymbolsForPermutationTest 已更新,以反映对 CPermuteTicks 所做的更改和 CPermuteRates 的引入。脚本的功能包含在类 CPermutedSymbolData 中,它允许根据现有交易品种生成具有置换分时或报价的自定义交易品种。
//+------------------------------------------------------------------+ //|Permute rates or ticks of symbol | //+------------------------------------------------------------------+ enum ENUM_RATES_TICKS { ENUM_USE_RATES=0,//Use rates ENUM_USE_TICKS//Use ticks }; //+------------------------------------------------------------------+ //| defines:max number of data download attempts and array resize | //+------------------------------------------------------------------+ #define MAX_DOWNLOAD_ATTEMPTS 10 #define RESIZE_RESERVE 100 //+------------------------------------------------------------------+ //|CPermuteSymbolData class | //| creates custom symbols from an existing base symbol's data | //| symbols represent permutations of base symbol's data | //+------------------------------------------------------------------+ class CPermuteSymbolData { private: ENUM_RATES_TICKS m_use_rates_or_ticks;//permute either ticks or rates string m_basesymbol; //base symbol string m_symbols_id; //common identifier added to names of new symbols datetime m_datarangestart; //beginning date for range of base symbol's data datetime m_datarangestop; //ending date for range of base symbol's data uint m_permutations; //number of permutations and ultimately the number of new symbols to create MqlTick m_baseticks[]; //base symbol's tick MqlTick m_permutedticks[]; //permuted ticks; MqlRates m_baserates[]; //base symbol's rates MqlRates m_permutedrates[]; //permuted rates; CPermuteRates *m_rates_shuffler; //object used to shuffle rates CPermuteTicks *m_ticks_shuffler; //object used to shuffle ticks CNewSymbol *m_csymbols[]; //array of created symbols public: CPermuteSymbolData(const ENUM_RATES_TICKS mode); ~CPermuteSymbolData(void); bool Initiate(const string base_symbol,const string symbols_id,const datetime start_date,const datetime stop_date); uint Generate(const uint permutations); };
这是通过在构造函数调用中指定要打乱的数据类型(分时或报价)来实现的。枚举 ENUM_RATES_TICKS 描述了构造函数的单个参数可用的选项。
//+-----------------------------------------------------------------------------------------+ //|set and check parameters for symbol creation, download data and initialize data shuffler | //+-----------------------------------------------------------------------------------------+ bool CPermuteSymbolData::Initiate(const string base_symbol,const string symbols_id,const datetime start_date,const datetime stop_date) { //---reset number of permutations previously done m_permutations=0; //---set base symbol m_basesymbol=base_symbol; //---make sure base symbol is selected, ie, visible in WatchList if(!SymbolSelect(m_basesymbol,true)) { Print("Failed to select ", m_basesymbol," error ", GetLastError()); return false; } //---set symbols id m_symbols_id=symbols_id; //---check, set data date range if(start_date>=stop_date) { Print("Invalid date range "); return false; } else { m_datarangestart= start_date; m_datarangestop = stop_date; } //---download data Comment("Downloading data"); uint attempts=0; int downloaded=-1; while(attempts<MAX_DOWNLOAD_ATTEMPTS && !IsStopped()) { downloaded=(m_use_rates_or_ticks==ENUM_USE_TICKS)?CopyTicksRange(m_basesymbol,m_baseticks,COPY_TICKS_ALL,long(m_datarangestart)*1000,long(m_datarangestop)*1000):CopyRates(m_basesymbol,PERIOD_M1,m_datarangestart,m_datarangestop,m_baserates); if(downloaded<=0) { Sleep(500); ++attempts; } else break; } //---check download result if(downloaded<=0) { Print("Failed to download data for ",m_basesymbol," error ", GetLastError()); Comment(""); return false; } //Print(downloaded," Ticks downloaded ", " data start ",m_basedata[0].time, " data end ", m_basedata[m_basedata.Size()-1].time); //---return shuffler initialization result switch(m_use_rates_or_ticks) { case ENUM_USE_TICKS: { if(m_ticks_shuffler==NULL) m_ticks_shuffler=new CPermuteTicks(); return m_ticks_shuffler.Initialize(m_baseticks); } case ENUM_USE_RATES: { if(m_rates_shuffler==NULL) m_rates_shuffler=new CPermuteRates(); return m_rates_shuffler.Initialize(m_baserates); } default: return false; } }
一旦创建了CPermutedSymbolData的实例,就应该调用Initiate()方法来指定交易品种和日期周期,定义置换将基于的分时或报价。
//+------------------------------------------------------------------+ //| generate symbols return newly created or refreshed symbols | //+------------------------------------------------------------------+ uint CPermuteSymbolData::Generate(const uint permutations) { //---check permutations if(!permutations) { Print("Invalid parameter value for Permutations "); Comment(""); return 0; } //---resize m_csymbols if(m_csymbols.Size()!=m_permutations+permutations) ArrayResize(m_csymbols,m_permutations+permutations,RESIZE_RESERVE); //--- string symspath=m_basesymbol+m_symbols_id+"_PermutedData"; //int exists; //---do more permutations for(uint i=m_permutations; i<m_csymbols.Size() && !IsStopped(); i++) { if(CheckPointer(m_csymbols[i])==POINTER_INVALID) m_csymbols[i]=new CNewSymbol(); if(m_csymbols[i].Create(m_basesymbol+m_symbols_id+"_"+string(i+1),symspath,m_basesymbol)<0) continue; Comment("Processing Symbol "+m_basesymbol+m_symbols_id+"_"+string(i+1)); if(!m_csymbols[i].Clone(m_basesymbol) || (m_use_rates_or_ticks==ENUM_USE_TICKS && !m_ticks_shuffler.Permute(m_permutedticks)) || (m_use_rates_or_ticks==ENUM_USE_RATES && !m_rates_shuffler.Permute(m_permutedrates))) break; else { m_csymbols[i].Select(true); Comment("Adding permuted data"); if(m_use_rates_or_ticks==ENUM_USE_TICKS) m_permutations+=(m_csymbols[i].TicksReplace(m_permutedticks)>0)?1:0; else m_permutations+=(m_csymbols[i].RatesUpdate(m_permutedrates)>0)?1:0; } } //---return successfull number of permutated symbols Comment(""); //--- if(IsStopped()) return 0; //--- return m_permutations; } //+------------------------------------------------------------------+
如果Initiate()返回true,则可以调用具有所需置换数的Generate()方法。该方法将返回自定义交易品种的计数,这些交易品种的数据已成功地用置换的分时或报价进行了补充。
//+------------------------------------------------------------------+ //| PrepareSymbolsForPermutationTests.mq5 | //| Copyright 2023, MetaQuotes Ltd. | //| https://www.MQL5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2023, MetaQuotes Ltd." #property link "https://www.MQL5.com" #property version "1.00" #include<PermutedSymbolData.mqh> #property script_show_inputs //--- input parameters input string BaseSymbol="EURUSD"; input ENUM_RATES_TICKS PermuteRatesOrTicks=ENUM_USE_RATES; input datetime StartDate=D'2022.01.01 00:00'; input datetime EndDate=D'2023.01.01 00:00'; input uint Permutations=100; input string CustomID="_p";//SymID to be added to symbol permutation names //--- CPermuteSymbolData *symdata; //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { ulong startime = GetTickCount64(); uint permutations_completed=0; // number of successfully added permuted data //---intialize the permuted symbol object symdata = new CPermuteSymbolData(PermuteRatesOrTicks); //---set the properties of the permuted symbol object if(symdata.Initiate(BaseSymbol,CustomID,StartDate,EndDate)) permutations_completed = symdata.Generate(Permutations); // do the permutations //---print number of symbols whose bar or tick data has been replenished. Print("Number of permuted symbols is ", permutations_completed, ", Runtime ",NormalizeDouble(double(GetTickCount64()-startime)/double(60000),1),"mins"); //---clean up delete symdata; } //+------------------------------------------------------------------+
上面是脚本的代码。所有源代码都附在文章后面。
应用置换测试
在文章的引言中,我们谈到了许多想要购买 EA 交易的人面临的一个常见问题。无良卖家可能会采用欺骗手段来推销他们的产品。卖家通常会展示具有吸引力的净值曲线来代表潜在利润。许多人成为了这些策略的受害者,并艰难地了解到这些截图是由诱导策略产生的。在本节中,我们将了解代码库中一个臭名昭著的EA,该EA可用于生成误导性的净值曲线。并应用置换测试来揭开骗局。
置换测试概述
应该注意的是,这种测试相当费力,并且需要大量的时间和计算资源。不过,在我看来,结果非常值得努力,可以避免人们做出糟糕的决定。所采用的方法需要选择合适的样本进行测试。将此样本分离为样本内数据集和样本外数据集。EA将在样本内数据上进行优化,并在使用优化参数对样本外数据进行测试时记录最终性能。这是使用原始数据序列以及至少100个置换的数据集来完成的。这正是为了测试我们演示中使用的EA所做的。
测试 grr-al EA
任何研究过MQL5文档或探索过代码库的人都一定遇到过这个EA。在MQL5文档中,它被描述为“测试圣杯”。当它在策略测试器中以1分钟OHLC或仅开盘价勾选生成模式运行时,会生成具有令人印象深刻的轮廓的净值曲线。这是我们将在演示中使用的EA。我们稍微修改了代码,以暴露一些全局变量进行优化。三个参数中的两个被选择用于优化,即SL、止损和DELTA。
#define MAGIC_NUMBER 12937 #define DEV 20 #define RISK 0.0 #define BASELOT 0.1 input double DELTA =30; input double SL =700; input double TP =100;
用于优化的设置如图所示。
选定的数据集是2022年全年小时图表上的 EURUSD 数据集。2022年的前六个月用于优化,后半年用作测试最佳参数的样本外期。
首先,脚本PrepareSymbolsForPermutationsTests用于生成置换数据的自定义交易品种。程序运行的计时和记录如下所示。错误代码是因为我第一次尝试时驱动器空间不足,只成功添加了99个自定义交易品种。
PR 0 11:53:04.548 PrepareSymbolsForPermutationTests (EURUSD,MN1) CNewSymbol::TicksReplace: failed to replace ticks! Error code: 5310 EL 0 11:53:04.702 PrepareSymbolsForPermutationTests (EURUSD,MN1) Number of permuted symbols is 99, Runtime 48.9mins
生成的数据量几乎是40G的分时数据,一年的数据置换了100次!
出于我们的兴趣考虑,使用报价要快得多,占用的空间也要小得多。
NK 0 12:51:23.166 PrepareSymbolsForPermutationTests (EURUSD,M1) Number of permuted symbols is 100, Runtime 1.4mins
使用这些数据,在样本集中对每个交易品种进行了优化。
在样本外测试中使用了产生最大绝对利润的参数。优化和样本外测试是使用开盘价分时模式进行的。这意味着EA完全有优势大放异彩。
所有测试的结果显示在下面的csv文件中。IS利润和OOS利润分别是样本内利润和样本外利润
<SYMBOL> <OPTIMAL DELTA> <OPTIMAL SL> <IS PROFIT> <OOS PROFIT> EURUSD 3.00 250.00 31995.60 32347.20 EURUSD_p_1 3.00 50.00 29283.40 34168.20 EURUSD_p_2 5.00 50.00 32283.50 21047.60 EURUSD_p_3 3.00 20.00 33696.20 34915.30 EURUSD_p_4 3.00 20.00 32589.30 38693.20 EURUSD_p_5 3.00 230.00 33771.10 40458.20 EURUSD_p_6 3.00 40.00 30899.10 34061.50 EURUSD_p_7 3.00 250.00 34309.10 31861.20 EURUSD_p_8 3.00 40.00 33729.00 35359.90 EURUSD_p_9 3.00 300.00 36027.90 38174.50 EURUSD_p_10 3.00 30.00 33405.90 35693.70 EURUSD_p_11 3.00 30.00 32723.30 36453.00 EURUSD_p_12 11.00 300.00 34191.20 34277.80 EURUSD_p_13 3.00 130.00 35029.70 33930.00 EURUSD_p_14 11.00 290.00 33924.40 34851.70 EURUSD_p_15 3.00 140.00 33920.50 32263.20 EURUSD_p_16 3.00 20.00 34388.00 33694.40 EURUSD_p_17 3.00 60.00 35081.70 35612.20 EURUSD_p_18 5.00 70.00 36830.00 40442.30 EURUSD_p_19 3.00 170.00 37693.70 37404.90 EURUSD_p_20 3.00 50.00 31265.30 34875.10 EURUSD_p_21 3.00 20.00 30248.10 38426.00 EURUSD_p_22 5.00 250.00 32369.80 37263.80 EURUSD_p_23 7.00 50.00 31197.50 35466.40 EURUSD_p_24 7.00 30.00 26252.20 34963.10 EURUSD_p_25 3.00 20.00 31343.90 37156.00 EURUSD_p_26 25.00 280.00 29762.10 27336.10 EURUSD_p_27 3.00 60.00 33775.10 37034.60 EURUSD_p_28 3.00 260.00 35341.70 36744.20 EURUSD_p_29 5.00 50.00 31775.80 34673.60 EURUSD_p_30 3.00 20.00 32520.30 37907.10 EURUSD_p_31 3.00 230.00 35481.40 42938.20 EURUSD_p_32 3.00 100.00 32862.70 38291.70 EURUSD_p_33 3.00 190.00 36511.70 26714.30 EURUSD_p_34 3.00 290.00 29809.10 35312.40 EURUSD_p_35 3.00 290.00 34044.60 33460.00 EURUSD_p_36 3.00 90.00 32203.10 35730.90 EURUSD_p_37 3.00 180.00 39506.50 30947.30 EURUSD_p_38 3.00 180.00 35844.90 41717.30 EURUSD_p_39 3.00 90.00 30602.30 35390.10 EURUSD_p_40 3.00 250.00 29592.20 33025.90 EURUSD_p_41 3.00 140.00 34281.80 31501.40 EURUSD_p_42 3.00 30.00 34235.70 39422.40 EURUSD_p_43 3.00 170.00 35580.10 35994.20 EURUSD_p_44 3.00 20.00 34400.60 36250.50 EURUSD_p_45 5.00 190.00 35942.70 31068.30 EURUSD_p_46 3.00 20.00 32560.60 37114.70 EURUSD_p_47 3.00 200.00 36837.30 40843.10 EURUSD_p_48 3.00 20.00 29188.30 33418.10 EURUSD_p_49 3.00 40.00 33985.60 29720.50 EURUSD_p_50 3.00 250.00 36849.00 38007.00 EURUSD_p_51 3.00 50.00 33867.90 39323.30 EURUSD_p_52 3.00 120.00 33066.30 39852.40 EURUSD_p_53 3.00 60.00 36977.30 37284.40 EURUSD_p_54 3.00 20.00 29990.30 35975.70 EURUSD_p_55 15.00 70.00 29872.80 34179.40 EURUSD_p_56 3.00 250.00 35909.60 35911.50 EURUSD_p_57 3.00 200.00 37642.70 34849.80 EURUSD_p_58 3.00 290.00 39164.00 35440.90 EURUSD_p_59 3.00 100.00 28312.70 33917.80 EURUSD_p_60 3.00 60.00 28141.60 38826.00 EURUSD_p_61 3.00 50.00 29670.90 34973.70 EURUSD_p_62 3.00 40.00 32170.80 31062.60 EURUSD_p_63 3.00 260.00 28312.80 29236.50 EURUSD_p_64 3.00 20.00 31632.50 35458.30 EURUSD_p_65 3.00 260.00 35345.20 38522.70 EURUSD_p_66 7.00 270.00 31077.60 34531.10 EURUSD_p_67 3.00 90.00 33893.70 30969.00 EURUSD_p_68 3.00 170.00 34118.70 37280.50 EURUSD_p_69 3.00 40.00 33867.50 35256.20 EURUSD_p_70 3.00 180.00 37710.60 30337.20 EURUSD_p_71 5.00 200.00 40851.10 40985.60 EURUSD_p_72 3.00 20.00 29258.40 31194.70 EURUSD_p_73 3.00 20.00 30956.50 38021.40 EURUSD_p_74 3.00 90.00 35807.40 32625.70 EURUSD_p_75 3.00 260.00 32801.10 36161.70 EURUSD_p_76 3.00 260.00 34825.40 28957.70 EURUSD_p_77 3.00 90.00 39725.80 35923.00 EURUSD_p_78 3.00 180.00 37880.80 37090.90 EURUSD_p_79 3.00 180.00 34191.50 38190.70 EURUSD_p_80 3.00 40.00 29235.30 33207.70 EURUSD_p_81 3.00 20.00 29923.50 34291.00 EURUSD_p_82 3.00 90.00 35077.80 37203.40 EURUSD_p_83 3.00 40.00 32901.50 32182.40 EURUSD_p_84 3.00 50.00 31302.60 34339.00 EURUSD_p_85 3.00 60.00 30336.90 37948.10 EURUSD_p_86 5.00 50.00 35166.10 37898.60 EURUSD_p_87 5.00 290.00 33005.20 32648.30 EURUSD_p_88 7.00 140.00 34349.70 31435.50 EURUSD_p_89 3.00 20.00 30680.20 37002.30 EURUSD_p_90 3.00 100.00 35382.50 37643.80 EURUSD_p_91 3.00 50.00 35187.20 36392.00 EURUSD_p_92 3.00 120.00 32423.10 35943.20 EURUSD_p_93 3.00 100.00 31722.70 39913.30 EURUSD_p_94 11.00 300.00 31548.40 32684.70 EURUSD_p_95 3.00 100.00 30094.00 38929.70 EURUSD_p_96 3.00 170.00 35400.30 29260.30 EURUSD_p_97 3.00 300.00 35696.50 35772.20 EURUSD_p_98 3.00 20.00 31336.20 35935.70 EURUSD_p_99 3.00 20.00 32466.30 39986.40 EURUSD_p_100 3.00 20.00 32082.40 33625.10
计算出的p值为08217821782178217。
MO 0 09:49:57.991 ProcessOptFiles (EURUSD,MN1) P值为 0.8217821782178217
这表明,原始数据集上观察到高性能是因为幸运的概率超过80%。这清楚地表明这个EA毫无价值。
为什么这样做?
在策略开发的背景下进行策略测试的前提是,EA策略是对用于在交易中获得优势的模式或规则集的描述。当它处理的数据被置换时,原本可以从中获利的原始模式就会被破坏。如果EA确实基于某种模式进行交易,那么它在置换过的数据上的性能将受到影响。当比较置换测试和未置换测试的性能时,很明显,即使在优化之后,EA实际上也依赖于一些独特的模式或规则。未置换数据集的性能应在置换测试中脱颖而出
正如我们从演示的测试中看到的那样,众所周知,有问题的EA利用了分时生成的方法,并且不使用任何真正的策略(模式或规则)。置换测试能够揭示这一点。
置换测试也可以用于给出优化后过拟合程度的指示。为了测试过拟合,我们需要测试和比较置换和未置换数据集的样本内性能。未置换性能数据与置换结果的差异程度可用于量化过拟合。当过拟合普遍存在时,置换和未置换的性能结果之间却几乎没有差异,我们会看到相当大的p值。
结论
我们看到了一种用于置换价格柱的算法的实现。以及用于生成具有置换分时或柱形的自定义交易品种的更新代码。所描述的程序用于置换测试一个伪装正向收益的 EA 交易。置换测试对于任何对自动交易感兴趣的人来说都是一个必不可少的工具。因此,我认为它应该作为MT5策略测试程序的一个功能添加。
文件 | 描述 |
---|---|
MQL5\Experts\grr-al.mq5 | 这是MQL5.com代码库中提供的EA的一个稍作修改的版本,它通过在1分钟OHLC模式下利用策略测试器的分时生成方法进行交易。 |
MQL5\Include\NewSymbol.mqh | 包含用于创建自定义交易品种的CNewSymbol类定义 |
MQL5\Include\ PermutedSymbolData.mqh | 定义CPermutedSymbolData类,用于创建具有置换价格或分时的自定义交易品种 |
MQL5\Include\PermuteRates.mqh | 包含CPermuteRates类,用于创建MqlRates数据数组的置换 |
MQL5\Include\PermuteTicks.mqh | 定义CPermuteTicks类,用于创建MqlTick数据数组的置换 |
MQL5\Include\UniformRandom.mqh | CUniFrand封装了一个均匀分布的随机数生成器 |
MQL5\Scripts\PrepareSymbolsForPermutationTests.mq5 | 这是一个脚本,它将所有代码实用程序绑定到一起,以在MetaTrader 5中生成自定义交易品种 |
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/13591



