English Русский Español Deutsch 日本語 Português
preview
MetaTrader 5中的蒙特卡罗置换测试

MetaTrader 5中的蒙特卡罗置换测试

MetaTrader 5示例 |
618 10
Francis Dube
Francis Dube

概述

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设置如下所示。

EA 设置

测试结果。

优化结果


策略测试器的结果选项卡显示我们可能感兴趣的所有性能数据,并根据所选的性能标准按降序排列交易品种,这些标准可以通过测试器窗口右上角的下拉菜单进行选择。从这个视图中,p值可以很容易地手动计算,或者如果需要,可以通过处理.xml文件自动计算,该文件可以通过右键单击从测试器导出。

使用该示例,我们甚至不必运行任何计算,因为可以看出,原始交易品种的测试数字位于结果选项卡的下方,有10多个置换的交易品种显示出更好的性能。这表明p值高于0.05。

当然,这个测试的结果应该谨慎对待,因为所选择的测试周期非常短。用户应该选择一个长的多的、并能代表真实交易中可能遇到的情况的测试期。

如前所述,为了计算p值,有许多选项可用于进一步处理我们的结果。任何进一步的操作都将集中在解析从策略测试器导出的xml文件中的数据上。我们将演示如何使用电子表格应用程序在几次点击和按键中处理文件。

显然,导出文件后,记下文件的保存位置,并使用任何电子表格应用程序打开它。下图显示了免费的OpenOffice Calc的使用情况,其中在表底部添加了一个新行。在继续之前,明智的做法是删除不应包含在计算中的交易品种行。在每个相关的对应列下,使用自定义宏计算p值。宏的公式参考置换交易品种的性能度量(位于所示文档中的行18中)以及每列的置换交易品种的性能度量。宏的完整公式如图所示。

在 OpenOffice Calc 中计算P值

除了使用电子表格应用程序,我们还可以使用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

附加的文件 |
NewSymbol.mqh (29.34 KB)
PermuteTicks.mqh (8.78 KB)
Mql5.zip (9.91 KB)
最近评论 | 前往讨论 (10)
fxsaber
fxsaber | 30 9月 2023 在 15:42

Tick 转换是一个罕见的话题。通常只在一个价格(例如出价)和条形图上进行。

感谢作者提出这个话题。


最近,俄语线程中有一个关于这个主题的话题。在那里,他们使用最好的机器学习方法,试图生成刻度线历史记录,以便不会丢失市场模式。有一个明确的标准。

不幸的是,所有不丢失模式的尝试都以失败告终。除了混合刻度线,还有更复杂的方法。


只有在这里发生了成功的事情。

Форум по трейдингу, автоматическим торговым системам и тестированию торговых стратегий

Машинное обучение в трейдинге: теория, модели, практика и алготорговля

fxsaber, 2023.09.07 07:33

我尝试了几种算法。为了清楚起见,下面是其中几种。

以平均价格建立 PO,条件是固定。

  • 绿点是柚木数组3Z 个顶点的 指数。
  • 紫色--顶点之间的平均指数。

这样做的目的是在刻度数组中运行,并在找到的索引位置随机分配增量。

结果发现,时间戳、增量的绝对值(Avg-price)和价差都被完全保留了下来。


结果显示

  1. 我只在绿色索引上运行 - 排水口。显然,这种随机化会拉直(减少 ZZ 数量)最终图形。
  2. 我只在紫色指数上运行--圣杯越强 最小条件越高。
  3. 我在两种颜色--梅花--上运行。
我认为,如果同时根据买入价/卖出价构建 3Z,那么第 2 点的圣杯会更强。

反向时间 可以很好地保留市场模式。
fxsaber
fxsaber | 19 12月 2023 在 18:59
//+------------------------------------------------------------------+
//|应用对数变换的辅助方法
//+------------------------------------------------------------------+
bool CPermuteTicks::LogTransformTicks(void)
  {
//--必要时调整 m_logticks 的大小 
   if(m_logticks.Size()!=m_ticks.Size())
      ArrayResize(m_logticks,m_ticks.Size());
//--只对相关数据进行日志转换,避免负值和零值
   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(1 e0);
      m_logticks[i].ask_d=(m_ticks[i].ask>0)?MathLog(m_ticks[i].ask):MathLog(1 e0);
      m_logticks[i].vol_d=(m_ticks[i].volume>0)?MathLog(m_ticks[i].volume):MathLog(1 e0);
      m_logticks[i].volreal_d=(m_ticks[i].volume_real>0)?MathLog(m_ticks[i].volume_real):MathLog(1 e0);
     }
//---
   return true;
  }
为什么要对体积进行对数运算?
fxsaber
fxsaber | 19 12月 2023 在 19:12

мы хотим, чтобы p-значения были как можно ближе к нулю, в диапазоне 0,05 и ниже.

完整公式如下:

z+1/r+1

其中,r 是进行的排列次数,z 是性能最佳的测试总数。

在这种情况下,这一标准将不起作用--在原始符号上进行优化,然后在排列上运行。

fxsaber
fxsaber | 19 12月 2023 在 19:25
Процедура перестановки важна для правильного проведения теста.

使用的排列算法。

  1. 创建一个买价/卖价对数增量(相邻刻度之间)数组。
  2. 对该数组进行洗牌。此外,还要进行强混合。
  3. 通过第 2 项的增量创建一个新的刻度数组。

这种方法消除了初始序列中的所有规律性(如果有的话)。因为输出结果是随机 漫步。


不应该这样做。

RustyKanuck
RustyKanuck | 8 7月 2024 在 05:21
是否有办法进行蒙特卡罗 分析,但用多货币 ea 代替?
开发回放系统 — 市场模拟(第 18 部分):跳价和更多跳价(II) 开发回放系统 — 市场模拟(第 18 部分):跳价和更多跳价(II)
显然,目前的衡量度与创建 1-分钟柱线的理想时间相距甚远。这是我们要率先解决的一件事。解决同步问题并不困难。也许这看起来很难,但实际上却很简单。在上一篇文章中,我们没有进行所需的调整,因为它的目的是解释如何把图表上创建 1-分钟柱线的跳价数据转移至市场观察窗口。
软件开发和 MQL5 中的设计范式(第一部分):创建范式 软件开发和 MQL5 中的设计范式(第一部分):创建范式
有一些方法可以用来解决许多重复性的问题。一旦明白如何运用这些方法,就可助您有效地创建软件,并贯彻 DRY(不要重复自己)的概念。在这种境况下,设计范式的主题就非常好用,因为它们为恰当描述过,且重复的问题提供了解决方案。
制作仪表板以显示指标和EA中的数据 制作仪表板以显示指标和EA中的数据
在本文中,我们将创建一个用于指标和EA的仪表板类。这是一个小系列文章中的介绍性文章,其中包含模板以在EA交易中包含和使用标准指标。我将首先创建一个类似于MetaTrader 5数据窗口的面板。
测试不同的移动平均类型以了解它们的洞察力 测试不同的移动平均类型以了解它们的洞察力
我们都知道移动平均指标对很多交易者的重要性。还有其他移动平均线类型在交易中也很有用,我们将在本文中确定这些类型,并将它们中的每一种与最流行的简单移动平均线进行简单比较,看看哪一种可以显示出最好的结果。