Создание самооптимизирующихся советников на MQL5 (Часть 6): Самоадаптирующиеся торговые правила (II)
В предыдущей статье, касавшейся самооадаптирующихся торговых правил, мы рассмотрели проблемы, с которыми сталкивается алгоритмический трейдер, пытающийся следовать передовым методам работы с индикатором RSI.
Мы обнаружили, что стандартизированные результаты не всегда получаются на основе данного показателя, поскольку зависят от нескольких факторов, таких как период, таймфрейм, а также конкретный рассматриваемый рынок.
Для решения этой проблемы мы предположили, что алгоритмические трейдеры могли бы изучать истинный диапазон индикатора, чтобы корректировать его среднее значение в соответствии с серединой наблюдаемого диапазона, а не с его полным возможным диапазоном. Это дает нам определенные гарантии в отношении генерации торговых сигналов, которые мы не можем получить, используя традиционные правила RSI. Мы получили дополнительный контроль над новым сигналом, регистрируя среднее отклонение от средней точки и записывая только сигналы, генерируемые кратными этому среднему отклонению.
Теперь мы перейдем от нашей первоначальной попытки разработать практическое решение к следующему этапу. Есть несколько моментов, которые мы можем улучшить по сравнению с нашей последней попыткой. Главное улучшение, к которому мы стремимся, — это возможность попытаться оценить значение выбранных нами уровней RSI.
В нашем последнем обсуждении мы просто предположили, что отклонения, значительно превышающие среднее отклонение, как правило, могут быть более прибыльными. Однако мы не пытались проверить, так ли это на самом деле. Мы не пытались количественно оценить значение предлагаемых нами новых уровней и сравнить их со значением традиционных уровней, 70 и 30.
Кроме того, в нашем последнем обсуждении рассматривался случай, когда период RSI был фиксированным. Это упрощающее предположение сделало нашу концепцию более понятной. Сегодня мы обратим внимание на противоположную сторону проблемы, когда трейдер не уверен в правильности выбора периода.
Визуализация проблемы
На случай если к нам присоединились новые читатели, на рисунке 1 ниже приведен скриншот дневного графика EURUSD с RSI за 2 периода.

Рис. 1: Визуализация качества сигналов, генерируемых RSI за короткий период времени
Под рисунком 1 мы применили 70-периодный RSI к тому же участку графика, что и на рисунке 2. Мы можем наблюдать, как сигнал RSI постепенно превращается в прямую линию с центром на уровне 50 RSI. Какие комментарии можно сделать при сравнении этих двух рисунков? Стоит отметить, что за период, охватываемый обоими рисунками, EURUSD упал с уровня 1,121 18 сентября 2024 года до минимума в 1,051 ко 2 декабря 2024 года. Однако двухпериодный RSI слишком часто менял уровни за тот же период времени, а RSI с периодом 70 вообще не менял уровни.
Означает ли это, что трейдеры должны навсегда ограничиться использованием лишь узкого диапазона периодов при применении RSI? Что потребуется для разработки алгоритмов, которые будут автоматически выбирать оптимальный период RSI без вмешательства человека? Кроме того, как можно написать алгоритмы, которые помогут нам находить выгодные торговые уровни независимо от начального периода?

Рис. 2. Визуализация работы индикатора RSI в течение длительного периода
Начало работы с MQL5
Существует множество способов решения этой проблемы. Мы могли бы использовать библиотеки в Python для генерации показаний RSI с различными периодами и таким образом оптимизировать период и уровни RSI. Однако это может повлечь за собой определенные недостатки. Наибольшее ограничение может быть связано с незначительными различиями в методах вычисления значений этих технических индикаторов.Чтобы этого избежать, мы реализуем наше решение на языке MQL5. Создав класс RSI, мы можем быстро записывать множество значений RSI в один CSV-файл и использовать эти значения для проведения численного анализа в Python с целью оценки оптимального периода RSI и альтернативных уровней, помимо 70 и 30.
Начнем с создания скрипта, который позволит нам сначала вручную получить значения RSI и рассчитать изменение уровня RSI. Затем мы создадим класс, который будет инкапсулировать необходимую нам функциональность. Мы хотим создать сетку показаний RSI с периодами, увеличивающимися с шагом в 5, от 5 до 70. Но прежде чем мы сможем этого добиться, нам необходимо реализовать и протестировать наш класс.
Создание класса в скрипте позволит нам быстро проверить выходные данные класса, сравнив их со стандартными выходными данными, полученными вручную от индикатора. Если класс указан корректно, то результат, полученный обоими методами, должен быть одинаковым. Это позволит нам создать полезный класс для генерации 14 индикаторов RSI с различными периодами, отслеживая при этом изменение каждого значения RSI для любого другого символа, которым мы захотим торговать.
Учитывая цели использования класса RSI, имеет смысл убедиться, что в нем есть механизм, предотвращающий попытки считывания значения индикатора до тех пор, пока мы не установим соответствующий буфер. Начнем с создания этой части нашего класса. Нам необходимо объявить о приватных членах нашего класса. Эти приватные логические флаги не позволят нам считывать значения RSI до того, как мы скопируем их из буфера индикатора.
//+------------------------------------------------------------------+ //| ProjectName | //| Copyright 2020, CompanyName | //| http://www.companyname.net | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #property script_show_inputs //--- The RSI class will manage our indicator settings and provide useful transformations we need class RSI { //--- Private members private: //--- Have the indicator values been copied to the buffer? bool indicator_values_initialized; bool indicator_differenced_values_initialized;
Я также добавил метод, который возвращает строки, чтобы сообщить пользователю, что происходит внутри объекта и как решить возникшие проблемы. Метод принимает целочисленный параметр, указывающий, в какой части кода была сгенерирована ошибка. Таким образом, решение обычно имеет вид текстового сообщения в терминале.
//--- Give the user feedback string user_feedback(int flag) { string message; //--- Check if the RSI indicator loaded correctly if(flag == 0) { //--- Check the indicator was loaded correctly if(IsValid()) message = "RSI Indicator Class Loaded Correcrtly \n"; return(message); //--- Something went wrong message = "Error loading RSI Indicator: [ERROR] " + (string) GetLastError(); return(message); } //--- User tried getting indicator values before setting them if(flag == 1) { message = "Please set the indicator values before trying to fetch them from memory"; return(message); } //--- We sueccessfully set our differenced indicator values if(flag == 2) { message = "Succesfully set differenced indicator values."; return(message); } //--- Failed to set our differenced indicator values if(flag == 3) { message = "Failed to set our differenced indicator values: [ERROR] " + (string) GetLastError(); return(message); } //--- The user is trying to retrieve an index beyond the buffer size and must update the buffer size first if(flag == 4) { message = "The user is attempting to use call an index beyond the buffer size, update the buffer size first"; return(message); } //--- No feedback else return(""); }
Теперь мы определим защищенные члены нашего класса. Эти элементы будут представлять собой подвижные части, необходимые для инициализации экземпляра класса iRSI() и взаимодействия с буфером индикатора.
//--- Protected members protected: //--- The handler for our RSI int rsi_handler; //--- The Symbol our RSI should be applied on string rsi_symbol; //--- Our RSI period int rsi_period; //--- How far into the future we wish to forecast int forecast_horizon; //--- The buffer for our RSI indicator double rsi_reading[]; vector rsi_differenced_values; //--- The current size of the buffer the user last requested int rsi_buffer_size; int rsi_differenced_buffer_size; //--- The time frame our RSI should be applied on ENUM_TIMEFRAMES rsi_time_frame; //--- The price should the RSI be applied on ENUM_APPLIED_PRICE rsi_price;
Переходим к участникам открытого класса. Первая необходимая нам функция — это проверка корректности обработчика индикатора. Если наш обработчик индикаторов настроен неправильно, мы можем немедленно сообщить об этом пользователю.
//--- Now, we can define public members: public: //--- Check if our indicator handler is valid bool IsValid(void) { return((this.rsi_handler != INVALID_HANDLE)); }
Наш конструктор по умолчанию создаст объект iRSI, устанавливающий значение EURUSD на дневном графике на период в 5 дней. Чтобы убедиться, что это именно выбор пользователя, наш класс выводит название рынка и период, с которыми он работает. Кроме того, конструктор по умолчанию специально выводит пользователю информацию о том, что текущий экземпляр объекта RSI был создан именно этим конструктором.
//--- Our default constructor void RSI(void): indicator_values_initialized(false), rsi_symbol("EURUSD"), rsi_time_frame(PERIOD_D1), rsi_period(5), rsi_price(PRICE_CLOSE), rsi_handler(iRSI(rsi_symbol,rsi_time_frame,rsi_period,rsi_price)) { //--- Give the user feedback on initilization Print(user_feedback(0)); //--- Remind the user they called the default constructor Print("Default Constructor Called: ",__FUNCSIG__," ",&this); }
В противном случае мы ожидаем, что пользователь вызовет параметрический конструктор объекта RSI и укажет все необходимые параметры.
//--- Parametric constructor void RSI(string user_symbol,ENUM_TIMEFRAMES user_time_frame,int user_period,ENUM_APPLIED_PRICE user_price) { indicator_values_initialized = false; rsi_symbol = user_symbol; rsi_time_frame = user_time_frame; rsi_period = user_period; rsi_price = user_price; rsi_handler = iRSI(rsi_symbol,rsi_time_frame,rsi_period,rsi_price); //--- Give the user feedback on initilization Print(user_feedback(0)); }
Нам также понадобится деструктор, чтобы освободить ненужные системные ресурсы, что позволит нам очистить систему после завершения работы.
//--- Destructor void ~RSI(void) { //--- Free up resources we don't need and reset our flags if(IndicatorRelease(rsi_handler)) { indicator_differenced_values_initialized = false; indicator_values_initialized = false; Print("RSI System logging off"); } }
Теперь ключевым компонентом нашего класса являются методы, необходимые для взаимодействия с буфером индикатора. Мы просим пользователя указать, сколько значений следует скопировать из буфера и следует ли располагать значения последовательно. Затем мы проверяем, не возвращают ли значения RSI нам нулевое значение, прежде чем завершить вызов метода.
//--- Copy readings for our RSI indicator bool SetIndicatorValues(int buffer_size,bool set_as_series) { rsi_buffer_size = buffer_size; CopyBuffer(this.rsi_handler,0,0,buffer_size,rsi_reading); if(set_as_series) ArraySetAsSeries(this.rsi_reading,true); indicator_values_initialized = true; //--- Did something go wrong? vector rsi_test; rsi_test.CopyIndicatorBuffer(rsi_handler,0,0,buffer_size); if(rsi_test.Sum() == 0) return(false); //--- Everything went fine. return(true); }
Простая функция для получения текущего значения RSI.
//--- Get the current RSI reading double GetCurrentReading(void) { double temp[]; CopyBuffer(this.rsi_handler,0,0,1,temp); return(temp[0]); }
Нам также может потребоваться получить значения по определенному индексу, а не только самое последнее значение. Функция GetReadingAt() предоставляет нам эту вспомогательную возможность. Функция сначала проверяет, не пытаемся ли мы выйти за пределы размера буфера, скопированного из индикатора. При правильном использовании функция вернет показание индикатора по указанному индексу. В противном случае будет выведено сообщение об ошибке.
//--- Get a specific RSI reading double GetReadingAt(int index) { //--- Is the user trying to call indexes beyond the buffer? if(index > rsi_buffer_size) { Print(user_feedback(4)); return(-1e10); } //--- Get the reading at the specified index if((indicator_values_initialized) && (index < rsi_buffer_size)) return(rsi_reading[index]); //--- User is trying to get values that were not set prior else { Print(user_feedback(1)); return(-1e10); } }
Нас также интересует изменение значений RSI. Нам недостаточно иметь доступ к текущим показаниям RSI. Нам также необходим доступ к данным об изменении уровня RSI, происходящем в течение любого произвольного размера окна, которое мы укажем. Как и прежде, мы просто вызываем функции CopyBuffer, чтобы пользователь мог в фоновом режиме рассчитать рост уровня RSI, но класс также включает дополнительную проверку, чтобы убедиться, что результат вычисления не является вектором из 0, прежде чем вернуть пользователю найденный ответ.
//--- Let's set the conditions for our differenced data bool SetDifferencedIndicatorValues(int buffer_size,int differencing_period,bool set_as_series) { //--- Internal variables rsi_differenced_buffer_size = buffer_size; rsi_differenced_values = vector::Zeros(rsi_differenced_buffer_size); //--- Prepare to record the differences in our RSI readings double temp_buffer[]; int fetch = (rsi_differenced_buffer_size + (2 * differencing_period)); CopyBuffer(rsi_handler,0,0,fetch,temp_buffer); if(set_as_series) ArraySetAsSeries(temp_buffer,true); //--- Fill in our values iteratively for(int i = rsi_differenced_buffer_size;i > 1; i--) { rsi_differenced_values[i-1] = temp_buffer[i-1] - temp_buffer[i-1+differencing_period]; } //--- If the norm of a vector is 0, the vector is empty! if(rsi_differenced_values.Norm(VECTOR_NORM_P) != 0) { Print(user_feedback(2)); indicator_differenced_values_initialized = true; return(true); } indicator_differenced_values_initialized = false; Print(user_feedback(3)); return(false); }
Наконец, нам нужен метод для получения разностного значения RSI при определенном значении индекса. Опять же, наша функция гарантирует, что пользователь не попытается вызвать функцию за пределами диапазона скопированного буфера. В таком случае пользователю следует сначала обновить размер буфера, а затем скопировать соответствующее значение индекса.
//--- Get a differenced value at a specific index double GetDifferencedReadingAt(int index) { //--- Make sure we're not trying to call values beyond our index if(index > rsi_differenced_buffer_size) { Print(user_feedback(4)); return(-1e10); } //--- Make sure our values have been set if(!indicator_differenced_values_initialized) { //--- The user is trying to use values before they were set in memory Print(user_feedback(1)); return(-1e10); } //--- Return the differenced value of our indicator at a specific index if((indicator_differenced_values_initialized) && (index < rsi_differenced_buffer_size)) return(rsi_differenced_values[index]); //--- Something went wrong. return(-1e10); } };
Создание остальной части нашего теста не представляет сложности. Мы вручную проверим аналогичный экземпляр индикатора RSI, инициализированный с теми же настройками. Если мы запишем оба показания в один и тот же файл, то увидим дублирование информации. В противном случае мы бы допустили ошибку в реализации класса.
//--- How far we want to forecast #define HORIZON 10 //--- Our handlers for our indicators int rsi_5_handle; //--- Data structures to store the readings from our indicators double rsi_5_reading[]; //--- File name string file_name = Symbol() + " Testing RSI Class.csv"; //--- Amount of data requested input int size = 3000;
Для остальной части нашего скрипта нам нужно лишь инициализировать класс RSI и настроить его с теми же параметрами, которые мы будем использовать с дублированной, но управляемой вручную версией RSI.
//+------------------------------------------------------------------+ //| Our script execution | //+------------------------------------------------------------------+ void OnStart() { //--- Testing the RSI Class //--- Initialize the class RSI my_rsi(Symbol(),PERIOD_CURRENT,5,PRICE_CLOSE); my_rsi.SetIndicatorValues(size,true); my_rsi.SetDifferencedIndicatorValues(size,10,true); //---Setup our technical indicators rsi_5_handle = iRSI(Symbol(),PERIOD_CURRENT,5,PRICE_CLOSE); int fetch = size + (2 * HORIZON); //---Set the values as series CopyBuffer(rsi_5_handle,0,0,fetch,rsi_5_reading); ArraySetAsSeries(rsi_5_reading,true); //---Write to file int file_handle=FileOpen(file_name,FILE_WRITE|FILE_ANSI|FILE_CSV,","); for(int i=size;i>=1;i--) { if(i == size) { FileWrite(file_handle,"Time","RSI 5","RSI 5 Class","RSI 5 Difference","RSI 5 Class Difference"); } else { FileWrite(file_handle, iTime(_Symbol,PERIOD_CURRENT,i), rsi_5_reading[i], my_rsi.GetReadingAt(i), rsi_5_reading[i] - rsi_5_reading[i + HORIZON], my_rsi.GetDifferencedReadingAt(i) ); } } //--- Close the file FileClose(file_handle); } //+------------------------------------------------------------------+ #undef HORIZON
Если наш класс реализован корректно, наш тестовый скрипт создаст файл EURUSD Testing RSI Class, который будет содержать дублированные показания RSI. Как видно из рисунка 3, наш класс RSI успешно прошел тест. Этот класс экономит время, поскольку нам не приходится многократно реализовывать одни и те же методы в разных проектах. Мы можем просто импортировать наш класс RSI и вызывать необходимые методы.

Рис. 3. Наш класс индикаторов RSI прошел проверку. Его использование идентично ручной работе с классом индикаторов RSI
Теперь, когда мы уверены в правильности реализации класса, давайте выделим его в отдельный включаемый файл, чтобы мы могли использовать его в нашем советнике и в любых других будущих задачах, которые могут потребовать аналогичного набора функций. В собранном виде наш класс выглядит так:
//+------------------------------------------------------------------+ //| RSI.mqh | //| Gamuchirai Ndawana | //| https://www.mql5.com/en/users/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com/en/users/gamuchiraindawa" #property version "1.00" //+------------------------------------------------------------------+ //| This class will provide us with usefull functionality | //+------------------------------------------------------------------+ class RSI { private: //--- Have the indicator values been copied to the buffer? bool indicator_values_initialized; bool indicator_differenced_values_initialized; //--- Give the user feedback string user_feedback(int flag); protected: //--- The handler for our RSI int rsi_handler; //--- The Symbol our RSI should be applied on string rsi_symbol; //--- Our RSI period int rsi_period; //--- How far into the future we wish to forecast int forecast_horizon; //--- The buffer for our RSI indicator double rsi_reading[]; vector rsi_differenced_values; //--- The current size of the buffer the user last requested int rsi_buffer_size; int rsi_differenced_buffer_size; //--- The time frame our RSI should be applied on ENUM_TIMEFRAMES rsi_time_frame; //--- The price should the RSI be applied on ENUM_APPLIED_PRICE rsi_price; public: RSI(); RSI(string user_symbol,ENUM_TIMEFRAMES user_time_frame,int user_period,ENUM_APPLIED_PRICE user_price); ~RSI(); bool SetIndicatorValues(int buffer_size,bool set_as_series); bool IsValid(void); double GetCurrentReading(void); double GetReadingAt(int index); bool SetDifferencedIndicatorValues(int buffer_size,int differencing_period,bool set_as_series); double GetDifferencedReadingAt(int index); }; //+------------------------------------------------------------------+ //| Our default constructor for our RSI class | //+------------------------------------------------------------------+ void RSI::RSI() { indicator_values_initialized = false; rsi_symbol = "EURUSD"; rsi_time_frame = PERIOD_D1; rsi_period = 5; rsi_price = PRICE_CLOSE; rsi_handler = iRSI(rsi_symbol,rsi_time_frame,rsi_period,rsi_price); //--- Give the user feedback on initilization Print(user_feedback(0)); //--- Remind the user they called the default constructor Print("Default Constructor Called: ",__FUNCSIG__," ",&this); } //+------------------------------------------------------------------+ //| Our parametric constructor for our RSI class | //+------------------------------------------------------------------+ void RSI::RSI(string user_symbol,ENUM_TIMEFRAMES user_time_frame,int user_period,ENUM_APPLIED_PRICE user_price) { indicator_values_initialized = false; rsi_symbol = user_symbol; rsi_time_frame = user_time_frame; rsi_period = user_period; rsi_price = user_price; rsi_handler = iRSI(rsi_symbol,rsi_time_frame,rsi_period,rsi_price); //--- Give the user feedback on initilization Print(user_feedback(0)); } //+------------------------------------------------------------------+ //| Our destructor for our RSI class | //+------------------------------------------------------------------+ void RSI::~RSI() { //--- Free up resources we don't need and reset our flags if(IndicatorRelease(rsi_handler)) { indicator_differenced_values_initialized = false; indicator_values_initialized = false; Print(user_feedback(5)); } } //+------------------------------------------------------------------+ //| Get our current reading from the RSI indicator | //+------------------------------------------------------------------+ double RSI::GetCurrentReading(void) { double temp[]; CopyBuffer(this.rsi_handler,0,0,1,temp); return(temp[0]); } //+------------------------------------------------------------------+ //| Set our indicator values and our buffer size | //+------------------------------------------------------------------+ bool RSI::SetIndicatorValues(int buffer_size,bool set_as_series) { rsi_buffer_size = buffer_size; CopyBuffer(this.rsi_handler,0,0,buffer_size,rsi_reading); if(set_as_series) ArraySetAsSeries(this.rsi_reading,true); indicator_values_initialized = true; //--- Did something go wrong? vector rsi_test; rsi_test.CopyIndicatorBuffer(rsi_handler,0,0,buffer_size); if(rsi_test.Sum() == 0) return(false); //--- Everything went fine. return(true); } //+--------------------------------------------------------------+ //| Let's set the conditions for our differenced data | //+--------------------------------------------------------------+ bool RSI::SetDifferencedIndicatorValues(int buffer_size,int differencing_period,bool set_as_series) { //--- Internal variables rsi_differenced_buffer_size = buffer_size; rsi_differenced_values = vector::Zeros(rsi_differenced_buffer_size); //--- Prepare to record the differences in our RSI readings double temp_buffer[]; int fetch = (rsi_differenced_buffer_size + (2 * differencing_period)); CopyBuffer(rsi_handler,0,0,fetch,temp_buffer); if(set_as_series) ArraySetAsSeries(temp_buffer,true); //--- Fill in our values iteratively for(int i = rsi_differenced_buffer_size;i > 1; i--) { rsi_differenced_values[i-1] = temp_buffer[i-1] - temp_buffer[i-1+differencing_period]; } //--- If the norm of a vector is 0, the vector is empty! if(rsi_differenced_values.Norm(VECTOR_NORM_P) != 0) { Print(user_feedback(2)); indicator_differenced_values_initialized = true; return(true); } indicator_differenced_values_initialized = false; Print(user_feedback(3)); return(false); } //--- Get a differenced value at a specific index double RSI::GetDifferencedReadingAt(int index) { //--- Make sure we're not trying to call values beyond our index if(index > rsi_differenced_buffer_size) { Print(user_feedback(4)); return(-1e10); } //--- Make sure our values have been set if(!indicator_differenced_values_initialized) { //--- The user is trying to use values before they were set in memory Print(user_feedback(1)); return(-1e10); } //--- Return the differenced value of our indicator at a specific index if((indicator_differenced_values_initialized) && (index < rsi_differenced_buffer_size)) return(rsi_differenced_values[index]); //--- Something went wrong. return(-1e10); } //+------------------------------------------------------------------+ //| Get a reading at a specific index from our RSI buffer | //+------------------------------------------------------------------+ double RSI::GetReadingAt(int index) { //--- Is the user trying to call indexes beyond the buffer? if(index > rsi_buffer_size) { Print(user_feedback(4)); return(-1e10); } //--- Get the reading at the specified index if((indicator_values_initialized) && (index < rsi_buffer_size)) return(rsi_reading[index]); //--- User is trying to get values that were not set prior else { Print(user_feedback(1)); return(-1e10); } } //+------------------------------------------------------------------+ //| Check if our indicator handler is valid | //+------------------------------------------------------------------+ bool RSI::IsValid(void) { return((this.rsi_handler != INVALID_HANDLE)); } //+------------------------------------------------------------------+ //| Give the user feedback on the actions he is performing | //+------------------------------------------------------------------+ string RSI::user_feedback(int flag) { string message; //--- Check if the RSI indicator loaded correctly if(flag == 0) { //--- Check the indicator was loaded correctly if(IsValid()) message = "RSI Indicator Class Loaded Correcrtly \nSymbol: " + (string) rsi_symbol + "\nPeriod: " + (string) rsi_period; return(message); //--- Something went wrong message = "Error loading RSI Indicator: [ERROR] " + (string) GetLastError(); return(message); } //--- User tried getting indicator values before setting them if(flag == 1) { message = "Please set the indicator values before trying to fetch them from memory, call SetIndicatorValues()"; return(message); } //--- We sueccessfully set our differenced indicator values if(flag == 2) { message = "Succesfully set differenced indicator values."; return(message); } //--- Failed to set our differenced indicator values if(flag == 3) { message = "Failed to set our differenced indicator values: [ERROR] " + (string) GetLastError(); return(message); } //--- The user is trying to retrieve an index beyond the buffer size and must update the buffer size first if(flag == 4) { message = "The user is attempting to use call an index beyond the buffer size, update the buffer size first"; return(message); } //--- The class has been deactivated by the user if(flag == 5) { message = "Goodbye."; return(message); } //--- No feedback else return(""); } //+------------------------------------------------------------------+
Теперь давайте получим необходимые рыночные данные, используя набор экземпляров нашего класса RSI. Мы будем хранить указатели на каждый экземпляр нашего класса в массиве пользовательского типа, который мы определили. MQL5 позволяет нам автоматически генерировать объекты на лету по мере необходимости. Однако такая гибкость достигается необходимостью постоянно удалять ненужное, чтобы предотвратить проблемы, связанные с утечкой памяти.
//+------------------------------------------------------------------+ //| ProjectName | //| Copyright 2020, CompanyName | //| http://www.companyname.net | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #property script_show_inputs //+------------------------------------------------------------------+ //| System constants | //+------------------------------------------------------------------+ #define HORIZON 10 //+------------------------------------------------------------------+ //| Libraries | //+------------------------------------------------------------------+ #include <VolatilityDoctor\Indicators\RSI.mqh> //+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ RSI *my_rsi_array[14]; string file_name = Symbol() + " RSI Algorithmic Input Selection.csv"; //+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ input int size = 3000; //+------------------------------------------------------------------+ //| Our script execution | //+------------------------------------------------------------------+ void OnStart() { //--- How much data should we store in our indicator buffer? int fetch = size + (2 * HORIZON); //--- Store pointers to our RSI objects for(int i = 0; i <= 13; i++) { //--- Create an RSI object my_rsi_array[i] = new RSI(Symbol(),PERIOD_CURRENT,((i+1) * 5),PRICE_CLOSE); //--- Set the RSI buffers my_rsi_array[i].SetIndicatorValues(fetch,true); my_rsi_array[i].SetDifferencedIndicatorValues(fetch,HORIZON,true); } //---Write to file int file_handle=FileOpen(file_name,FILE_WRITE|FILE_ANSI|FILE_CSV,","); for(int i=size;i>=1;i--) { if(i == size) { FileWrite(file_handle,"Time","True Close","Open","High","Low","Close","RSI 5","RSI 10","RSI 15","RSI 20","RSI 25","RSI 30","RSI 35","RSI 40","RSI 45","RSI 50","RSI 55","RSI 60","RSI 65","RSI 70","Diff RSI 5","Diff RSI 10","Diff RSI 15","Diff RSI 20","Diff RSI 25","Diff RSI 30","Diff RSI 35","Diff RSI 40","Diff RSI 45","Diff RSI 50","Diff RSI 55","Diff RSI 60","Diff RSI 65","Diff RSI 70"); } else { FileWrite(file_handle, iTime(_Symbol,PERIOD_CURRENT,i), iClose(_Symbol,PERIOD_CURRENT,i), iOpen(_Symbol,PERIOD_CURRENT,i) - iOpen(Symbol(),PERIOD_CURRENT,i + HORIZON), iHigh(_Symbol,PERIOD_CURRENT,i) - iHigh(Symbol(),PERIOD_CURRENT,i + HORIZON), iLow(_Symbol,PERIOD_CURRENT,i) - iLow(Symbol(),PERIOD_CURRENT,i + HORIZON), iClose(_Symbol,PERIOD_CURRENT,i) - iClose(Symbol(),PERIOD_CURRENT,i + HORIZON), my_rsi_array[0].GetReadingAt(i), my_rsi_array[1].GetReadingAt(i), my_rsi_array[2].GetReadingAt(i), my_rsi_array[3].GetReadingAt(i), my_rsi_array[4].GetReadingAt(i), my_rsi_array[5].GetReadingAt(i), my_rsi_array[6].GetReadingAt(i), my_rsi_array[7].GetReadingAt(i), my_rsi_array[8].GetReadingAt(i), my_rsi_array[9].GetReadingAt(i), my_rsi_array[10].GetReadingAt(i), my_rsi_array[11].GetReadingAt(i), my_rsi_array[12].GetReadingAt(i), my_rsi_array[13].GetReadingAt(i), my_rsi_array[0].GetDifferencedReadingAt(i), my_rsi_array[1].GetDifferencedReadingAt(i), my_rsi_array[2].GetDifferencedReadingAt(i), my_rsi_array[3].GetDifferencedReadingAt(i), my_rsi_array[4].GetDifferencedReadingAt(i), my_rsi_array[5].GetDifferencedReadingAt(i), my_rsi_array[6].GetDifferencedReadingAt(i), my_rsi_array[7].GetDifferencedReadingAt(i), my_rsi_array[8].GetDifferencedReadingAt(i), my_rsi_array[9].GetDifferencedReadingAt(i), my_rsi_array[10].GetDifferencedReadingAt(i), my_rsi_array[11].GetDifferencedReadingAt(i), my_rsi_array[12].GetDifferencedReadingAt(i), my_rsi_array[13].GetDifferencedReadingAt(i) ); } } //--- Close the file FileClose(file_handle); //--- Delete our RSI object pointers for(int i = 0; i <= 13; i++) { delete my_rsi_array[i]; } } //+------------------------------------------------------------------+ #undef HORIZON
Анализ данных в Python
Теперь мы можем приступить к анализу данных, собранных с помощью терминала MetaTrader 5. Первым делом мы загрузим необходимые нам стандартные библиотеки.
import pandas as pd import numpy as np import seaborn as sns import matplotlib.pyplot as plt import plotly
Теперь считаем рыночные данные. Также давайте создадим флаг, который будет указывать, превышает ли текущий уровень цену, предложенную 10 дней назад, или нет. Напомним, что 10 дней — это тот же период, который мы использовали в нашем скрипте для расчета изменения уровня RSI.
#Let's read in our market data data = pd.read_csv("EURUSD RSI Algorithmic Input Selection.csv") data['Bull'] = np.NaN data.loc[data['True Close'] > data['True Close'].shift(10),'Bull'] = 1 data.loc[data['True Close'] < data['True Close'].shift(10),'Bull'] = 0 data.dropna(inplace=True) data.reset_index(inplace=True,drop=True)
Нам также необходимо рассчитать фактическую рыночную доходность.
#Estimate the market returns #Define our forecast horizon HORIZON = 10 data['Target'] = 0 data['Return'] = data['True Close'].shift(-HORIZON) - data['True Close'] data.loc[data['Return'] > 0,'Target'] = 1 #Drop missing values data.dropna(inplace=True)
Most importantly, we need to delete all the data that overlaps with our intended back test period.
#No cheating boys. _ = data.iloc[((-365 * 3) + 95):,:] data = data.iloc[:((-365 * 3) + 95),:] data

Рис. 4. Наш набор данных больше не содержит ни одной даты, совпадающей с периодом нашего тестирования на истории
Мы можем визуализировать распределение доходности рынка EURUSD за 10 дней и быстро увидеть, что доходность рынка остается стабильной около нуля. Такая общая форма распределения не вызывает особого удивления и не является уникальной для пары EURUSD.
plt.title('Distribution of EURUSD 10 Day Returns')
plt.grid()
sns.histplot(data['Return'],color='black') 
Рис. 5. Визуализация распределения доходности EURUSD за 10 дней
Это упражнение предоставляет нам уникальную возможность визуально увидеть разницу между распределением уровней RSI за короткий период и за длительный период. Пунктирные вертикальные красные линии обозначают стандартизированные уровни 30 и 70. Черные столбцы показывают распределение уровней RSI при периоде 5. Мы видим, что 5-периодный RSI будет генерировать множество сигналов, превышающих стандартизированные уровни. Белые столбцы представляют распределение уровней RSI при установке периода на 70. Визуально мы видим, что сигналы практически не будут генерироваться. Именно это изменение формы распределения создает сложности для алгоритмических трейдеров в соблюдении «лучших практик» использования индикатора.
plt.title('Comapring The Distribution of RSI Changes Across Different RSI Periods')
sns.histplot(data['RSI 5'],color='black')
sns.histplot(data['RSI 70'],color='white')
plt.xlabel('RSI Level')
plt.legend(['RSI 5','RSI 70'])
plt.axvline(30,color='red',linestyle='--')
plt.axvline(70,color='red',linestyle='--')
plt.grid() 
Рис. 6. Сравнение распределения уровней RSI в разные периоды RSI
Построение диаграммы рассеяния с 10-периодным изменением 60-периодного RSI по осям x и y позволяет визуализировать наличие взаимосвязи между изменением индикатора и целевым значением. Похоже, что изменение уровня RSI на 10 уровней может быть разумным торговым сигналом для открытия коротких позиций, если значение RSI снизилось на 10 уровней. Or enter long positions if the RSI level increased by 10.
plt.title('Scatter Plot of 10 Day Change in 50 Period RSI & EURUSD 10 Day Return')
sns.scatterplot(data,y='Diff RSI 60',x='Diff RSI 60',hue='Target')
plt.xlabel('50 Period RSI')
plt.ylabel('50 Period RSI')
plt.grid()
plt.axvline(-10,color='red',linestyle='--')
plt.axvline(10,color='red',linestyle='--') 
Рис. 7. Визуализация взаимосвязи между изменением 60-периодного RSI и целевым значением
Попытка объединить сигналы, генерируемые изменениями RSI за разные периоды, может показаться вполне разумной идеей. Однако, похоже, это мало что меняет в плане более четкого разграничения двух интересующих нас групп.
plt.title('Scatter Plot of 10 Day Change in 5 Period RSI & EURUSD 10 Day Return')
sns.scatterplot(data,y='Diff RSI 60',x='Diff RSI 5',hue='Target')
plt.xlabel('5 Period RSI')
plt.ylabel('5 Period RSI')
plt.grid()

Рис. 8. По всей видимости, простое использование индикаторов RSI с разными периодами является плохим источником подтверждения
У нас есть 14 различных индикаторов RSI на выбор. Вместо того чтобы проводить 14 тестирований на истории, чтобы определить, какой период может быть для нас наиболее подходящим, мы можем оценить оптимальный период, оценив производительность модели, обученной на всех 14 индикаторах RSI в качестве входных данных, а затем оценив важность признаков, которые статистическая модель изучила на основе данных, на которых она обучалась.
Обратите внимание, что у нас всегда есть выбор: использовать статистические модели для повышения точности прогнозирования или для интерпретации и получения ценных выводов. В этой статье мы займемся последним. Мы построим ридж-модель, используя разницу во всех 14 входных данных индикатора RSI. Модель Ridge имеет собственные параметры настройки. Поэтому мы выполним поиск по сетке в пространстве входных данных для статистической ридж-модели. В частности, мы будем корректировать параметры настройки:
- Alpha - ридж-модель требует добавления штрафной функции для контроля коэффициентов модели.
- Tolerance (допуск) - определяет наименьшее изменение, необходимое для установки условий остановки/других условий для подпрограмм в зависимости от выбранного пользователем метода решения.
Для нашего обсуждения мы будем использовать ридж-модель с методом sparse_cg. Читатель также может по своему желанию доработать модель.
Ридж-модель особенно полезна для нас, поскольку она сводит свои коэффициенты к нулю, чтобы уменьшить потери модели. Поэтому мы проведем поиск в широком диапазоне начальных настроек для нашей модели, а затем сосредоточимся на тех начальных настройках, которые привели к наименьшей ошибке. Конфигурация весовых коэффициентов модели в режиме максимальной производительности позволяет быстро определить, от какого периода RSI наша модель зависела в наибольшей степени. В нашем сегодняшнем примере наибольшие коэффициенты в нашей наиболее эффективной модели были получены при изменении RSI на 10 периодов (55 периодов).
#Set the max levels we wish to check ALPHA_LEVELS = 10 TOL_LEVELS = 10 #DataFrame labels r_c = 'TOL_LEVEL_' r_r = 'ALHPA_LEVEL_' results_columns = [] results_rows = [] for c in range(TOL_LEVELS): n_c = r_c + str(c) n_r = r_r + str(c) results_columns.append(n_c) results_rows.append(n_r) #Create a DataFrame to store our results results = pd.DataFrame(columns=results_columns,index=results_rows) #Cross validate our model for i in range(TOL_LEVELS): tol = 10 ** (-i) error = [] for j in range(ALPHA_LEVELS): #Set alpha alpha = 10 ** (-j) #Its good practice to generally check the 0 case if(i == 0 & j == 0): model = Ridge(alpha=j,tol=i,solver='sparse_cg') #Otherwise use a float model = Ridge(alpha=alpha,tol=tol,solver='sparse_cg') #Store the error levels error.append(np.mean(np.abs(cross_val_score(model,data.loc[:,['Diff RSI 5', 'Diff RSI 10', 'Diff RSI 15', 'Diff RSI 20', 'Diff RSI 25', 'Diff RSI 30', 'Diff RSI 35', 'Diff RSI 40', 'Diff RSI 45', 'Diff RSI 50', 'Diff RSI 55', 'Diff RSI 60', 'Diff RSI 65', 'Diff RSI 70',]],data['Return'],cv=tscv)))) #Record the error levels results.iloc[:,i] = error results
В таблице ниже приведено краткое изложение наших результатов. Мы отмечаем, что наименьший уровень ошибок был получен, когда мы установили начальные параметры нашей модели равными 0.
| Параметр настройки | Ошибка модели |
|---|---|
| ALHPA_LEVEL_0 | 0.053509 |
| ALHPA_LEVEL_1 | 0.056245 |
| ALHPA_LEVEL_2 | 0.060158 |
| ALHPA_LEVEL_3 | 0.062230 |
| ALHPA_LEVEL_4 | 0.061521 |
| ALHPA_LEVEL_5 | 0.064312 |
| ALHPA_LEVEL_6 | 0.073248 |
| ALHPA_LEVEL_7 | 0.079310 |
| ALHPA_LEVEL_8 | 0.081914 |
| ALHPA_LEVEL_9 | 0.085171 |
Также наши результаты можно визуализировать с помощью контурного графика. Мы хотим использовать модели в области графика, связанной с низкой погрешностью, то есть в синих областях. Это наши лучшие модели на данный момент. Теперь давайте визуализируем величину каждого коэффициента в модели. Наибольший коэффициент будет присвоен тому входному параметру, от которого наша модель в наибольшей степени зависела.
import plotly.graph_objects as go fig = go.Figure(data = go.Contour( z=results, colorscale='bluered' )) fig.update_layout( width = 600, height = 400, title='Contour Plot Of Our Error Forecasting EURUSD Using Grid Search ' ) fig.show()

Рис. 9: Мы нашли оптимальные входные параметры для нашей модели Ridge, прогнозирующей доходность EURUSD за 10 дней
Визуально отобразив данные на графике, мы можем быстро увидеть, что коэффициенту, связанному с 55-периодным RSI, было присвоено наибольшее абсолютное значение. Это вселяет в нас уверенность в том, что мы можем сосредоточиться именно на этой конкретной конфигурации RSI.
#Let's visualize the importance of each column model = Ridge(alpha=0,tol=0,solver='sparse_cg') model.fit(data.loc[:,['Diff RSI 5', 'Diff RSI 10', 'Diff RSI 15', 'Diff RSI 20', 'Diff RSI 25', 'Diff RSI 30', 'Diff RSI 35', 'Diff RSI 40', 'Diff RSI 45', 'Diff RSI 50', 'Diff RSI 55', 'Diff RSI 60', 'Diff RSI 65', 'Diff RSI 70',]],data['Return']) #Clearly our model relied on the 25 Period RSI the most, from all the data it had available at training sns.barplot(np.abs(model.coef_),color='black') plt.title('Rleative Feature Importance') plt.ylabel('Coefficient Value') plt.xlabel('Coefficient Index') plt.grid()

Рис. 10. Коэффициенту, связанному с разностью RSI за 55 периодов, было присвоено наибольшее значение
Теперь, когда мы определили интересующий нас период, давайте также оценим, как изменяется ошибка нашей модели по мере того, как мы циклически проходим через возрастающие уровни RSI, равные 10. Мы создадим 3 дополнительных столбца в нашем фрейме данных. В столбце 1 будет значение 1, если показание RSI превышает первое значение, которое мы хотим проверить. В противном случае, если показание RSI меньше определенного значения, которое мы хотим проверить, во втором столбце будет значение 1. В любом другом случае столбец 3 будет иметь значение 1.
def objective(x): data = pd.read_csv("EURUSD RSI Algorithmic Input Selection.csv") data['0'] = 0 data['1'] = 0 data['2'] = 0 HORIZON = 10 data['Return'] = data['True Close'].shift(-HORIZON) - data['True Close'] data.dropna(subset=['Return'],inplace=True) data.iloc[data['Diff RSI 55'] > x[0],12] = 1 data.iloc[data['Diff RSI 55'] < x[1],13] = 1 data.iloc[(data['Diff RSI 55'] < x[0]) & (data['RSI 55'] > x[1]),14] = 1 #Calculate or RMSE When using those levels model = Ridge(alpha=0,tol=0,solver='sparse_cg') error = np.mean(np.abs(cross_val_score(model,data.iloc[:,12:15],data['Return'],cv=tscv))) return(error)
Давайте оценим ошибку, возникающую в нашей модели, если мы примем за критический уровень 0.
#Bad rules for using the RSI objective([0,0])
0.026897725573317266
Если мы подставим в нашу функцию значения 70 и 30, ошибка возрастет. Мы проведем поиск по сетке с шагом в 10, чтобы найти изменения уровней RSI, которые лучше подходят для 55-периодного RSI. Наши результаты показали, что оптимальный уровень изменения находится вблизи 10 уровней RSI.
#Bad rules for using the RSI objective([70,30])
0.031258730612736006
LEVELS = 10 results = [] for i in np.arange(0,(LEVELS)): results.append(objective([i * 10,-(i * 10)])) plt.plot(results,color='black') plt.ylabel('Error Rate') plt.xlabel('Change in RSI as multiples of 10') plt.grid() plt.scatter(results.index(min(results)),min(results),color='red') plt.title('Measuring The Strength of Changes In RSI Levels')

Рис. 11. Визуализация оптимального уровня изменения индикатора RSI
Теперь проведем еще один, более точный поиск, в интервале изменений RSI от 0 до 20. При более внимательном рассмотрении мы видим, что истинный оптимум, по-видимому, находится при значении 9. Однако мы не стремимся к идеальному соответствию историческим данным, это называется переобучением и является плохой практикой. Мы не занимаемся подгонкой кривых. Вместо того чтобы брать оптимальное значение точно там, где оно появляется в нашем анализе исторических данных, мы принимаем тот факт, что положение оптимума может меняться, и вместо этого стремимся находиться в пределах определенной доли стандартного отклонения от оптимального значения с обеих сторон, чтобы использовать эти значения в качестве доверительных интервалов.
LEVELS = 20 coef = 0.5 results = [] for i in np.arange(0,(LEVELS),1): results.append(objective([i,-(i)])) plt.plot(results) plt.ylabel('Error Rate') plt.xlabel('Change in RSI') plt.grid() plt.scatter(results.index(min(results)),min(results),color='red') plt.title('Measuring The Strength of Changes In RSI Levels') plt.axvline(results.index(min(results)),color='red') plt.axvline(results.index(min(results)) - (coef * np.std(data['Diff RSI 55'])),linestyle='--',color='red') plt.axvline(results.index(min(results)) + (coef * np.std(data['Diff RSI 55'])),linestyle='--',color='red')

Рис. 12. Визуализация частоты ошибок, связанных с установкой различных пороговых значений для изменений уровня RSI
Мы можем визуально увидеть область, которая, по нашему мнению, может быть оптимальной, наложенную на историческое распределение изменений RSI.
sns.histplot(data['Diff RSI 55'],color='black') coef = 0.5 plt.axvline((results.index(min(results))),linestyle='--',color='red') plt.axvline(results.index(min(results)) - (coef * np.std(data['Diff RSI 55'])),color='red') plt.axvline(results.index(min(results)) + (coef * np.std(data['Diff RSI 55'])),color='red') plt.axvline(-(results.index(min(results))),linestyle='--',color='red') plt.axvline(-(results.index(min(results)) - (coef * np.std(data['Diff RSI 55']))),color='red') plt.axvline(-(results.index(min(results)) + (coef * np.std(data['Diff RSI 55']))),color='red') plt.title("Visualizing our Optimal Point in The Distribution")

Рис. 13. Визуализация оптимальных регионов, которые мы выбрали для наших торговых сигналов RSI
Давайте получим значения наших оценок доверительных интервалов; они послужат критическими значениями в нашем советнике, которые будут запускать сигналы на покупку и продажу.
results.index(min(results)) + ( coef * np.std(data['Diff RSI 55']))
10.822857254027287
И наша нижняя граница.
results.index(min(results)) - (coef * np.std(data['Diff RSI 55']))
7.177142745972713
Давайте получим объяснение от нашей ридж-модели, чтобы интерпретировать индикатор RSI способом, о котором мы, возможно, не догадывались.
def explanation(x): data = pd.read_csv("EURUSD RSI Algorithmic Input Selection.csv") data['0'] = 0 data['1'] = 0 data['2'] = 0 HORIZON = 10 data['Return'] = data['True Close'].shift(-HORIZON) - data['True Close'] data.dropna(subset=['Return'],inplace=True) data.iloc[data['Diff RSI 55'] > x[0],12] = 1 data.iloc[data['Diff RSI 55'] < x[1],13] = 1 data.iloc[(data['Diff RSI 55'] < x[0]) & (data['RSI 55'] > x[1]),14] = 1 #Calculate or RMSE When using those levels model = Ridge(alpha=0,tol=0,solver='sparse_cg') model.fit(data.iloc[:,12:15],data['Return']) return(model.coef_.copy())
Мы видим, что когда индикатор RSI изменяется более чем на 9 (наше оптимальное значение), наша модель обучается положительным коэффициентам, что подразумевает необходимость открытия длинных позиций. В противном случае, согласно модели, мы должны продавать при любых других условиях.
opt = 9 print(explanation([opt,-opt]))
[ 1.97234840e-04 -1.64215118e-04 -7.55222156e-05]
Создание советника
Предположим, что наши знания о прошлом являются хорошей моделью для будущего. Можем ли мы создать приложение для прибыльной торговли парой EURUSD, используя полученные нами знания о рынке? Для начала определим важные системные константы, которые понадобятся нам на протяжении всей программы и во всех последующих версиях, которые мы можем создать.
//+------------------------------------------------------------------+ //| Algorithmic Input Selection.mq5 | //| Gamuchirai Ndawana | //| https://www.mql5.com/en/users/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com/en/users/gamuchiraindawa" #property version "1.00" //+------------------------------------------------------------------+ //| System constants | //+------------------------------------------------------------------+ #define RSI_PERIOD 55 #define RSI_TIME_FRAME PERIOD_D1 #define SYSTEM_TIME_FRAME PERIOD_D1 #define RSI_PRICE PRICE_CLOSE #define RSI_BUFFER_SIZE 20 #define TRADING_VOLUME SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN)
Загрузим наши библиотеки.
//+------------------------------------------------------------------+ //| Load our RSI library | //+------------------------------------------------------------------+ #include <VolatilityDoctor\Indicators\RSI.mqh> #include <Trade\Trade.mqh>
Нам понадобится несколько глобальных переменных.
//+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ CTrade Trade; RSI rsi_55(Symbol(),RSI_TIME_FRAME,RSI_PERIOD,RSI_PRICE); double last_value; int count; int ma_o_handler,ma_c_handler; double ma_o[],ma_c[]; double trade_sl;
Наши специалисты по организации мероприятий будут использовать свой собственный выделенный метод для обработки подпроцессов, необходимых для выполнения своих задач.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- setup(); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- IndicatorRelease(ma_c_handler); IndicatorRelease(ma_o_handler); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- update(); } //+------------------------------------------------------------------+
Функция обновления обновляет все системные переменные и проверяет, нужно ли нам открыть позицию или управлять уже открытыми позициями.
//+------------------------------------------------------------------+ //| Update our system variables | //+------------------------------------------------------------------+ void update(void) { static datetime time_stamp; datetime current_time = iTime(Symbol(),SYSTEM_TIME_FRAME,0); if(time_stamp != current_time) { time_stamp = current_time; CopyBuffer(ma_c_handler,0,0,1,ma_c); CopyBuffer(ma_o_handler,0,0,1,ma_o); if((count == 0) && (PositionsTotal() == 0)) { rsi_55.SetIndicatorValues(RSI_BUFFER_SIZE,true); last_value = rsi_55.GetReadingAt(RSI_BUFFER_SIZE - 1); count = 1; } if(PositionsTotal() == 0) check_signal(); if(PositionsTotal() > 0) manage_setup(); } }
Необходимо постоянно корректировать стоп-лоссы по нашим позициям, чтобы по возможности снижать уровень риска.
//+------------------------------------------------------------------+ //| Manage our open trades | //+------------------------------------------------------------------+ void manage_setup(void) { double bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); double ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); if(PositionSelect(Symbol())) { double current_sl = PositionGetDouble(POSITION_SL); double current_tp = PositionGetDouble(POSITION_TP); double new_sl = (current_tp > current_sl) ? (bid-trade_sl) : (ask+trade_sl); double new_tp = (current_tp < current_sl) ? (bid+trade_sl) : (ask-trade_sl); //--- Buy setup if((current_tp > current_sl) && (new_sl < current_sl)) Trade.PositionModify(Symbol(),new_sl,new_tp); //--- Sell setup if((current_tp < current_sl) && (new_sl > current_sl)) Trade.PositionModify(Symbol(),new_sl,new_tp); } }
Наша функция настройки отвечает за запуск системы с нуля. Это подготовит необходимые нам индикаторы и сбросит счетчики.
//+------------------------------------------------------------------+ //| Setup our global variables | //+------------------------------------------------------------------+ void setup(void) { ma_c_handler = iMA(Symbol(),SYSTEM_TIME_FRAME,2,0,MODE_EMA,PRICE_CLOSE); ma_o_handler = iMA(Symbol(),SYSTEM_TIME_FRAME,2,0,MODE_EMA,PRICE_OPEN); count = 0; last_value = 0; trade_sl = 1.5e-2; }
Наконец, наши торговые правила были сгенерированы благодаря значениям коэффициентов, которые наша модель изучила на основе обучающих данных. Это последняя функция, которую мы определим перед тем, как отменить определение системных переменных, созданных в начале нашей программы.
//+------------------------------------------------------------------+ //| Check if we have a trading setup | //+------------------------------------------------------------------+ void check_signal(void) { double current_reading = rsi_55.GetCurrentReading(); Comment("Last Reading: ",last_value,"\nDifference in Reading: ",(last_value - current_reading)); double bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); double ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); double cp_lb = 7.17; double cp_ub = 10.82; if((((last_value - current_reading) <= -(cp_lb)) && ((last_value - current_reading) > (cp_ub)))|| ((((last_value - current_reading) > -(cp_lb))) && ((last_value - current_reading) < (cp_lb)))) { if(ma_o[0] > ma_c[0]) { if(PositionsTotal() == 0) { Trade.Sell(TRADING_VOLUME,Symbol(),bid,(ask+trade_sl),(ask-trade_sl)); count = 0; } } } if(((last_value - current_reading) >= (cp_lb)) < ((last_value - current_reading) < cp_ub)) { if(ma_c[0] < ma_o[0]) { if(PositionsTotal() == 0) { Trade.Buy(TRADING_VOLUME,Symbol(),ask,(bid-trade_sl),(bid+trade_sl)); count = 0; } } } } //+------------------------------------------------------------------+ #undef RSI_BUFFER_SIZE #undef RSI_PERIOD #undef RSI_PRICE #undef RSI_TIME_FRAME #undef SYSTEM_TIME_FRAME #undef TRADING_VOLUME //+------------------------------------------------------------------+
Давайте теперь приступим к тестированию торговой системы на истории. Напомним, что на рис. 4 мы удалили все данные, которые пересекаются с результатами нашего тестирования на истории. Таким образом, это может служить нам приблизительным представлением того, как наша стратегия может работать в реальном времени. Мы проведем трехлетнее тестирование нашей стратегии на основе ежедневных данных, начиная с 1 января 2022 года и до марта 2025 года.

Рис. 14. Даты, которые мы будем использовать для тестирования торговой стратегии на истории
Для достижения наилучших результатов мы всегда используем параметр "Каждый тик на основе реальных тиков", поскольку он обеспечивает наиболее точную симуляцию прошлых рыночных условий на основе исторических тиков, собранных терминалом MetaTrader 5 от вашего имени вашим брокером.

Рис. 15. Условия, в которых мы будем проводить наши тесты, имеют огромное значение и влияют на прибыльность нашей стратегии
Кривая эквити, полученная с помощью нашего инструмента тестирования стратегий, выглядит многообещающей. Но давайте вместе проанализируем подробную статистику, чтобы получить полное представление об эффективности нашей стратегии.

Рис. 16. Кривая эквити, построенная на основе торговой стратегии EURUSD с 55-периодным RSI
При применении нашей стратегии в тестере были получены следующие статистические данные:
- Коэффициент Шарпа: 0,92
- Матожидание выигрыша: 2,49
- Чистая прибыль: USD151,87
- Прибыльные сделки: 57,38%
Однако обратите внимание на то, что из 61 совершенной сделки только 4 были на покупку. Почему наша стратегия в такой непропорциональной степени ориентирована на продажу? Как мы можем это исправить? Давайте еще раз взглянем на график EURUSD и попробуем разобраться вместе.

Рис. 17. Подробная статистика исторической эффективности советника на EURUSD D1.
Улучшение советника
На рис. 18 я приложил скриншот месячного курса EURUSD. Две красные вертикальные линии обозначают начало 2009 года и конец 2021 года соответственно. Зеленая вертикальная линия обозначает начало обучающих данных, которые мы использовали для нашей статистической модели. Нетрудно понять, почему модель выработала склонность к коротким позициям, учитывая устойчивый медвежий тренд, начавшийся в 2008 году.

Рис. 18. Понимание того, почему наша модель имеет такой медвежий настрой
Нам не всегда нужны дополнительные исторические данные, чтобы попытаться это исправить. Вместо этого мы можем использовать более гибкую модель, чем та ридж-модель, с которой мы начали. Опытный разработчик с легкостью добавит нашей стратегии дополнительные сигналы на покупку.
Мы можем дать нашему советнику достаточно точную оценку вероятности роста курса в течение следующих 10 дней. Мы можем обучить регрессор на основе случайного леса прогнозировать вероятность того, что мы увидим бычье ценовое действие. Когда ожидаемая вероятность превысит 0,5, наш советник откроет длинную позицию. В противном случае мы будем следовать стратегии, которую переняли у нашей ридж-модели.
Моделирование вероятностей в Python
Для начала импортируем несколько библиотек.
from sklearn.metrics import accuracy_score from sklearn.ensemble import RandomForestRegressor
Затем определим наши зависимые и независимые переменные.
#Independent variable X = data[['Diff RSI 55']] #Dependent variable y = data['Target']
Подберем модель.
model = RandomForestRegressor() model.fit(X,y)
Подготовимся к экспорту в ONNX.
import onnx from skl2onnx import convert_sklearn from skl2onnx.common.data_types import FloatTensorType
Определим формы входных данных.
inital_params = [("float_input",FloatTensorType([1,1]))]
Создадим прототип ONNX и сохраним его на диск.
onnx_proto = convert_sklearn(model=model,initial_types=inital_params,target_opset=12) onnx.save(onnx_proto,"EURUSD Diff RSI 55 D1 1 1.onnx")
Вы также можете визуализировать свою модель, используя библиотеку netron, чтобы убедиться, что вашей модели ONNX были заданы правильные входные и выходные атрибуты.
import netron
netron.start("EURUSD Diff RSI 55 D1 1 1.onnx") Это графическое представление нашего регрессора случайного леса и атрибутов, которыми обладает модель. Netron также можно использовать для визуализации нейронных сетей и многих других типов моделей ONNX.

Рис. 19. Визуализация нашей модели регрессора случайного леса ONNX
Формы входных и выходных данных были корректно заданы ONNX, поэтому теперь мы можем перейти к применению регрессора случайного леса, чтобы помочь нашему советнику прогнозировать вероятность бычьего движения цены в течение следующих 10 дней.

Рис. 20. Характеристики нашей модели ONNX соответствуют ожидаемым параметрам, которые мы хотели проверить
Улучшение советника
Теперь, когда мы экспортировали вероятностную модель рынка EURUSD, мы можем импортировать нашу модель ONNX в наш советник.
//+------------------------------------------------------------------+ //| Algorithmic Input Selection.mq5 | //| Gamuchirai Ndawana | //| https://www.mql5.com/en/users/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com/en/users/gamuchiraindawa" #property version "1.00" //+------------------------------------------------------------------+ //| System resources | //+------------------------------------------------------------------+ #resource "\\Files\\EURUSD Diff RSI 55 D1 1 1.onnx" as uchar onnx_model_buffer[];
Нам также потребуется несколько новых макросов, определяющих форму нашей модели ONNX.
#define ONNX_INPUTS 1 #define ONNX_OUTPUTS 1
Кроме того, модель ONNX заслуживает нескольких глобальных переменных, поскольку они могут быстро понадобиться нам в различных частях кода.
long onnx_model; vectorf onnx_output(1); vectorf onnx_input(1);
В приведенном ниже фрагменте кода мы исключили неизмененные части и показали только изменения, внесенные для инициализации модели ONNX и установки параметров ее конфигурации.
//+------------------------------------------------------------------+ //| Setup our global variables | //+------------------------------------------------------------------+ bool setup(void) { //--- Setup our technical indicators ... //--- Create our ONNX model onnx_model = OnnxCreateFromBuffer(onnx_model_buffer,ONNX_DATA_TYPE_FLOAT); //--- Validate the ONNX model if(onnx_model == INVALID_HANDLE) { return(false); } //--- Define the I/O signature ulong onnx_param[] = {1,1}; if(!OnnxSetInputShape(onnx_model,0,onnx_param)) return(false); if(!OnnxSetOutputShape(onnx_model,0,onnx_param)) return(false); return(true); }
Кроме того, наша проверка на действительность сделок была сокращена таким образом, чтобы выделять только добавленный дополнительный код и избегать дублирования одного и того же кода.
//+------------------------------------------------------------------+ //| Check if we have a trading setup | //+------------------------------------------------------------------+ void check_signal(void) { rsi_55.SetDifferencedIndicatorValues(RSI_BUFFER_SIZE,HORIZON,true); onnx_input[0] = (float) rsi_55.GetDifferencedReadingAt(RSI_BUFFER_SIZE - 1); //--- Our Random forest model if(!OnnxRun(onnx_model,ONNX_DATA_TYPE_FLOAT,onnx_input,onnx_output)) Comment("Failed to obtain a forecast from our model!"); else { if(onnx_output[0] > 0.5) if(ma_o[0] < ma_c[0]) Trade.Buy(TRADING_VOLUME,Symbol(),ask,(bid-trade_sl),(bid+trade_sl)); Print("Model Bullish Probabilty: ",onnx_output); } }
В целом, вот как выглядит наша вторая версия торговой стратегии в полностью готовом виде.
//+------------------------------------------------------------------+ //| Algorithmic Input Selection.mq5 | //| Gamuchirai Ndawana | //| https://www.mql5.com/en/users/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com/en/users/gamuchiraindawa" #property version "1.00" //+------------------------------------------------------------------+ //| System resources | //+------------------------------------------------------------------+ #resource "\\Files\\EURUSD Diff RSI 55 D1 1 1.onnx" as uchar onnx_model_buffer[]; //+------------------------------------------------------------------+ //| System constants | //+------------------------------------------------------------------+ #define RSI_PERIOD 55 #define RSI_TIME_FRAME PERIOD_D1 #define SYSTEM_TIME_FRAME PERIOD_D1 #define RSI_PRICE PRICE_CLOSE #define RSI_BUFFER_SIZE 20 #define TRADING_VOLUME SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN) #define ONNX_INPUTS 1 #define ONNX_OUTPUTS 1 #define HORIZON 10 //+------------------------------------------------------------------+ //| Load our RSI library | //+------------------------------------------------------------------+ #include <VolatilityDoctor\Indicators\RSI.mqh> #include <Trade\Trade.mqh> //+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ CTrade Trade; RSI rsi_55(Symbol(),RSI_TIME_FRAME,RSI_PERIOD,RSI_PRICE); double last_value; int count; int ma_o_handler,ma_c_handler; double ma_o[],ma_c[]; double trade_sl; long onnx_model; vectorf onnx_output(1); vectorf onnx_input(1); //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- setup(); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- IndicatorRelease(ma_c_handler); IndicatorRelease(ma_o_handler); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- update(); } //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Update our system variables | //+------------------------------------------------------------------+ void update(void) { static datetime time_stamp; datetime current_time = iTime(Symbol(),SYSTEM_TIME_FRAME,0); if(time_stamp != current_time) { time_stamp = current_time; CopyBuffer(ma_c_handler,0,0,1,ma_c); CopyBuffer(ma_o_handler,0,0,1,ma_o); if((count == 0) && (PositionsTotal() == 0)) { rsi_55.SetIndicatorValues(RSI_BUFFER_SIZE,true); rsi_55.SetDifferencedIndicatorValues(RSI_BUFFER_SIZE,HORIZON,true); last_value = rsi_55.GetReadingAt(RSI_BUFFER_SIZE - 1); count = 1; } if(PositionsTotal() == 0) check_signal(); if(PositionsTotal() > 0) manage_setup(); } } //+------------------------------------------------------------------+ //| Manage our open trades | //+------------------------------------------------------------------+ void manage_setup(void) { double bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); double ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); if(PositionSelect(Symbol())) { double current_sl = PositionGetDouble(POSITION_SL); double current_tp = PositionGetDouble(POSITION_TP); double new_sl = (current_tp > current_sl) ? (bid-trade_sl) : (ask+trade_sl); double new_tp = (current_tp < current_sl) ? (bid+trade_sl) : (ask-trade_sl); //--- Buy setup if((current_tp > current_sl) && (new_sl < current_sl)) Trade.PositionModify(Symbol(),new_sl,new_tp); //--- Sell setup if((current_tp < current_sl) && (new_sl > current_sl)) Trade.PositionModify(Symbol(),new_sl,new_tp); } } //+------------------------------------------------------------------+ //| Setup our global variables | //+------------------------------------------------------------------+ bool setup(void) { //--- Setup our technical indicators ma_c_handler = iMA(Symbol(),SYSTEM_TIME_FRAME,2,0,MODE_EMA,PRICE_CLOSE); ma_o_handler = iMA(Symbol(),SYSTEM_TIME_FRAME,2,0,MODE_EMA,PRICE_OPEN); count = 0; last_value = 0; trade_sl = 1.5e-2; //--- Create our ONNX model onnx_model = OnnxCreateFromBuffer(onnx_model_buffer,ONNX_DATA_TYPE_FLOAT); //--- Validate the ONNX model if(onnx_model == INVALID_HANDLE) { return(false); } //--- Define the I/O signature ulong onnx_param[] = {1,1}; if(!OnnxSetInputShape(onnx_model,0,onnx_param)) return(false); if(!OnnxSetOutputShape(onnx_model,0,onnx_param)) return(false); return(true); } //+------------------------------------------------------------------+ //| Check if we have a trading setup | //+------------------------------------------------------------------+ void check_signal(void) { rsi_55.SetDifferencedIndicatorValues(RSI_BUFFER_SIZE,HORIZON,true); last_value = rsi_55.GetReadingAt(RSI_BUFFER_SIZE - 1); double current_reading = rsi_55.GetCurrentReading(); Comment("Last Reading: ",last_value,"\nDifference in Reading: ",(last_value - current_reading)); double bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); double ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); double cp_lb = 7.17; double cp_ub = 10.82; onnx_input[0] = (float) rsi_55.GetDifferencedReadingAt(RSI_BUFFER_SIZE - 1); //--- Our Random forest model if(!OnnxRun(onnx_model,ONNX_DATA_TYPE_FLOAT,onnx_input,onnx_output)) Comment("Failed to obtain a forecast from our model!"); else { if(onnx_output[0] > 0.5) if(ma_o[0] < ma_c[0]) Trade.Buy(TRADING_VOLUME,Symbol(),ask,(bid-trade_sl),(bid+trade_sl)); Print("Model Bullish Probabilty: ",onnx_output); } //--- The trading rules we learned from our Ridge Regression Model //--- Ridge Regression Sell if((((last_value - current_reading) <= -(cp_lb)) && ((last_value - current_reading) > (cp_ub)))|| ((((last_value - current_reading) > -(cp_lb))) && ((last_value - current_reading) < (cp_lb)))) { if(ma_o[0] > ma_c[0]) { if(PositionsTotal() == 0) { Trade.Sell(TRADING_VOLUME,Symbol(),bid,(ask+trade_sl),(ask-trade_sl)); count = 0; } } } //--- Ridge Regression Buy else if(((last_value - current_reading) >= (cp_lb)) < ((last_value - current_reading) < cp_ub)) { if(ma_c[0] < ma_o[0]) { if(PositionsTotal() == 0) { Trade.Buy(TRADING_VOLUME,Symbol(),ask,(bid-trade_sl),(bid+trade_sl)); count = 0; } } } } //+------------------------------------------------------------------+ #undef RSI_BUFFER_SIZE #undef RSI_PERIOD #undef RSI_PRICE #undef RSI_TIME_FRAME #undef SYSTEM_TIME_FRAME #undef TRADING_VOLUME #undef ONNX_INPUTS #undef ONNX_OUTPUTS //+------------------------------------------------------------------+
Мы проверим стратегию в тех же условиях, которые показаны на рис. 14 и 15. Вот результаты, полученные нами со второй версией нашей торговой стратегии. Кривая эквити, полученная с помощью нашей второй версии торговой стратегии, по всей видимости, практически идентична первой кривой.

Рис. 21. Визуализация прибыльности второй торговой стратегии
Различия между двумя стратегиями начинают проявляться только при рассмотрении подробной статистики. Коэффициент Шарпа и ожидаемая прибыль снизились. Наша новая стратегия позволила совершить 85 сделок, что на 39% больше, чем 61 сделка, совершенная по нашей первоначальной стратегии. Кроме того, общее количество наших длинных позиций увеличилось с 4 в первом тесте до 42 во втором. Это увеличение на 950%. Таким образом, с учетом значительного объема дополнительного риска, точность и прибыльность показателей снижаются лишь незначительно. Это хороший знак. В нашем предыдущем тесте прибыльными оказались 57,38% всех сделок, а теперь этот показатель составляет 56,47%, что означает снижение точности примерно на 1,59%.

Рис. 22: Подробная статистика эффективности пересмотренной версии нашей торговой стратегии
Заключение
Мы получили представление о том, как можно использовать методы поиска по сетке в сочетании со статистическими моделями, чтобы выбрать оптимальный период для индикаторов, не прибегая к многократным ретроспективным тестам и ручному поиску по каждому возможному периоду. Кроме того, мы узнали один из возможных способов оценки и сравнения значений новых уровней RSI, на которых трейдеры хотят торговать, со значениями традиционных уровней 70 и 30, что позволит им торговать в неблагоприятных рыночных условиях с новой уверенностью в своих способностях.
| Имя файла | Описание |
|---|---|
| Algorithmic Inputs Selection.ipynb | Для проведения численного анализа с использованием Python мы использовали Jupyter Notebook. |
| EURUSD Testing RSI Class.mql5 | MQL5-скрипт для тестирования пользовательской реализации класса RSI. |
| EURUSD RSI Algorithmic Input Selection.mql5 | Скрипт для получения исторических данных о рынке. |
| Algorithmic Input Selection.mql5 | Первоначальная версия разработанной нами торговой стратегии. |
| Algorithmic Input Selection 2.mql5 | Усовершенствованная версия торговой стратегии, в которой исправлены ошибки, выявленные в ходе разработки первоначальной торговой стратегии. |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/17571
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Особенности написания Пользовательских Индикаторов
Знакомство с языком MQL5 (Часть 27): Освоение API и функции WebRequest в языке MQL5
От новичка до эксперта: Алгоритмическая дисциплина трейдера — советник Risk Enforcer вместо эмоций
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования