English 中文 Español Deutsch 日本語 Português
preview
Тесты на перестановку Монте-Карло в MetaTrader 5

Тесты на перестановку Монте-Карло в MetaTrader 5

MetaTrader 5Примеры | 19 декабря 2023, 10:26
967 3
Francis Dube
Francis Dube

Введение

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

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

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

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

Мы делаем это много раз, каждый раз меняя ценовые ряды и записывая полученные критерии производительности, которые мы отметили для других тестов. Это нужно сделать минимум сто раз, в идеале – несколько тысяч раз. Чем больше раз мы будем переставлять и тестировать, тем более надежными будут результаты. Но чего мы ожидаем от результатов?


Зачем нужны тесты на перестановку?

После проведения ряда итеративных тестов мы получаем набор показателей производительности для каждой перестановки. Не имеет значения, какой показатель производительности мы используем, это может быть коэффициент Шарпа, коэффициент прибыли или просто итоговый баланс или чистая прибыль. Предположим, было проведено 99 перестановок (или 100, включая исходный тест без перестановок). У нас есть 100 показателей производительности для сравнения.

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

Продолжая наш гипотетический тест с перестановками из 100 итераций, выяснилось, что ровно 29 показателей производительности с перестановками были лучше, чем эталонный тест без перестановок. Мы получаем p-значение 0,3, то есть 29+1/100. Это означает, что с вероятностью 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 данные графиков отображаются в виде столбцов, построенных на основе тиковых данных. Переставлять один ценовой ряд намного проще, чем переставлять информацию о барах, поэтому мы будем использовать тиковые данные. Использование тиков также приводит к ряду других сложностей, поскольку тики включают в себя и другую информацию, помимо необработанных цен. Имеется информация об объеме, времени и тиковых флагах.


Во-первых, информация о времени и тиковом флаге остается без изменений, поэтому наша процедура перестановки не должна менять эту информацию. Нас интересуют только бид, аск и объем. Вторая сложность связана с возможностью того, что любое из этих значений может быть равно нулю, что вызовет проблемы при применении к ним логарифмического преобразования. Чтобы продемонстрировать, как преодолеть эти проблемы, давайте посмотрим на код.

Реализация алгоритма перестановки тиков

Класс CPermuteTicks, содержащийся во включаемом файле PermuteTicks.mqh, реализует процедуру перестановки тиков. Внутри 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 имеет три частных свойства массива, которые хранят: исходные тики, хранящиеся в 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_logticks перестраивается путем отмены различий с использованием переставленных тиковых данных m_differenced. Наконец, единственный аргумент метода 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. Первый позволяет создавать собственные символы и указывать их свойства. С помощью второго можно оптимизировать советники в соответствии с включенными символами в списке "Обзор рынка". То есть в процесс добавляются еще как минимум два шага.

Мы можем переставлять тики и создавать собственные символы на основе любого существующего. При этом для каждого пользовательского символа указывается уникальная перестановка тиков для символа, используемого в качестве основы. Символы можно создавать вручную. Но разумнее будет автоматизировать задачу по созданию символов и добавлению перестановочных тиков.

Именно этим и занимается скрипт 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() следует вызывать первым после создания объекта. Он имеет четыре параметра, которые соответствуют пользовательским входным параметрам уже упомянутого скрипта.

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


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

Пример

Давайте посмотрим, как всё это выглядит, запустив тест с использованием идущего в комплекте советника MACD Sample. Тест будет проводиться на символе AUDUSD со 100 заданными в скрипте перестановками.

Настройки скрипта



После запуска скрипта у нас есть 100 дополнительных символов, основанных на переставленных тиках выборки символа AUDUSD.

Пользовательские символы в Обзоре



Наконец, запустим тест оптимизации.

Настройки тестера

  Настройки советника показаны ниже.

Параметры советника

Результаты теста.

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


На вкладке результатов тестера стратегий отображаются все показатели производительности, которые могут нас заинтересовать, а символы располагаются в порядке убывания на основе выбранных критериев производительности, которые можно выбрать в раскрывающемся меню в правом верхнем углу окна тестера. В этом представлении p-значение можно легко рассчитать вручную или, если необходимо, автоматически, обработав файл .xml, который при необходимости можно экспортировать из тестера щелчком правой кнопки мыши.

Используя этот пример, нам даже не нужно выполнять какие-либо вычисления, поскольку видно, что тестовые значения исходного символа находятся далеко внизу на вкладке результатов, а более 10 переставленных символов демонстрируют лучшую производительность. Это указывает на то, что p-значение превышает 0,05.

Конечно, к результату этого теста следует относиться скептически, поскольку выбранный период тестирования был очень коротким. Пользователям следует выбрать тестовый период, который будет гораздо более продолжительным и репрезентативным для условий, которые могут возникнуть в реальной торговле.

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

Очевидно, что после экспорта файла необходимо записать, где он сохранен. Откройте его с помощью любого приложения для работы с электронными таблицами. На рисунке ниже показано использование бесплатного OpenOffice Calc, в котором была добавлена новая строка внизу таблицы. Прежде чем идти дальше, было бы разумно удалить строки для символов, которые не должны участвовать в вычислениях. Под каждым соответствующим столбцом p-значение рассчитывается с использованием пользовательского макроса. Формула макроса использует показатели производительности переставленного символа (расположенные в строке 18 показанного документа), а также показатели производительности переставленных символов для каждого столбца. Полная формула макроса представлена на рисунке.

Расчет P-значений в OpenOffice Calc

Помимо использования приложения для работы с электронными таблицами, мы могли бы использовать Python, который имеет множество модулей для анализа XML-файлов. Если пользователь владеет MQL5, парсить файлы можно и простым скриптом. Просто не забудьте выбрать доступный каталог при экспорте результатов оптимизации из тестера.

Заключение

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

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

Анализ, в котором используются перестановочные данные о ценах, можно применять по-разному. Мы можем использовать метод для анализа поведения индикаторов, а также на разных этапах разработки стратегии. Возможности огромны. Иногда при разработке или тестировании стратегий может показаться, что данных недостаточно. Использование перестановочных ценовых рядов значительно увеличивает доступность данных для тестирования. Исходные коды всех описанных здесь mql5-программ приложены к статье. Надеюсь, они будут полезны читателям.

Имя файла
Тип программы
Описание
GenerateSymbols.mqh
Include-файл
Определение класса CGenerateSymbols для генерации символов с данными тиков, переставленными из выбранного базового символа
NewSymbol.mqh
Include-файл
Определение класса CNewSymbol для создания пользовательских символов
PermuteTicks.mqh
Include-файл
Определяет класс 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)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (3)
fxsaber
fxsaber | 19 дек. 2023 в 18:59
//+------------------------------------------------------------------+
//|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;
  }
Зачем логарифмировать объемы?!
fxsaber
fxsaber | 19 дек. 2023 в 19:12

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

Полная формула приведена ниже:

z+1/r+1

где r — количество выполненных перестановок, а z — общее количество тестов с лучшей производительностью.

Этот критерий не будет работать в таком случае - оптимизировали на исходном символе, а затем прогнали на перестановках.

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

Используемый алгоритм перестановки.

  1. Создается массив логарифмированных приращений (между соседними тиками) bid/ask.
  2. Этот массив перемешивается. Причем сильно.
  3. Создается новый массив тиков через приращения из п.2.

Такой подход убивает напрочь все закономерности (если они были), что были в исходном ряде. Т.к. на выходе получаем случайное блуждание.


Нельзя так делать.

Индикатор исторических позиций на графике в виде диаграммы их прибыли/убытка Индикатор исторических позиций на графике в виде диаграммы их прибыли/убытка
В статье рассмотрим вариант получения информации о закрытых позициях по истории их сделок. Создадим простой индикатор, отображающий в виде диаграммы приблизительный профит/убыток позиций на каждом баре.
Теория категорий в MQL5 (Часть 17): Функторы и моноиды Теория категорий в MQL5 (Часть 17): Функторы и моноиды
Это последняя статья серии, посвященная функторам. В ней мы вновь рассматриваем моноиды как категорию. Моноиды, которые мы уже представили в этой серии, используются здесь для помощи в определении размера позиции вместе с многослойными перцептронами.
Популяционные алгоритмы оптимизации: Алгоритмы эволюционных стратегий (Evolution Strategies, (μ,λ)-ES и (μ+λ)-ES) Популяционные алгоритмы оптимизации: Алгоритмы эволюционных стратегий (Evolution Strategies, (μ,λ)-ES и (μ+λ)-ES)
В этой статье будет рассмотрена группа алгоритмов оптимизации, известных как "Эволюционные стратегии" (Evolution Strategies или ES). Они являются одними из самых первых популяционных алгоритмов, использующих принципы эволюции для поиска оптимальных решений. Будут представлены изменения, внесенные в классические варианты ES, а также пересмотрена тестовая функция и методика стенда для алгоритмов.
Популяционные алгоритмы оптимизации: Изменяем форму и смещаем распределения вероятностей и тестируем на "Умном головастике" (Smart Cephalopod, SC) Популяционные алгоритмы оптимизации: Изменяем форму и смещаем распределения вероятностей и тестируем на "Умном головастике" (Smart Cephalopod, SC)
В данной статье исследуется влияние изменения формы распределений вероятностей на производительность алгоритмов оптимизации. Мы проводим эксперименты на тестовом алгоритме 'Умный головастик' (SC), чтобы оценить эффективность различных распределений вероятностей в контексте оптимизационных задач.