English Русский 中文 日本語 Português
preview
Permutieren von Preisbalken in MQL5

Permutieren von Preisbalken in MQL5

MetaTrader 5Beispiele | 5 Februar 2024, 16:28
165 0
Francis Dube
Francis Dube

Einführung

Der Strategy Tester von MetaTrader 5 ist das wichtigste Tool, das von vielen verwendet wird, um das Potenzial von Expert Advisors (EA) zu bewerten. Seine Funktionen sind zwar ausreichend, aber erfahrene Entwickler können damit „Trick-EAs“ erstellen, die eine außergewöhnliche Leistung vortäuschen können. Wir haben alle diese Screenshots von Kapitalkurven gesehen, die eine unglaubliche Leistung von EA-Verkäufern zeigen. Auf den ersten Blick sieht das alles beeindruckend aus, aber wenn die Strategie in der realen Welt angewendet wird, ergibt sich oft eine völlig andere Aktienkurve. Wie können wir uns den Ärger ersparen, auf diese billigen Tricks hereinzufallen? In diesem Artikel werden wir uns ein solches System ansehen und zeigen, wie Permutationstests verwendet werden können, um den Nebel irreführender Aktienkurven zu durchdringen und eine genauere Aussage über die Leistung der Strategie zu erhalten. Außerdem haben wir in einem früheren Artikel die Implementierung eines Algorithmus zur Permutation von Tickdaten gesehen. Diesmal werden wir eine Methode zum Vertauschen von Preisbalken beschreiben.


Permutieren von OHLC-Daten

Die Permutation von Preisbalken ist etwas schwieriger zu bewerkstelligen, da mehrere Zeitreihen beteiligt sind. Ähnlich wie bei der Permutation von Tickdaten bemühen wir uns bei der Behandlung von Preisbalken, den allgemeinen Trend der ursprünglichen Preisreihe beizubehalten. Es ist auch wichtig, dass wir niemals zulassen, dass der Eröffnungs- oder Schlusskurs eines Balkens über oder unter die Grenzen des Hochs bzw. Tiefs geht. Ziel ist es, eine Reihe von Balken zu erhalten, deren Verteilung der Merkmale genau mit den ursprünglichen Daten übereinstimmt.

Neben dem Trend müssen wir auch die Streuung der Kursveränderungen im Verlauf der Serie vom Eröffnungs- bis zum Schlusskurs beachten. Die Spanne der Kursveränderungen zwischen Eröffnungs- und Schlusskurs sollte in den umgewandelten Balken gleich groß sein wie in den ursprünglichen. Außerhalb der Balken selbst müssen wir sicherstellen, dass die Verteilung der Preisänderungen von Balken zu Balken ebenfalls gleich ist. Genauer gesagt, die Differenz zwischen dem Schlusskurs eines Balkens und dem Eröffnungskurs des nächsten.  

Dies ist wichtig, um die getestete Strategie nicht zu benachteiligen. Die allgemeinen Merkmale der Serien sollten ähnlich sein, der einzige Unterschied sollten die absoluten Werte jedes Open, High, Low, Close (OHLC) zwischen dem ersten und letzten Balken sein. Der Code zur Implementierung dieser Funktion ähnelt dem der Klasse CPermuteTicks, die im Artikel Monte Carlo Permutation im MetaTrader 5 vorgestellt wurde. Der Code für die Permutation der Preisbalken wird in der Klasse CPermuteRates gekapselt, die in PermuteRates.mqh enthalten ist.


Die Klasse 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 beginnt mit der Definition einer einfachen Struktur, in der die Protokolldifferenzen der Rohpreise gespeichert werden.

  •  rel_open enthält die Protokolldifferenz zwischen dem aktuellen Eröffnungs- und dem Schlusskurs des vorherigen Balkens.
  • rel_high ist die logarithmische Differenz zwischen dem Höchststand des aktuellen Balkens und der Eröffnung.
  •  rel_low bezieht sich auf die logarithmische Differenz zwischen dem aktuellen Tiefststand und der Eröffnung.
  •  rel_close ist wiederum die Log-Differenz zwischen dem aktuellen Bar Close und Open.

Die nutzerdefinierte Struktur CRelRates stellt Daten dar, die aus MqlRates extrahiert und permutiert werden. Andere Strukturmitglieder von MqlRates werden nicht verändert. Das Endergebnis der permutierten Kurse enthält diese Strukturelemente, die aus den ursprünglichen Preisreihen kopiert wurden. Wie bereits erwähnt, werden sich nur die OHLC-Werte ändern.

//+------------------------------------------------------------------+
//| 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;
//---
  }   


Die Permutation wird mit der Methode Permute() durchgeführt. Die CRelRates-Struktur unterteilt die Daten der Balken in zwei Arten von Deskriptoren. Die Zeitreihe rel_open steht für Veränderungen von einem Balken zum nächsten, während rel_high, rel_low und rel_close die Veränderungen innerhalb eines Balkens darstellen. Um die Balken zu vertauschen, mischen wir zunächst die Zeitreihe der rel_open-Preise, d.h. die Differenzen zwischen den Balken. Von dort aus werden die inneren Balkenwechsel gemischt. Die neuen OHLC-Serien werden aus den gemischten Inter-Balken-Daten konstruiert, um die neuen Eröffnungswerte mit den entsprechenden Hoch-, Tief- und Schlusskursen zu erhalten, die aus den gemischten Änderungen der inneren Balken konstruiert werden.


Änderungen an CPermuteTicks

Es gibt eine Reihe von Unterschieden zwischen CPermuteRates und der alten Klasse CPermuteTicks. Eine davon ist die Verwendung eines nutzerdefinierten Zufallszahlengenerators, der meiner Meinung nach ein wenig schneller ist als die in MQL5 eingebauten Funktionen.

//+------------------------------------------------------------------+
//|                                                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();
  }
//+------------------------------------------------------------------+

Er wird auch auf die neue Klasse CPermuteTicks angewendet. Unnötige Zwischenschritte wurden im Interesse der Effizienz eliminiert. Nur die Geldkurse (Bid) werden neu gemischt. Da auch andere Tick-Eigenschaften aus der ursprünglichen Tick-Serie kopiert werden, wird damit ein Problem gelöst, das manchmal zu permutierten Ticks mit unrealistischen Spreads führte. Die neue CPermuteTick-Serie ist unten abgebildet.

//+------------------------------------------------------------------+
//|                                                 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 funktioniert immer noch auf die gleiche Weise wie die vorherige Version und CPermuteRates funktioniert ähnlich. Der Unterschied zwischen den beiden besteht darin, dass die eine mit Ticks und die andere mit MqlRates arbeitet.


Die Klasse CPermutedSymbolData

Das Skript PrepareSymbolsForPermutationTest wurde aktualisiert, um die Änderungen an CPermuteTicks und die Einführung von CPermuteRates zu berücksichtigen. Die Funktionalität des Skripts ist in der Klasse CPermutedSymbolData enthalten, sie ermöglicht die Erstellung von nutzerdefinierten Symbolen mit entweder permutierten Ticks oder Raten auf der Grundlage eines bestehenden Symbols.

//+------------------------------------------------------------------+
//|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);
  };


Dies wird erreicht, indem im Konstruktoraufruf die Art der zu mischenden Daten, entweder Ticks oder Raten, angegeben wird. Die Enumeration ENUM_RATES_TICKS beschreibt die Optionen, die für den einzigen Parameter des Konstruktors verfügbar sind.

//+-----------------------------------------------------------------------------------------+
//|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;
     }
  }

Sobald eine Instanz von CPermutedSymbolData erstellt wurde, sollte die Methode Initiate() aufgerufen werden, um das Symbol und den Datumszeitraum anzugeben, der die Ticks oder Kurse definiert, auf denen die Permutationen basieren werden.

//+------------------------------------------------------------------+
//| 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;
  }
//+------------------------------------------------------------------+

Wenn Initiate() true zurückgibt, kann die Methode Generate() mit der Anzahl der erforderlichen Permutationen aufgerufen werden. Die Methode gibt die Anzahl der nutzerdefinierten Symbole zurück, deren Daten erfolgreich mit permutierten Ticks oder Kursen aufgefüllt wurden.

//+------------------------------------------------------------------+
//|                            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;
  }
//+------------------------------------------------------------------+

Oben sehen Sie den Code des Skripts. Der gesamte Quellcode ist dem Artikel beigefügt.


Anwendung von Permutationstests

In der Einleitung des Artikels sprachen wir über ein häufiges Problem, mit dem viele konfrontiert sind, die Expert Advisors kaufen möchten. Es besteht die Möglichkeit, dass skrupellose Verkäufer trügerische Taktiken anwenden, um ihre Produkte zu vermarkten. Oft zeigen die Verkäufer Screenshots attraktiver Aktienkurven, um die potenziellen Gewinne darzustellen. Viele sind dieser Taktik zum Opfer gefallen und haben auf die harte Tour gelernt, dass diese Bildschirmfotos aus induzierten Strategien entstanden sind. In diesem Abschnitt werden wir einen Blick auf einen berüchtigten EA werfen, der in der Codebasis verfügbar ist und mit dem sich irreführende Aktienkurven erstellen lassen. Und wenden Sie einen Permutationstest an, um die Täuschung aufzudecken.

Saldenkurve

Überblick über den Permutationstest

Es sei darauf hingewiesen, dass diese Art der Prüfung recht aufwändig ist und viel Zeit und Rechenleistung erfordert. Meiner Meinung nach sind die Ergebnisse jedoch die Mühe wert und können einen vor einer schlechten Entscheidung bewahren. Die angewandte Methode besteht darin, eine geeignete Probe für die Untersuchung auszuwählen. Trennung dieser Stichprobe in Datensätze innerhalb und außerhalb der Stichprobe. Der EA wird anhand von In-Sample-Daten optimiert, und die endgültige Leistung wird anhand von Tests mit Out-of-Sample-Daten unter Verwendung der optimierten Parameter ermittelt. Dies geschieht mit den ursprünglichen Datenreihen und mit mindestens 100 permutierten Datensätzen. Genau das wurde getan, um den in unserer Demonstration verwendeten EA zu testen.

Testen des grr-al EA

Jeder, der die MQL5-Dokumentation studiert oder die Codebasis erforscht hat, muss auf diesen EA gestoßen sein. In der MQL5-Dokumentation wird es als "Testing Grail" bezeichnet. Wenn sie im Strategietester entweder im 1-Minuten-OHLC- oder im Nur-Tick-Erzeugungsmodus für offene Kurse ausgeführt wird, wird eine Saldenkurve mit einem beeindruckenden Profil erzeugt. Dies ist der EA, den wir in unserer Demonstration verwenden werden. Wir haben den Code leicht modifiziert, um einige der globalen Variablen zur Optimierung freizulegen. Zwei der 3 Parameter wurden für die Optimierung ausgewählt, nämlich SL, der Stoploss und 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;


Die für die Optimierung verwendeten Einstellungen sind in der Grafik dargestellt.

Eingaben zur Optimierung



Als Datensatz wurde der EURUSD ausgewählt, und zwar für das gesamte Jahr 2022 auf Stundenbasis. Die ersten sechs Monate des Jahres 2022 wurden für die Optimierung verwendet, und die letzte Hälfte wurde als Zeitraum außerhalb der Stichprobe für die Prüfung der optimalen Parameter genutzt.

Optimierungseinstellungen


Zunächst wurde das Skript PrepareSymbolsForPermutationsTests verwendet, um die nutzerdefinierten Symbole der permutierten Daten zu erzeugen. Der Programmlauf wurde wie unten dargestellt zeitlich erfasst und notiert. Der Fehlercode kommt daher, dass mir beim ersten Versuch der Speicherplatz ausgegangen ist und nur 99 nutzerdefinierte Symbole erfolgreich hinzugefügt wurden. 

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

Die erzeugte Datenmenge belief sich auf fast 40 Gigabyte an Tickdaten für ein Jahr, wobei die Daten 100 Mal permutiert wurden!


Größe des Tick-Daten-Ordners

Aus Interessensgründen war die Verwendung von MQL-Rates viel schneller und nahm viel weniger Platz in Anspruch.

NK      0       12:51:23.166    PrepareSymbolsForPermutationTests (EURUSD,M1)   Number of permuted symbols is 100, Runtime 1.4mins


Anhand dieser Daten wurde jedes Symbol in den Stichproben optimiert.

Schnappschuss der Optimierungsergebnisse

Die Parameter, die den größten absoluten Gewinn erbrachten, wurden für den Stichprobentest verwendet. Die Optimierung und die Out-of-Sample-Tests wurden nur im Tick-Modus mit Eröffnungspreisen durchgeführt. Das bedeutet, dass der EA alle Vorteile hatte, um zu glänzen.

Die Ergebnisse aller Tests sind unten in einer csv-Datei dargestellt. IS-Gewinn und OOS-Gewinn sind der Gewinn in der Stichprobe bzw. außerhalb der Stichprobe

<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

Der berechnete p-Wert wird mit 0,8217821782178217 angegeben.

MO      0       09:49:57.991    ProcessOptFiles (EURUSD,MN1)    P-value is 0.8217821782178217

Dies bedeutet, dass die Wahrscheinlichkeit, die im ursprünglichen Datensatz erzielte Leistung durch Glück zu erreichen, bei über 80 % liegt. Dies zeigt eindeutig, dass dieser EA wertlos ist.


Warum funktioniert das?

Die Prämisse der Permutationstests im Zusammenhang mit der Strategieentwicklung ist, dass eine EA-Strategie eine Beschreibung eines Musters oder einer Reihe von Regeln ist, die dazu dienen, einen Vorteil beim Handel zu erzielen. Wenn die Daten, mit denen er arbeitet, permutiert werden, werden die ursprünglichen Muster, von denen es sonst profitieren würde, gestört. Wenn der EA tatsächlich nach einem bestimmten Muster handelt, leidet seine Leistung bei permutierten Daten. Vergleicht man die Leistung von permutierten und nicht-permutierten Tests, so wird deutlich, dass der EA auch nach der Optimierung tatsächlich auf einem einzigartigen Muster oder einer Regel beruht. Die Leistung des unverfälschten Datensatzes sollte sich von den permutierten Tests unterscheiden.

Wie wir in dem demonstrierten Test gesehen haben, nutzt der fragliche EA bekanntermaßen die Methode der Tickgenerierung aus und verwendet keine echte Strategie (Muster oder Regeln). Der Permutationstest konnte dies aufzeigen.

Permutationstests können auch verwendet werden, um einen Hinweis auf das Ausmaß der Überanpassung nach der Optimierung zu geben. Um zu prüfen, ob eine Überanpassung vorliegt, müssten wir die Leistung der permutierten und nicht permutierten Datensätze in der Stichprobe prüfen und vergleichen. Das Ausmaß, in dem die nicht angepassten Leistungsdaten von den angepassten Ergebnissen abweichen, kann zur Quantifizierung der Überanpassung verwendet werden. Wenn eine Überanpassung vorherrscht, gibt es kaum einen Unterschied zwischen den permutierten und den unpermutierten Leistungsergebnissen. Wir würden ziemlich große p-Werte sehen. 


Schlussfolgerung

Wir haben die Implementierung eines Algorithmus zur Permutation von Preisbalken gesehen. Außerdem wurde der Code für die Erzeugung von nutzerdefinierten Symbolen mit permutierten Ticks oder Balken aktualisiert. Die beschriebenen Programme wurden zur Demonstration eines Permutationstests an einem EA mit induzierten positiven Leistungsergebnissen verwendet. Permutationstests sind ein unverzichtbares Instrument für jeden, der sich für den automatisierten Handel interessiert. Zusammenfassend denke ich, es sollte als eine Funktion von Mt5's Strategie-Tester hinzugefügt werden.

Datei
Beschreibung
MQL5\Experts\grr-al.mq5
Dies ist eine leicht modifizierte Version des EA, der in der Codebasis von MQL5.com verfügbar ist. Er handelt unter Ausnutzung der Tick-Generierungsmethode des Strategietesters im 1-Minuten-OHLC-Modus.
MQL5\Include\NewSymbol.mqh
enthält die Klassendefinition CNewSymbol zur Erstellung nutzerdefinierter Symbole.
MQL5\Include\PermutedSymbolData.mqh
definiert die Klasse CPermutedSymbolData zur Erstellung nutzerdefinierter Symbole mit permutierten Kursen oder Ticks.
MQL5\Include\PermuteRates.mqh
 enthält die Klasse CPermuteRates zur Erstellung von Permutationen eines Arrays von MqlRates-Daten.
MQL5\Include\PermuteTicks.mqh
definiert die Klasse CPermuteTicks zur Erzeugung von Permutationen eines Arrays von MqlTick-Daten.
MQL5\Include\UniformRandom.mqh
CUniFrand kapselt einen gleichmäßig verteilten Zufallszahlengenerator.
MQL5\Scripts\PrepareSymbolsForPermutationTests.mq5
Dies ist ein Skript, das alle Code-Utilities zusammenfasst, um nutzerdefinierte Symbole in MetaTrader 5 zu erzeugen.


Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/13591

Beigefügte Dateien |
grr-al.mq5 (4.94 KB)
NewSymbol.mqh (29.34 KB)
PermuteRates.mqh (6.04 KB)
PermuteTicks.mqh (4.79 KB)
UniformRandom.mqh (2.84 KB)
Integrieren Sie Ihr eigenes LLM in EA (Teil 2): Beispiel für den Einsatz in einer Umgebung Integrieren Sie Ihr eigenes LLM in EA (Teil 2): Beispiel für den Einsatz in einer Umgebung
Angesichts der rasanten Entwicklung der künstlichen Intelligenz sind Sprachmodelle (language models, LLMs) heute ein wichtiger Bestandteil der künstlichen Intelligenz, sodass wir darüber nachdenken sollten, wie wir leistungsstarke LLMs in unseren algorithmischen Handel integrieren können. Für die meisten Menschen ist es schwierig, diese leistungsstarken Modelle auf ihre Bedürfnisse abzustimmen, sie lokal einzusetzen und sie dann auf den algorithmischen Handel anzuwenden. In dieser Artikelserie werden wir Schritt für Schritt vorgehen, um dieses Ziel zu erreichen.
Lernen Sie, wie man mit Datum und Uhrzeit in MQL5 umgeht Lernen Sie, wie man mit Datum und Uhrzeit in MQL5 umgeht
Ein neuer Artikel über ein neues wichtiges Thema, das sich mit Datum und Zeit beschäftigt. Als Händler oder Programmierer von Handelsinstrumenten ist es sehr wichtig zu verstehen, wie man mit diesen beiden Aspekten Datum und Zeit sehr gut und effektiv umgehen kann. Ich werde also einige wichtige Informationen darüber weitergeben, wie wir mit Datum und Zeit umgehen können, um effektive Handelsinstrumente reibungslos und einfach zu erstellen, ohne dass es zu Komplikationen kommt.
MQL5-Assistenz-Techniken, die Sie kennen sollten (Teil 07): Dendrogramme MQL5-Assistenz-Techniken, die Sie kennen sollten (Teil 07): Dendrogramme
Die Klassifizierung von Daten zu Analyse- und Prognosezwecken ist ein sehr vielfältiger Bereich des maschinellen Lernens, der eine große Anzahl von Ansätzen und Methoden umfasst. Dieser Beitrag befasst sich mit einem solchen Ansatz, der Agglomerativen Hierarchischen Klassifikation.
Kategorientheorie in MQL5 (Teil 23): Ein anderer Blick auf den doppelten exponentiellen gleitenden Durchschnitt Kategorientheorie in MQL5 (Teil 23): Ein anderer Blick auf den doppelten exponentiellen gleitenden Durchschnitt
In diesem Artikel setzen wir unser Thema vom letzten Mal fort, indem wir uns mit alltäglichen Handelsindikatoren befassen, die wir in einem „neuen“ Licht betrachten. Wir befassen uns in diesem Beitrag mit der horizontalen Zusammensetzung natürlicher Transformationen, und der beste Indikator dafür, der das soeben behandelte Thema noch erweitert, ist der doppelte exponentielle gleitende Durchschnitt (DEMA).