English 中文 Deutsch 日本語 Português
preview
Перестановка ценовых баров в MQL5

Перестановка ценовых баров в MQL5

MetaTrader 5Примеры | 28 февраля 2024, 11:26
522 1
Francis Dube
Francis Dube

Введение

Тестер стратегий MetaTrader 5 — это основной инструмент, используемый многими трейдерами для оценки потенциала советников. Опытные разработчики могут использовать его для создания "хитрых" советников, способных демонстрировать исключительную производительность. Мы все видели скриншоты с кривыми эквити, демонстрирующими невероятную эффективность. Выглядят они, конечно, впечатляюще, но часто, когда стратегия применяется на реальном рынке, картина получается совершенно другой. Как мы можем спастись от таких уловок? В этой статье мы рассмотрим такую систему и продемонстрируем, как можно использовать тест перестановки, чтобы прорваться сквозь дымовую завесу вводящих в заблуждение кривых капитала и получить более точное представление об эффективности стратегии. Также в предыдущей статье мы видели реализацию алгоритма перестановки тиковых данных. На этот раз мы опишем метод перестановки ценовых баров.


Перестановка данных OHLC

Перестановку ценовых баров выполнить немного сложнее из-за задействования нескольких рядов. Подобно перестановке тиковых данных, при работе с ценовыми барами мы стремимся сохранить общую тенденцию исходного ценового ряда. Также важно, чтобы мы никогда не позволяли открытию или закрытию бара выходить за пределы максимума и минимума. Цель состоит в том, чтобы получить серию баров с точно таким же распределением признаков, как и в исходных данных.

Помимо тренда, мы должны поддерживать дисперсию изменений цен по мере продвижения серии от открытия к закрытию. Разброс изменений цен между открытием и закрытием должен быть таким же в переставленных барах, как и в исходном. За пределами самих баров мы должны убедиться, что распределение изменений цен между барами также одинаково, включая разницу между закрытием одного бара и открытием следующего.  

Это важно, чтобы не поставить в невыгодное положение тестируемую стратегию. Общие характеристики серии должны быть схожими. Единственное различие должна заключаться в абсолютных значениях каждого открытия, максимума, минимума и закрытия (OHLC) между первым и последним баром. Код реализации очень похож на код, используемый в классе CPermuteTicks, представленном в статье "Тесты на перестановку Монте-Карло в MetaTrader 5". Код перестановки ценовых баров будет инкапсулирован в класс CPermuteRates, содержащийся в PermuteRates.mqh.


Класс 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;
  }
//+------------------------------------------------------------------+

Выше приведен код скрипта. Весь исходный код приложен к статье.


Применение тестирования на перестановку

Во введении к статье я упомянул о проблеме, с которой сталкиваются многие потенциальные покупатели советников. Существует вероятность того, что недобросовестные продавцы могут использовать обманную тактику. Часто продавцы демонстрируют скриншоты привлекательных кривых эквити, демонстрирующих потенциальную прибыль. Многие стали жертвами этой тактики и на собственном горьком опыте усвоили, что эти скриншоты были созданы на основе искусственных стратегий. В этом разделе мы рассмотрим один такой советник, доступный в CodeBase, который можно использовать для создания вводящих в заблуждение кривых эквити. Также мы применим тест на перестановку, чтобы раскрыть обман.

Кривая эквити

Обзор теста на перестановку

Тест достаточно трудоемкий и требует значительных временных и вычислительных ресурсов. Хотя, на мой взгляд, результаты стоят затраченных усилий и могут уберечь от ошибок. Используемый метод предполагает отбор подходящего выборки для тестирования. Выборка разделяется на внутривыборочные и вневыборочные наборы данных. Советник будет оптимизирован на внутривыборочных данных, а окончательная производительность будет определена в ходе тестов, проведенных на вневыборочных данных с использованием оптимизированных параметров. Это делается с использованием исходного ряда данных, а также как минимум 100 перестановочных наборов данных. Именно это и было сделано для тестирования советника, использованного в нашей демонстрации.

Тестирование советника grr-al

Любой, кто изучал документацию MQL5 или изучал кодовую базу, наверняка сталкивался с этим советником. В документации MQL5 он назван "тестовым Граалем". Когда он запускается в тестере стратегий в режиме генерации тиков "OHLC на M1" или "Только цены открытия", получается впечатляющая кривая эквити. Это советник, который мы будем использовать в нашей демонстрации. Мы немного изменили код, чтобы показать некоторые глобальные переменные для оптимизации. Два из трех параметров были выбраны для оптимизации, а именно SL, TP и 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;


Настройки, используемые для оптимизации, показаны на скриншоте.

Входные данные для оптимизации



В качестве набора данных был выбран EURUSD H1 за весь 2022 год. Первые шесть месяцев 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

Объем сгенерированных данных составил почти 40 гигабайт тиковых данных за год, когда данные переставлялись 100 раз!


Размер папки тиковых данных

При использовании цен всё происходило гораздо быстрее, а данные занимали гораздо меньше места.

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


С этими данными каждый символ был оптимизирован на внутривыборочных наборах.

Результаты оптимизации

Параметры, которые принесли наибольшую абсолютную прибыль, использовались во вневыборочном тесте. Оптимизация и вневыборочное тестирование проводились с использованием тикового режима по ценам открытия. Это означает, что у советника были все преимущества для отображения выдающихся результатов.

Результаты всех тестов представлены в csv-файле. IS Profit и OOS PROFIT - это прибыль в выборке и вне выборки соответственно

<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 равно 0,8217821782178217.

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

Таким образом, вероятность наблюдения производительности, достигнутой в исходном наборе данных случайным образом, превышает 80 процентов. Это ясно указывает на то, что советник бесполезен.


Почему это работает?

Предпосылка тестирования на перестановку в контексте разработки стратегии заключается в том, что стратегия советника представляет собой описание шаблона или набора правил, используемых для получения преимущества в торговле. Когда данные, с которыми он работает, изменяются, исходные шаблоны, следуя которым советник мог бы получить прибыль, могут быть нарушены. Если советник действительно торгует по какому-то шаблону, его производительность на перестановочных данных пострадает. Если сравнить производительность тестов с перестановкой и без, становится ясно, что даже после оптимизации советник на самом деле полагается на какой-то уникальный шаблон или правило. Производительность неперестановочного набора данных должна отличаться от перестановочных тестов.

Как мы увидели из продемонстрированного теста, рассматриваемый советник использует метод генерации тиков и не применяет никакой реальной стратегии (паттернов или правил). Тест на перестановку смог это выявить.

Тесты на перестановку также можно использовать для определения степени переобучения после оптимизации. Чтобы проверить переобучение, нам нужно будет протестировать и сравнить производительность в выборке из перестановочных и неперестановочных наборов данных. Степень, в которой показатели производительности без перестановок отличаются от результатов с перестановками, можно использовать для количественной оценки переобучения. Когда преобладает переобучение, разница между перестановочными и неперестановочными результатами производительности будет небольшой. Мы увидим довольно большие значения p.


Заключение

Мы рассмотрели реализацию алгоритма перестановки ценовых баров, а также обновили код для создания пользовательских символов с переставленными тиками или барами. Описанные программы были использованы для демонстрации перестановочного теста на советнике с положительными результатами производительности. Тестирование на перестановку — важный инструмент для всех, кто интересуется автоматической торговлей. Настолько важный, что, на мой взгляд, его следует добавить в качестве функции тестера стратегий MetaTrader 5.

Файл
Описание
MQL5\Experts\grr-al.mq5
Это слегка модифицированная версия советника, доступная в CodeBase на MQL5.com. Она торгует, используя метод генерации тиков тестера стратегий в режиме "OHLC на M1"
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

Прикрепленные файлы |
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)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (1)
Aleksandr Slavskii
Aleksandr Slavskii | 29 февр. 2024 в 13:40

Самая высокая скорость в мире на автомобиле и на любом наземном управляемом транспортном средстве — 1228 км/ч — была показана на реактивном автомобиле Thrust SSC англичанином Энди Грином 15 октября 1997 года.

Трасса длиной 21 километр была размечена на дне высохшего озера в пустыне Блэк-Рок, штат Невада, США.

//---

Давайте проверим не было ли это сфабриковано с целью обмануть общественность.

Мы сделаем точно такую же трассу, но насыпем на неё камней и попробуем проехать. 

Таким образом, мы выясним, что вероятность набрать такую же скорость, которой достиг Энди Грин, стремится к нулю процентов. Это ясно указывает на то, что  ̶с̶о̶в̶е̶т̶н̶и̶к̶   рекорд скорости фиктивен.

:)

//---

Пример использования данного метода получения пользовательских котировок, не корректен.

Добавляем пользовательскую LLM в торгового робота (Часть 2): Пример развертывания среды Добавляем пользовательскую LLM в торгового робота (Часть 2): Пример развертывания среды
Языковые модели (LLM) являются важной частью быстро развивающегося искусственного интеллекта, поэтому нам следует подумать о том, как интегрировать мощные LLM в нашу алгоритмическую торговлю. Большинству людей сложно настроить эти модели в соответствии со своими потребностями, развернуть их локально, а затем применить к алгоритмической торговле. В этой серии статей будет рассмотрен пошаговый подход к достижению этой цели.
Работа с ONNX-моделями в форматах float16 и float8 Работа с ONNX-моделями в форматах float16 и float8
Форматы данных, используемые для представления моделей машинного обучения, играют ключевую роль в их эффективности. В последние годы появилось несколько новых типов данных, разработанных специально для работы с моделями глубокого обучения. В данной статье мы обратим внимание на два новых формата данных, которые стали широко применяться в современных моделях.
Базовый класс популяционных алгоритмов как основа эффективной оптимизации Базовый класс популяционных алгоритмов как основа эффективной оптимизации
Уникальная исследовательская попытка объединения разнообразных популяционных алгоритмов в единый класс с целью упрощения применения методов оптимизации. Этот подход не только открывает возможности для разработки новых алгоритмов, включая гибридные варианты, но и создает универсальный базовый стенд для тестирования. Этот стенд становится ключевым инструментом для выбора оптимального алгоритма в зависимости от конкретной задачи.
Нейросети — это просто (Часть 78): Детектор объектов на основе Трансформера (DFFT) Нейросети — это просто (Часть 78): Детектор объектов на основе Трансформера (DFFT)
В данной статье я предлагаю посмотреть на вопрос построения торговой стратегии с другой стороны. Мы не будем прогнозировать будущее ценовое движение, а попробуем построить торговую систему на основе анализа исторических данных.