Тесты на перестановку Монте-Карло в MetaTrader 5
Введение
Алексей Николаев написал интересную статью "Применение метода Монте-Карло для оптимизации торговых стратегий". Она описывает тесты на перестановку, при которых сделки сменяются случайным образом. Автор кратко упоминает другой тип теста на перестановку, где последовательность ценовых данных меняется случайным образом, и производительность одного советника сравнивается с производительностью, достигнутой при тестировании на многочисленных других вариациях последовательности того же ценового ряда.
На мой взгляд, автор ошибочно предположил, что такой тест невозможно провести на произвольном советнике с помощью 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 показанного документа), а также показатели производительности переставленных символов для каждого столбца. Полная формула макроса представлена на рисунке.
Помимо использования приложения для работы с электронными таблицами, мы могли бы использовать 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
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
мы хотим, чтобы p-значения были как можно ближе к нулю, в диапазоне 0,05 и ниже.
Полная формула приведена ниже:
z+1/r+1
где r — количество выполненных перестановок, а z — общее количество тестов с лучшей производительностью.
Этот критерий не будет работать в таком случае - оптимизировали на исходном символе, а затем прогнали на перестановках.
Используемый алгоритм перестановки.
Такой подход убивает напрочь все закономерности (если они были), что были в исходном ряде. Т.к. на выходе получаем случайное блуждание.
Нельзя так делать.