MetaTrader 5中的蒙特卡罗置换测试
概述
Aleksey Nikolayev 写过一篇有趣的文章,题为“蒙特卡洛方法在交易策略优化中的应用”。它描述了一种置换测试方法,其中测试中的交易序列被随机转换。作者简要提到了另一种类型的置换测试,其中价格数据的序列是随机变化的,并且将单个EA交易的性能与在相同价格序列的许多其他序列变化上测试时获得的性能进行比较。
在我看来,作者错误地认为,没有办法使用MetaTrader 5对任意EA进行这样的测试。至少不完全是这样。因此,在本文中,我们将使用MetaTrader 5演示一个涉及随机排列价格数据的置换测试。我们将提供用于置换价格序列的代码,以及一个脚本,该脚本在准备进行完整EA的置换测试时自动执行初始步骤。
置换测试概述
简单地说,我们将描述的置换测试类型包括选择价格数据的样本。最好在样品之外进行试验。在对这个价格系列进行测试后,我们会记录下我们可能感兴趣的任何性能标准。然后我们随机改变原始价格序列的顺序,测试EA并记录性能。
我们多次这样做,每次都会置换价格序列,并记录我们为其他测试记录的最终性能标准。这应该至少做一百次,最好是数千次。我们置换和测试的次数越多,结果就越稳健。但是,等等,我们希望我们的结果能揭示出关于正在测试的EA的什么?
进行置换测试的价值
当进行了大量的迭代测试后,我们最终会得到每个排列的性能数据集合。我们使用什么性能数据并不重要,它可能是夏普比率、利润因子,也可能只是由此产生的余额或净利润。假设已经进行了99次置换,包括100次原始未置换测试。我们有100个性能数据可供比较。
下一步是列举未置换测试的性能数字被超过的次数,并将该数字作为所进行测试的一个分数,在本例中为100。这个分数是在EA根本没有盈利潜力的情况下,偶然获得未置换测试结果或更好结果的概率。在统计学中,它被称为p值,是进行假设检验的结果。
继续我们假设的100次迭代的置换测试,结果发现正好有29个置换的性能数据比基准的未置换测试要好。我们得到的p值为0.3,即29+1/100。这意味着赔钱的EA获得与未置换测试操作类似或更好性能的概率为0.3。这样的结果可能看起来令人鼓舞,但我们想要的是尽可能接近零的p值——在0.05及以下的范围内。
完整的公式如下:
z+1/r+1
其中r是完成的置换次数,z是具有更好性能的置换测试的总数。为了正确地进行测试,置换过程很重要。
调整价格系列
为了正确地排列一组数据,我们必须确保每一个可能的序列变化都是同样可能的。这需要生成一个在0和1之间均匀分布的随机数。mql5标准库在统计库中提供了一个满足此需求的工具。使用它,我们可以指定所需值的范围。
//+------------------------------------------------------------------+ //| Random variate from the Uniform distribution | //+------------------------------------------------------------------+ //| Computes the random variable from the Uniform distribution | //| with parameters a and b. | //| | //| Arguments: | //| a : Lower endpoint (minimum) | //| b : Upper endpoint (maximum) | //| error_code : Variable for error code | //| | //| Return value: | //| The random value with uniform distribution. | //+------------------------------------------------------------------+ double MathRandomUniform(const double a,const double b,int &error_code) { //--- check NaN if(!MathIsValidNumber(a) || !MathIsValidNumber(b)) { error_code=ERR_ARGUMENTS_NAN; return QNaN; } //--- check upper bound if(b<a) { error_code=ERR_ARGUMENTS_INVALID; return QNaN; } error_code=ERR_OK; //--- check ranges if(a==b) return a; //--- return a+MathRandomNonZero()*(b-a); }
修改价格数据有其独特的需求。首先,我们不能简单地改变价格值的位置,因为这会扰乱金融时间序列的时间关系特征。因此,我们将调整价格变化,而不是实际价格。通过在调整之前首先对价格进行对数转换,我们将原始价格差异变化的影响降至最低。
使用这种方法,我们必须保留第一个价格值,并将其从排列中排除。当序列被重构时,结果将是原始价格序列中存在的趋势的保留。唯一的变化是原始系列相同的第一个和最后一个价格之间的内部价格变动。
在实际排列价格系列之前,我们必须决定我们将使用什么数据。在MetaTrader 5中,图表数据显示为由分时数据构建的条形图。置换单个价格系列比置换柱形图信息容易得多。因此,我们将使用分时数据。使用分时还会带来许多其他复杂情况,因为分时除了原始价格之外还包括其他信息。有关于交易量、时间和分时标志的信息。
首先,时间和分时标志信息将保持不变,因此我们的置换例程不应更改此信息。我们只对报价、询价和交易量感兴趣。第二个复杂情况是,这些值中的任何一个都可能为零,这将在对其应用日志转换时造成问题。为了演示如何克服这些挑战,让我们看一些代码。
分时置换算法的实现
包含文件PermuteTicks.mqh中包含的CPermuteTicks类实现了我们的分时置换过程。在PermuteTicks.mqh中,我们包含了标准库中的Uniform.mqh,以访问一个实用程序,该实用程序可以在一个设置范围内输出统一生成的随机数。随后的定义指定了这个范围,如果您觉得需要更改这些值,请小心,确保最小值实际上小于最大阈值。
//+------------------------------------------------------------------+ //| PermuteTicks.mqh | //| Copyright 2023, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2023, MetaQuotes Ltd." #property link "https://www.mql5.com" #include<Math\Stat\Uniform.mqh> //+-----------------------------------------------------------------------------------+ //| defines: representing range of random values from random number generator | //+-----------------------------------------------------------------------------------+ #define MIN_THRESHOLD 1e-5 #define MAX_THRESHOLD 1.0
CMqlTick结构表示类将操作的内置MqlTick架构的相应成员。其他分时信息将不会被改变。
//+------------------------------------------------------------------+ //| struct to handle tick data to be worked on | //+------------------------------------------------------------------+ struct CMqlTick { double ask_d; double bid_d; double vol_d; double volreal_d; };
CPermuteTicks类有3个私有数组属性,用于存储:首先是保存在m_ticks中的原始分时,其次是保存在m_logticks中经过日志转换的分时,最后是在m_differenced中收集的差分分时。
//+------------------------------------------------------------------+ //| Class to enable permutation of a collection of ticks in an array | //+------------------------------------------------------------------+ class CPermuteTicks { private : MqlTick m_ticks[]; //original tick data to be shuffled CMqlTick m_logticks[]; //log transformed tick data of original ticks CMqlTick m_differenced[]; //log difference of tick data bool m_initialized; //flag representing proper preparation of a dataset //helper methods bool LogTransformTicks(void); bool ExpTransformTicks(MqlTick &out_ticks[]); public : //constructor CPermuteTicks(void); //desctrucotr ~CPermuteTicks(void); bool Initialize(MqlTick &in_ticks[]); bool Permute(MqlTick &out_ticks[]); };
m_initialized是一个布尔型标志,表示在进行置换之前预处理操作成功。
若要使用该类,用户必须在创建对象的实例后调用Initialize()方法。该方法需要一个要置换的分时数组。在方法内部,调整不可访问的类数组的大小,并使用LogTranformTicks()来转换分时数据。这是通过确保避免零值或负值,将其替换为1.0来完成的。一旦完成置换并通过ExpTransformTicks()私有方法将转换后的分时数据返回存储到其原始域。
//+--------------------------------------------------------------------+ //|Initialize the permutation process by supplying ticks to be permuted| //+--------------------------------------------------------------------+ bool CPermuteTicks::Initialize(MqlTick &in_ticks[]) { //---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); //---apply log transformation to relevant tick data members if(!LogTransformTicks()) { Print("Log transformation failed ", GetLastError()); return false; } //---fill m_differenced with differenced values, excluding the first tick for(uint i=1; i<m_logticks.Size(); i++) { m_differenced[i-1].bid_d=(m_logticks[i].bid_d)-(m_logticks[i-1].bid_d); m_differenced[i-1].ask_d=(m_logticks[i].ask_d)-(m_logticks[i-1].ask_d); m_differenced[i-1].vol_d=(m_logticks[i].vol_d)-(m_logticks[i-1].vol_d); m_differenced[i-1].volreal_d=(m_logticks[i].volreal_d)-(m_logticks[i-1].volreal_d); } //---set the initilization flag m_initialized=true; //--- return true; }
要输出置换的分时,应该调用名称恰当的方法Permute()。它有一个动态MqlTick数组的单一参数要求,排列后的分时将放置在该数组中。这就是分时混洗过程所在的位置,在while循环中,该循环根据每次迭代生成的随机数交换差分分时值的位置。
//+------------------------------------------------------------------+ //|Public method which applies permutation and gets permuted ticks | //+------------------------------------------------------------------+ bool CPermuteTicks::Permute(MqlTick &out_ticks[]) { //---zero out tick array ZeroMemory(out_ticks); //---ensure required data already supplied through initialization if(!m_initialized) { Print("not initialized"); return false; } //---resize output array if necessary if(out_ticks.Size()!=m_ticks.Size()) ArrayResize(out_ticks,m_ticks.Size()); //--- int i,j; CMqlTick tempvalue; i=(int)m_ticks.Size()-1; int error_value; double unif_rando; ulong time = GetTickCount64(); while(i>1) { error_value=0; unif_rando=MathRandomUniform(MIN_THRESHOLD,MAX_THRESHOLD,error_value); if(!MathIsValidNumber(unif_rando)) { Print("Invalid random value ",error_value); return(false); } j=(int)(unif_rando*i); if(j>=i) j=i-1; --i; //---swap tick data randomly tempvalue.bid_d=m_differenced[i].bid_d; tempvalue.ask_d=m_differenced[i].ask_d; tempvalue.vol_d=m_differenced[i].vol_d; tempvalue.volreal_d=m_differenced[i].volreal_d; m_differenced[i].bid_d=m_differenced[j].bid_d; m_differenced[i].ask_d=m_differenced[j].ask_d; m_differenced[i].vol_d=m_differenced[j].vol_d; m_differenced[i].volreal_d=m_differenced[j].volreal_d; m_differenced[j].bid_d=tempvalue.bid_d; m_differenced[j].ask_d=tempvalue.ask_d; m_differenced[j].vol_d=tempvalue.vol_d; m_differenced[j].volreal_d=tempvalue.volreal_d; } //---undo differencing for(uint k = 1; k<m_ticks.Size(); k++) { m_logticks[k].bid_d=m_logticks[k-1].bid_d + m_differenced[k-1].bid_d; m_logticks[k].ask_d=m_logticks[k-1].ask_d + m_differenced[k-1].ask_d; m_logticks[k].vol_d=m_logticks[k-1].vol_d + m_differenced[k-1].vol_d; m_logticks[k].volreal_d=m_logticks[k-1].volreal_d + m_differenced[k-1].volreal_d; } //---copy the first tick out_ticks[0].bid=m_ticks[0].bid; out_ticks[0].ask=m_ticks[0].ask; out_ticks[0].volume=m_ticks[0].volume; out_ticks[0].volume_real=m_ticks[0].volume_real; out_ticks[0].flags=m_ticks[0].flags; out_ticks[0].last=m_ticks[0].last; out_ticks[0].time=m_ticks[0].time; out_ticks[0].time_msc=m_ticks[0].time_msc; //---return transformed data return ExpTransformTicks(out_ticks); } //+------------------------------------------------------------------+
完成所有迭代后,将通过使用置换的 m_differenced 分时数据撤消差分来重建m_logticks数组。最后,Permute()方法的唯一参数填充返回到其原始域的m_logtick数据,以及从原始分时序列复制的时间和分时标志信息。
//+-------------------------------------------------------------------+ //|Helper method applying log transformation | //+-------------------------------------------------------------------+ bool CPermuteTicks::LogTransformTicks(void) { //---resize m_logticks if necessary if(m_logticks.Size()!=m_ticks.Size()) ArrayResize(m_logticks,m_ticks.Size()); //---log transform only relevant data members, avoid negative and zero values for(uint i=0; i<m_ticks.Size(); i++) { m_logticks[i].bid_d=(m_ticks[i].bid>0)?MathLog(m_ticks[i].bid):MathLog(1e0); m_logticks[i].ask_d=(m_ticks[i].ask>0)?MathLog(m_ticks[i].ask):MathLog(1e0); m_logticks[i].vol_d=(m_ticks[i].volume>0)?MathLog(m_ticks[i].volume):MathLog(1e0); m_logticks[i].volreal_d=(m_ticks[i].volume_real>0)?MathLog(m_ticks[i].volume_real):MathLog(1e0); } //--- return true; } //+-----------------------------------------------------------------------+ //|Helper method undoes log transformation before outputting permuted tick| //+-----------------------------------------------------------------------+ bool CPermuteTicks::ExpTransformTicks(MqlTick &out_ticks[]) { //---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(); k++) { out_ticks[k].bid=(m_logticks[k].bid_d)?MathExp(m_logticks[k].bid_d):0; out_ticks[k].ask=(m_logticks[k].ask_d)?MathExp(m_logticks[k].ask_d):0; out_ticks[k].volume=(m_logticks[k].vol_d)?(ulong)MathExp(m_logticks[k].vol_d):0; out_ticks[k].volume_real=(m_logticks[k].volreal_d)?MathExp(m_logticks[k].volreal_d):0; out_ticks[k].flags=m_ticks[k].flags; out_ticks[k].last=m_ticks[k].last; out_ticks[k].time=m_ticks[k].time; out_ticks[k].time_msc=m_ticks[k].time_msc; } //--- return true; }
我们现在有了一种处理价格序列排列的算法,可以说,这只是战斗的一半,我们还需要做测试。
置换测试程序
置换测试程序将利用MetaTrader 5终端的两个功能。第一个是能够创建自定义交易品种并指定其特性。第二个是根据市场观察列表中启用的交易品种优化EA的能力。因此,整个过程至少还有两个步骤。
我们可以置换分时,并创建自定义交易品种,将这些放在一起,我们可以基于任何现有交易品种生成自定义交易品种。每个自定义交易品种都以交易品种的唯一置换分时为基础进行指定。创建交易品种可以手动完成,然而,当我们可以自动完成交易品种创建和添加置换分时的整个任务时,我们为什么要惩罚自己呢。
脚本PrepareSymbolsForPermutationTests正是这样做的。其用户指定的输入允许设置基本交易品种、置换中使用的基本交易品种的分时的日期范围、与将创建的自定义交易品种的数量相对应的所需置换的数量以及将附加到新自定义交易品种的名称的可选字符串标识符。//+------------------------------------------------------------------+ //| 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<GenerateSymbols.mqh> #property script_show_inputs //--- input parameters input string BaseSymbol="EURUSD"; input datetime StartDate=D'2023.06.01 00:00'; input datetime EndDate=D'2023.08.01 00:00'; input uint Permutations=100; input string CustomID="";//SymID to be added to symbol permutation names //--- CGenerateSymbols generateSymbols(); //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- if(!generateSymbols.Initiate(BaseSymbol,CustomID,StartDate,EndDate)) return; //--- Print("Number of newly generated symbols is ", generateSymbols.Generate(Permutations)); //--- } //+------------------------------------------------------------------+
脚本使用基本交易品种名称自动创建交易品种名称,并在末尾添加枚举。完成这一切的代码包含在GenerateSymbols.mqh中,该代码包含CGenerateSymbols类的定义。类定义依赖于另外两个依赖项:NewSymbol.mqh,其中包含CNewSymbol类的定义,该类改编自文章“MQL5 酷客宝典:使用自定义交易品种的交易策略压力测试”中的代码。
//+------------------------------------------------------------------+ //| Class CNewSymbol. | //| Purpose: Base class for a custom symbol. | //+------------------------------------------------------------------+ class CNewSymbol : public CObject { //--- === Data members === --- private: string m_name; string m_path; MqlTick m_tick; ulong m_from_msc; ulong m_to_msc; uint m_batch_size; bool m_is_selected; //--- === Methods === --- public: //--- constructor/destructor void CNewSymbol(void); void ~CNewSymbol(void) {}; //--- create/delete int Create(const string _name,const string _path="",const string _origin_name=NULL, const uint _batch_size=1e6,const bool _is_selected=false); bool Delete(void); //--- methods of access to protected data string Name(void) const { return(m_name); } bool RefreshRates(void); //--- fast access methods to the integer symbol properties bool Select(void) const; bool Select(const bool select); //--- service methods bool Clone(const string _origin_symbol,const ulong _from_msc=0,const ulong _to_msc=0); bool LoadTicks(const string _src_file_name); //--- API bool SetProperty(ENUM_SYMBOL_INFO_DOUBLE _property,double _val) const; bool SetProperty(ENUM_SYMBOL_INFO_INTEGER _property,long _val) const; bool SetProperty(ENUM_SYMBOL_INFO_STRING _property,string _val) const; double GetProperty(ENUM_SYMBOL_INFO_DOUBLE _property) const; long GetProperty(ENUM_SYMBOL_INFO_INTEGER _property) const; string GetProperty(ENUM_SYMBOL_INFO_STRING _property) const; bool SetSessionQuote(const ENUM_DAY_OF_WEEK _day_of_week,const uint _session_index, const datetime _from,const datetime _to); bool SetSessionTrade(const ENUM_DAY_OF_WEEK _day_of_week,const uint _session_index, const datetime _from,const datetime _to); int RatesDelete(const datetime _from,const datetime _to); int RatesReplace(const datetime _from,const datetime _to,const MqlRates &_rates[]); int RatesUpdate(const MqlRates &_rates[]) const; int TicksAdd(const MqlTick &_ticks[]) const; int TicksDelete(const long _from_msc,long _to_msc) const; int TicksReplace(const MqlTick &_ticks[]) const; //--- private: template<typename PT> bool CloneProperty(const string _origin_symbol,const PT _prop_type) const; int CloneTicks(const MqlTick &_ticks[]) const; int CloneTicks(const string _origin_symbol) const; };
该类帮助在现有交易品种的基础上创建新的自定义交易品种。最后一个需要的依赖项是我们已经遇到的PermuteTicks.mqh。
//+------------------------------------------------------------------+ //| GenerateSymbols.mqh | //| Copyright 2023, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2023, MetaQuotes Ltd." #property link "https://www.mql5.com" #include<PermuteTicks.mqh> #include<NewSymbol.mqh> //+------------------------------------------------------------------+ //| defines:max number of ticks download attempts and array resize | //+------------------------------------------------------------------+ #define MAX_DOWNLOAD_ATTEMPTS 10 #define RESIZE_RESERVE 100 //+------------------------------------------------------------------+ //|CGenerateSymbols class | //| creates custom symbols from an existing base symbol's tick data | //| symbols represent permutations of base symbol's ticks | //+------------------------------------------------------------------+ class CGenerateSymbols { private: string m_basesymbol; //base symbol string m_symbols_id; //common identifier added to names of new symbols long m_tickrangestart; //beginning date for range of base symbol's ticks long m_tickrangestop; //ending date for range of base symbol's ticks uint m_permutations; //number of permutations and ultimately the number of new symbols to create MqlTick m_baseticks[]; //base symbol's ticks MqlTick m_permutedticks[];//permuted ticks; CNewSymbol *m_csymbols[]; //array of created symbols CPermuteTicks *m_shuffler; //object used to shuffle tick data public: CGenerateSymbols(void); ~CGenerateSymbols(void); bool Initiate(const string base_symbol,const string symbols_id,const datetime start_date,const datetime stop_date); uint Generate(const uint permutations); };
CGenerateSymbols有两个用户需要注意的成员函数。Initiate()方法应该在对象创建后首先调用,它有4个参数,与前面提到的脚本的用户输入相对应。
//+-----------------------------------------------------------------------------------------+ //|set and check parameters for symbol creation, download ticks and initialize tick shuffler| //+-----------------------------------------------------------------------------------------+ bool CGenerateSymbols::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 ticks date range if(start_date>=stop_date) { Print("Invalid date range "); return false; } else { m_tickrangestart=long(start_date)*1000; m_tickrangestop=long(stop_date)*1000; } //---check shuffler object if(CheckPointer(m_shuffler)==POINTER_INVALID) { Print("CPermuteTicks object creation failed"); return false; } //---download ticks Comment("Downloading ticks"); uint attempts=0; int downloaded=-1; while(attempts<MAX_DOWNLOAD_ATTEMPTS) { downloaded=CopyTicksRange(m_basesymbol,m_baseticks,COPY_TICKS_ALL,m_tickrangestart,m_tickrangestop); if(downloaded<=0) { Sleep(500); ++attempts; } else break; } //---check download result if(downloaded<=0) { Print("Failed to get tick data for ",m_basesymbol," error ", GetLastError()); return false; } Comment("Ticks downloaded"); //---return shuffler initialization result return m_shuffler.Initialize(m_baseticks); }
Generate()方法将所需的置换数量作为输入,并返回添加到终端的市场观察中的新自定义交易品种的数量。
运行脚本的结果将显示在终端的“专家”选项卡中。
//+------------------------------------------------------------------+ //| generate symbols return newly created or refreshed symbols | //+------------------------------------------------------------------+ uint CGenerateSymbols::Generate(const uint permutations) { //---check permutations if(!permutations) { Print("Invalid parameter value for Permutations "); 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+"_PermutedTicks"; int exists; //---do more permutations for(uint i=m_permutations; i<m_csymbols.Size(); i++) { if(CheckPointer(m_csymbols[i])==POINTER_INVALID) m_csymbols[i]=new CNewSymbol(); exists=m_csymbols[i].Create(m_basesymbol+m_symbols_id+"_"+string(i+1),symspath,m_basesymbol); if(exists>0) { Comment("new symbol created "+m_basesymbol+m_symbols_id+"_"+string(i+1) ); if(!m_csymbols[i].Clone(m_basesymbol) || !m_shuffler.Permute(m_permutedticks)) break; else { m_csymbols[i].Select(true); Comment("adding permuted ticks"); if(m_csymbols[i].TicksAdd(m_permutedticks)>0) m_permutations++; } } else { Comment("symbol exists "+m_basesymbol+m_symbols_id+"_"+string(i+1) ); m_csymbols[i].Select(true); if(!m_shuffler.Permute(m_permutedticks)) break; Comment("replacing ticks "); if(m_csymbols[i].TicksReplace(m_permutedticks)>0) m_permutations++; else break; } } //---return successful number of permutated symbols Comment(""); //--- return m_permutations; }
下一步是在策略测试器中运行优化,确保选择最后一种优化方法并指定要测试的EA。开始测试,找点事情做一段时间,因为这可能需要很长时间。当策略测试完成后,我们有一组性能数据可以深入研究。
示例
让我们看看通过使用系统自带的MACD示例EA运行测试来完成这一切是什么样子的。测试将在脚本中设置了100个置换的AUDUSD交易品种上运行。
运行脚本后,我们有了100个额外的交易品种,这些交易品种是基于所选AUDUSD交易品种中样本的置换分时。
最后,我们运行优化测试。
所使用的EA设置如下所示。
测试结果。
策略测试器的结果选项卡显示我们可能感兴趣的所有性能数据,并根据所选的性能标准按降序排列交易品种,这些标准可以通过测试器窗口右上角的下拉菜单进行选择。从这个视图中,p值可以很容易地手动计算,或者如果需要,可以通过处理.xml文件自动计算,该文件可以通过右键单击从测试器导出。
使用该示例,我们甚至不必运行任何计算,因为可以看出,原始交易品种的测试数字位于结果选项卡的下方,有10多个置换的交易品种显示出更好的性能。这表明p值高于0.05。
当然,这个测试的结果应该谨慎对待,因为所选择的测试周期非常短。用户应该选择一个长的多的、并能代表真实交易中可能遇到的情况的测试期。
如前所述,为了计算p值,有许多选项可用于进一步处理我们的结果。任何进一步的操作都将集中在解析从策略测试器导出的xml文件中的数据上。我们将演示如何使用电子表格应用程序在几次点击和按键中处理文件。
显然,导出文件后,记下文件的保存位置,并使用任何电子表格应用程序打开它。下图显示了免费的OpenOffice Calc的使用情况,其中在表底部添加了一个新行。在继续之前,明智的做法是删除不应包含在计算中的交易品种行。在每个相关的对应列下,使用自定义宏计算p值。宏的公式参考置换交易品种的性能度量(位于所示文档中的行18中)以及每列的置换交易品种的性能度量。宏的完整公式如图所示。
除了使用电子表格应用程序,我们还可以使用python,它有大量用于解析xml文件的模块。如果用户精通mql5,那么也可以使用简单的脚本来解析文件。从测试器导出优化结果时,只需记住选择一个可访问的目录。
结论
我们已经证明了置换测试可以应用于任何EA,而无需访问源代码。这种置换测试是非常宝贵的,因为它应用了相当稳健的统计数据,不需要对所涉及的任何数据的分布做出任何假设。与策略制定中使用的许多其他统计测试不同。
最大的缺点与进行测试所需的时间和计算机资源有关。它不仅需要强大的处理器,还需要大量的存储空间。生成新的分时数据和交易品种将占用您的可用硬盘空间。在我看来,任何从事EA采购业务的人都应该注意这种分析方法,它需要时间,但也可以避免你做出糟糕的决定,这会让你付出代价。
使用置换价格数据的分析可以以多种方式应用。我们可以使用该方法来分析指标的行为,也可以在策略开发的不同阶段使用它。有很多可能性。有时,在开发或测试策略时,可能会出现数据不足的情况。使用置换价格序列大大增加了测试数据的可用性。文章中描述的所有mql5程序的源代码都附在后面,我希望读者会发现它们很有用
文件名 | 程序类型 | 描述 |
---|---|---|
GenerateSymbols.mqh | 包含文件 | 文件包含CGenerateSymbols类的定义,用于生成具有从选定的基本交易品种置换的分时数据的交易品种 |
NewSymbol.mqh | 包含文件 | 包含用于创建自定义交易品种的CNewSymbol类定义 |
PermuteTicks.mqh | 包含文件 | 定义CPermuteTicks类,用于创建分时数据数组的排列 |
PrepareSymbolsForPermutationTests.mq5 | 脚本文件 | 自动创建带有置换交易品种的自定义交易品种的脚本,为置换测试做准备 |
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/13162
您好!这篇文章提供了很有价值的见解,我一直在寻找它。不过,在使用您分享的示例时,我遇到了一个问题。由于卖出价的原因,价差似乎很大。能否请您告诉我是否有遗漏或错误之处? 。
就我所知,我找不到任何错误。在我自己的测试中,我也遇到了价差较大的价格序列变化。这种情况有可能发生。如果这种情况无法接受,您可以做更多的排列,并在价差更实际的系列上进行测试。
//---swap tick data randomly tempvalue.bid_d=m_differenced[i].bid_d; tempvalue.ask_d=m_differenced[i].ask_d; tempvalue.vol_d=m_differenced[i].vol_d; tempvalue.volreal_d=m_differenced[i].volreal_d; m_differenced[i].bid_d=m_differenced[j].bid_d; m_differenced[i].ask_d=m_differenced[j].ask_d; m_differenced[i].vol_d=m_differenced[j].vol_d; m_differenced[i].volreal_d=m_differenced[j].volreal_d; m_differenced[j].bid_d=tempvalue.bid_d; m_differenced[j].ask_d=tempvalue.ask_d; m_differenced[j].vol_d=tempvalue.vol_d; m_differenced[j].volreal_d=tempvalue.volreal_d;
同样的说法也适用于结构数据的对数和逆变换方法。等等。
Tick 转换是一个罕见的话题。通常只在一个价格(例如出价)和条形图上进行。
感谢作者提出这个话题。
最近,俄语线程中有一个关于这个主题的话题。在那里,他们使用最好的机器学习方法,试图生成刻度线历史记录,以便不会丢失市场模式。有一个明确的标准。
不幸的是,所有不丢失模式的尝试都以失败告终。除了混合刻度线,还有更复杂的方法。
只有在这里发生了成功的事情。
Форум по трейдингу, автоматическим торговым системам и тестированию торговых стратегий
Машинное обучение в трейдинге: теория, модели, практика и алготорговля
fxsaber, 2023.09.07 07:33
我尝试了几种算法。为了清楚起见,下面是其中几种。
以平均价格建立 PO,条件是固定。
这样做的目的是在刻度数组中运行,并在找到的索引位置随机分配增量。
结果发现,时间戳、增量的绝对值(Avg-price)和价差都被完全保留了下来。
结果显示