
Прогнозирование трендов с помощью LSTM для стратегий следования за трендом
Введение
Долгая кратковременная память (LSTM) - это тип рекуррентной нейронной сети (RNN), предназначенной для моделирования последовательных данных путем эффективного учета долгосрочных зависимостей и решения проблемы исчезающего градиента. В настоящей статье мы рассмотрим, как использовать LSTM для прогнозирования будущих тенденций, повышая эффективность стратегий следования за трендами. В статье будет рассказано о внедрении ключевых концепций и стоящей за разработкой мотивации, извлечении данных из MetaTrader 5, использовании этих данных для обучения модели на Python, интеграции модели машинного обучения в MQL5, а также о результатах и перспективах на будущее на основании статистического бэк-тестирования.
Мотивация
Интуитивно понятно, что стратегии следования за трендами извлекают выгоду на трендовых рынках, но плохо работают на нестабильных рынках, где стратегия заканчивается покупкой с наценкой и продажей со скидкой. Научные исследования показали, что классические стратегии следования за трендами, такие как "золотой крест", работают на нескольких рынках и таймфреймах на протяжении длительных периодов истории. Хотя эти стратегии, возможно, и не очень прибыльны, они продемонстрировали стабильную прибыль. Стратегии следования за трендами обычно получают прибыль от экстремальных выбросов, приносящих значительно более высокую прибыль, чем средний убыток. Жесткий стоп-лосс стратегии и принцип "пусть прибыль растет" приводят к низкому коэффициенту выигрыша, но высокому соотношению прибыли и риска на сделку.
LSTM (Долгая кратковременная память) - это специальный тип рекуррентной нейронной сети (RNN), предназначенной для учета долгосрочных зависимостей при моделировании последовательных данных. В ней используются ячейки памяти, которые могут сохранять информацию в течение длительного времени, устраняя проблему с исчезающим градиентом, которая обычно возникает в традиционных RNN. Эта возможность сохранять и иметь доступ к информации, полученной ранее в последовательности, делает LSTM особенно эффективной для таких задач, как прогнозирование временных рядов и тенденций. Для решения задач регрессии LSTM может моделировать временные взаимосвязи между входными характеристиками и прогнозировать непрерывные выходные данные с высокой точностью, что делает её идеальной для прогнозирования применения.
Мотивация для настоящей статьи - это использовать возможности LSTM для регрессии тренда, прогнозирования будущих тенденций и потенциальной фильтрации неудачных сделок, которые являются результатом низкой трендовости. Эта мотивация основана на гипотезе о том, что стратегии следования за трендами лучше работают на трендовых рынках по сравнению с рынками, на которых нет тренда.
Для определения силы тренда будем использовать ADX (Индекс среднего направленного движения), поскольку это один из самых популярных индикаторов для оценки текущей тенденции. Мы стремимся предсказать его будущее значение, а не использовать его текущее значение, поскольку высокий ADX обычно указывает на то, что тренд уже начался или заканчивается, а значит, наша точка входа слишком поздняя, чтобы извлечь выгоду.
ADX рассчитывается по формуле:
Подготовка и предварительная обработка данных
Прежде чем получить данные, сначала нужно уточнить, какие данные необходимы. Мы планируем использовать несколько функций для обучения регрессионной модели, прогнозирующей будущие значения ADX. Эти характеристики включают в себя RSI, указывающий на текущую относительную силу рынка, процент доходности последней свечи, который служит постоянным значением цены закрытия, и сам ADX, напрямую связанный со значением, которое мы стремимся спрогнозировать. Обратите внимание, что мы только что объяснили, как интуитивно выбрать эти характеристики. Вы можете выбрать характеристики самостоятельно, но убедитесь, что они обоснованы и стационарны. Мы планируем обучить модель, используя почасовые данные с 01.01.2020 по 01.01.2024, и протестировать работу модели с 01.01.2024 по 01.01.2025 в качестве теста вне выборки.
Теперь, когда мы выяснили, какие данные надо получить, давайте создадим советника для получения этих данных.
Мы будем использовать представленный в этой статье класс CFileCSV, чтобы сохранить массив в виде строки в CSV-файле. Как показано ниже, код этого процесса довольно прост.
#include <FileCSV.mqh> CFileCSV csvFile; int barsTotal = 0; int handleRsi; int handleAdx; string headers[] = { "time", "ADX", "RSI", "Stationary" }; string data[1000000][4]; int indexx = 0; vector xx; input string fileName = "XAU-1h-2020-2024.csv"; input bool SaveData = true; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() {//Initialize model handleRsi = iRSI(_Symbol,PERIOD_CURRENT,14,PRICE_CLOSE); handleAdx = iADX(_Symbol,PERIOD_CURRENT,14); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { if (!SaveData) return; if(csvFile.Open(fileName, FILE_WRITE|FILE_ANSI)) { //Write the header csvFile.WriteHeader(headers); //Write data rows csvFile.WriteLine(data); //Close the file csvFile.Close(); } else { Print("File opening error!"); } } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { int bars = iBars(_Symbol,PERIOD_CURRENT); if (barsTotal!= bars){ barsTotal = bars; double rsi[]; double adx[]; CopyBuffer(handleAdx,0,1,1,adx); CopyBuffer(handleRsi,0,1,1,rsi); data[indexx][0] =(string)TimeTradeServer(); data[indexx][1] = DoubleToString(adx[0], 2); data[indexx][2] = DoubleToString(rsi[0], 2); data[indexx][3] = DoubleToString((iClose(_Symbol,PERIOD_CURRENT,1)-iOpen(_Symbol,PERIOD_CURRENT,1))/iClose(_Symbol,PERIOD_CURRENT,1),3); indexx++; } }
Этот советник предназначен для отслеживания и записи значений Индекса относительной силы (RSI) и Индекса среднего направленного движения (ADX) для данного инструмента. Советник использует функции iRSI и iADX для получения текущих значений RSI и ADX и сохраняет их вместе с временной меткой в CSV-файле. Создается CSV-файл с заголовками "time", "ADX", "RSI" и "Stationary". Если включена опция SaveData, то при деинициализации данные записываются в файл (указанный как fileName). Отслеживает новые данные по каждому тику и сохраняет их при изменении количества баров.
Запустим советник (EA) в тестере стратегий для выполнения одного теста. После запуска теста файл должен быть сохранен по следующему пути к файлу: /Tester/Agent-sth000/MQL5/Files.
Затем переходим на Python для предварительной обработки данных в рамках подготовки к обучению нашей модели машинного обучения.
Мы планируем использовать подход контролируемого обучения, при котором модель обучается прогнозировать желаемый результат на основе помеченных данных. Процесс обучения включает в себя настройку весов для различных операций, применяемых к характеристикам, чтобы свести к минимуму количество ошибок и получить конечный результат.
В качестве метки я предлагаю использовать среднее значение ADX из следующих 10 значений ADX. Такой подход гарантирует, что тренд еще не полностью установился к моменту нашего входа в сделку, а также предотвращает чрезмерное отклонение тренда от текущего сигнала. Использование среднего значения следующих 10 значений ADX - отличный способ гарантировать, что тренд останется активным в течение следующих нескольких баров, что позволит нам зайти и зафиксировать прибыль от предстоящих направленных движений.
import pandas as pd data = pd.read_csv('XAU-1h-2020-2024.csv', sep=';') data= data.dropna().set_index('time') data['output'] = data['ADX'].shift(-10) data = data[:-10] data['output']= data['output'].rolling(window=10).mean() data = data[9:]
Этот код считывает CSV-файл и разделяет данные по разным столбцам, поскольку значения группируются вместе с помощью точки с запятой (';'). Затем он удаляет все пустые ряды и устанавливает столбец 'time' в качестве индекса, чтобы обеспечить хронологический порядок данных для обучения. Затем создается новый столбец под названием "output", в котором вычисляется среднее значение следующих 10 значений ADX. После этого код удаляет все оставшиеся пустые ряды, так как в некоторых рядах может не хватить будущих значений ADX для вычисления выходных данных.
Обучение моделей
Эта схема иллюстрирует, чего LSTM пытается добиться в ходе нашего учебного процесса. Мы хотим, чтобы входные данные имели форму (sample_amount, time_steps, feature_amount), где time_step представляет, сколько предыдущих временных точек данных нам надо использовать для прогнозирования следующего значения. Например, мы могли бы использовать данные с понедельника по четверг, чтобы прогнозировать какой-либо результат на пятницу. LSTM использует алгоритмы для выявления паттернов во временных рядах и взаимосвязи между характеристиками и результатом. Она создает один или несколько уровней нейронных сетей, каждый из которых состоит из множества весовых единиц (нейронов), применяющих веса к каждому признаку и каждому временному шагу, чтобы в конечном итоге получить окончательный прогноз.
Для простоты вы могли бы просто запустить следующий код, и он выполнит процесс обучения за вас.
import numpy as np import matplotlib.pyplot as plt from sklearn.model_selection import train_test_split from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense # Assume data is your DataFrame already loaded with the specified columns and a time-based index # data.columns should include ['ADX', 'RSI', 'Stationary', 'output'] # --- Step 1: Data Preparation --- time_step = 5 # Select features and target features = ['ADX', 'RSI', 'Stationary'] target = 'output' # --- Step 2: Create sequences for LSTM input --- def create_sequences(data, target_col, time_step): """ Create sequences of length time_step from the DataFrame. data: DataFrame of input features and target. target_col: Name of the target column. Returns: X, y arrays suitable for LSTM. """ X, y = [], [] feature_cols = data.columns.drop(target_col) for i in range(len(data) - time_step): seq_x = data.iloc[i:i+time_step][feature_cols].values # predict target at the next time step after the sequence seq_y = data.iloc[i+time_step][target_col] X.append(seq_x) y.append(seq_y) return np.array(X), np.array(y) # Create sequences X, y = create_sequences(data, target_col=target, time_step=time_step) # --- Step 3: Split into training and evaluation sets --- # Use a simple 80/20 split for training and evaluation X_train, X_eval, y_train, y_eval = train_test_split(X, y, test_size=0.2, shuffle=False) # --- Step 4: Build the LSTM model --- n_features = len(features) # number of features per time step model = Sequential() model.add(LSTM(50, input_shape=(time_step, n_features))) # LSTM layer with 50 units model.add(Dense(1)) # output layer for regression model.compile(optimizer='adam', loss='mse') model.summary() # --- Step 5: Train the model --- epochs = 50 batch_size = 100 history = model.fit( X_train, y_train, epochs=epochs, batch_size=batch_size, validation_data=(X_eval, y_eval) )
Вот несколько важных соображений, которые следует учитывать при обучении с использованием приведенного выше кода:
-
Предварительная обработка данных: На этапе предварительной обработки убедитесь, что ваши данные приведены в порядок по времени. Невыполнение этого требования может привести к искажению прогноза при разбиении данных на блоки time_steps.
-
Разделение на обучение и тестирование: При разделении данных на тренировочный и тестовый наборы не перемешивайте данные. Необходимо соблюдать временной порядок, чтобы избежать искажения в будущем прогнозе.
-
Сложность модели: Для анализа временных рядов, особенно при ограниченном количестве точек данных, нет необходимости создавать слишком много слоев, нейронов или эпох. Чрезмерное усложнение модели может привести к переобучению или высокому уровню отклонения параметров. Настроек, использованных в примере, должно быть достаточно.
# --- Step 6: Evaluate the model --- eval_loss = model.evaluate(X_eval, y_eval) print(f"Evaluation Loss: {eval_loss}") # --- Step 7: Generate Predictions and Plot --- # Generate predictions on the evaluation set predictions = model.predict(X_eval).flatten() # Create a plot for predictions vs actual values plt.figure(figsize=(12, 6)) plt.plot(predictions, label='Predicted Output', color='red') plt.plot(y_eval, label='Actual Output', color='blue') plt.title('LSTM Predictions vs Actual Output') plt.xlabel('Sample Index') plt.ylabel('Output Value') plt.legend() plt.show()
Этот код должен вывести среднеквадратичную погрешность оценочного набора по сравнению с прогнозом модели, как это показано ниже.
Evaluation Loss: 57.405677795410156
Она рассчитывается по формуле:
Где n - размер образца, yi - прогнозируемое значение для каждого образца, а y^i - фактическое значение для каждого результата оценки.
Как видно из расчета, вы можете сравнить убыток модели с квадратом средних значений того, что вы прогнозируете, чтобы проверить, не чрезмерно ли высок относительный убыток. Кроме того, убедитесь, что потери аналогичны потерям в обучающих наборах, что указывает на то, что модель не перегружена обучающими данными.
Наконец, чтобы сделать модель совместимой с MQL5, нам надо сохранить ее в формате ONNX. Поскольку модели LSTM напрямую не поддерживают ONNX-переходы, нам сначала нужно сохранить модель как функциональную, при этом явно определив формат ее ввода и вывода. После этого мы можем сохранить его как ONNX-файл, что сделает его пригодным для дальнейшего использования с MQL5.
import tensorflow as tf import tf2onnx # Define the input shape based on your LSTM requirements: (time_step, n_features) time_step = 5 n_features = 3 # Create a new Keras Input layer matching the shape of your data inputs = tf.keras.Input(shape=(time_step, n_features), name="input") # Pass the input through your existing sequential model outputs = model(inputs) functional_model = tf.keras.Model(inputs=inputs, outputs=outputs) # Create an input signature that matches the defined input shape input_signature = ( tf.TensorSpec((None, time_step, n_features), dtype=tf.float32, name="input"), ) output_path = "regression2024.onnx" # Convert the functional model to ONNX format onnx_model, _ = tf2onnx.convert.from_keras( functional_model, input_signature=input_signature, # matching the input signature opset=15, output_path=output_path ) print(f"Model successfully converted to ONNX at {output_path}")
Обратите внимание, что "None" в качестве вводного формата здесь означает, что модель может принимать любое количество образцов. Он автоматически выводит соответствующие прогнозы для каждого образца, что делает его гибким для различных размеров партии.
Создание советника
Теперь, когда мы сохранили файл модели ONNX, надо скопировать его в каталог /MQL5/Files для последующего использования.
Возвращаемся к MetaEditor. Мы будем развивать классическую стратегию следования за трендами, основанную на логике сигналов золотого креста. Это то же самое, что я реализовал в своей предыдущей статье о машинном обучении. Основная логика заключается в использовании двух скользящих средних: быстрой и медленной. Торговый сигнал генерируется, когда две скользящие средние пересекаются, и направление сделки следует за быстрой скользящей средней, отсюда и термин "следование за трендами". Сигнал выхода возникает, когда цена пересекает медленную скользящую среднюю, что открывает больше возможностей для трейлинг-стопов. Полный код выглядит следующим образом:
#include <Trade/Trade.mqh> //XAU - 1h. CTrade trade; input int MaPeriodsFast = 15; input int MaPeriodsSlow = 25; input int MaPeriods = 200; input double lott = 0.01; ulong buypos = 0, sellpos = 0; input int Magic = 0; int barsTotal = 0; int handleMaFast; int handleMaSlow; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { trade.SetExpertMagicNumber(Magic); handleMaFast =iMA(_Symbol,PERIOD_CURRENT,MaPeriodsFast,0,MODE_SMA,PRICE_CLOSE); handleMaSlow =iMA(_Symbol,PERIOD_CURRENT,MaPeriodsSlow,0,MODE_SMA,PRICE_CLOSE); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { int bars = iBars(_Symbol,PERIOD_CURRENT); //Beware, the last element of the buffer list is the most recent data, not [0] if (barsTotal!= bars){ barsTotal = bars; double maFast[]; double maSlow[]; CopyBuffer(handleMaFast,BASE_LINE,1,2,maFast); CopyBuffer(handleMaSlow,BASE_LINE,1,2,maSlow); double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); double lastClose = iClose(_Symbol, PERIOD_CURRENT, 1); //The order below matters if(buypos>0&& lastClose<maSlow[1]) trade.PositionClose(buypos); if(sellpos>0 &&lastClose>maSlow[1])trade.PositionClose(sellpos); if (maFast[1]>maSlow[1]&&maFast[0]<maSlow[0]&&buypos ==sellpos)executeBuy(); if(maFast[1]<maSlow[1]&&maFast[0]>maSlow[0]&&sellpos ==buypos) executeSell(); if(buypos>0&&(!PositionSelectByTicket(buypos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){ buypos = 0; } if(sellpos>0&&(!PositionSelectByTicket(sellpos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){ sellpos = 0; } } } //+------------------------------------------------------------------+ //| Expert trade transaction handling function | //+------------------------------------------------------------------+ void OnTradeTransaction(const MqlTradeTransaction& trans, const MqlTradeRequest& request, const MqlTradeResult& result) { if (trans.type == TRADE_TRANSACTION_ORDER_ADD) { COrderInfo order; if (order.Select(trans.order)) { if (order.Magic() == Magic) { if (order.OrderType() == ORDER_TYPE_BUY) { buypos = order.Ticket(); } else if (order.OrderType() == ORDER_TYPE_SELL) { sellpos = order.Ticket(); } } } } } //+------------------------------------------------------------------+ //| Execute sell trade function | //+------------------------------------------------------------------+ void executeSell() { double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); bid = NormalizeDouble(bid,_Digits); trade.Sell(lott,_Symbol,bid); sellpos = trade.ResultOrder(); } //+------------------------------------------------------------------+ //| Execute buy trade function | //+------------------------------------------------------------------+ void executeBuy() { double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); ask = NormalizeDouble(ask,_Digits); trade.Buy(lott,_Symbol,ask); buypos = trade.ResultOrder(); }
Я не буду подробно останавливаться на проверке и предложениях по выбору вашей стратегии бэк-тестирования. Более подробную информацию можно найти в моей предыдущей статье о машинном обучении, ссылка на которую находится здесь.
Теперь мы попробуем запустить нашу модель LSTM на основе этого фреймворка.
Во-первых, мы объявляем глобальные переменные, определяющие форму наших входных и выходных данных, а также два мульти-массива для хранения входных и выходных данных. Кроме того, мы объявляем хэндл модели, который будет управлять процессом вывода данных в модель и извлечения из нее прогнозов. Такая настройка обеспечивает надлежащий поток данных и взаимодействие между моделью и входными/выходными переменными.
#resource "\\Files\\regression2024.onnx" as uchar lstm_onnx[] float data[1][5][3]; float out[1][1]; long lstmHandle = INVALID_HANDLE; const long input_shape[] = {1,5,3}; const long output_shape[]={1,1};
Далее, в функции OnInit() , мы инициализируем соответствующие индикаторы, такие как RSI и ADX, а также модель ONNX. Во время этой инициализации мы проверяем, что входная форма и выходная форма, объявленные в MQL5, соответствуют указанным ранее в функциональной модели Python. Этот шаг обеспечивает согласованность и предотвращает ошибки во время инициализации модели, гарантируя, что модель сможет корректно обрабатывать данные в ожидаемом формате.
int handleMaFast; int handleMaSlow; int handleAdx; // Average Directional Movement Index - 3 int handleRsi; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() {//Initialize model trade.SetExpertMagicNumber(Magic); handleMaFast =iMA(_Symbol,PERIOD_CURRENT,MaPeriodsFast,0,MODE_SMA,PRICE_CLOSE); handleMaSlow =iMA(_Symbol,PERIOD_CURRENT,MaPeriodsSlow,0,MODE_SMA,PRICE_CLOSE); handleAdx=iADX(_Symbol,PERIOD_CURRENT,14);//Average Directional Movement Index - 3 handleRsi = iRSI(_Symbol,PERIOD_CURRENT,14,PRICE_CLOSE); // Load the ONNX model lstmHandle = OnnxCreateFromBuffer(lstm_onnx, ONNX_DEFAULT); //--- specify the shape of the input data if(!OnnxSetInputShape(lstmHandle,0,input_shape)) { Print("OnnxSetInputShape failed, error ",GetLastError()); OnnxRelease(lstmHandle); return(-1); } //--- specify the shape of the output data if(!OnnxSetOutputShape(lstmHandle,0,output_shape)) { Print("OnnxSetOutputShape failed, error ",GetLastError()); OnnxRelease(lstmHandle); return(-1); } if (lstmHandle == INVALID_HANDLE) { Print("Error creating model OnnxCreateFromBuffer ", GetLastError()); return(INIT_FAILED); } return(INIT_SUCCEEDED); }
Далее объявляем функцию для обновления входных данных с каждым новым баром. Эта функция выполняет цикл по time_step (в данном случае 5) для сохранения соответствующих данных в глобальном мульти-массиве. Она преобразует данные в тип float, чтобы убедиться, что они соответствуют 32-разрядным требованиям, предъявляемым моделью ONNX. Кроме того, функция гарантирует, что порядок в мультимассиве корректный: сначала добавляются старые данные, а затем последовательно добавляются более новые. Это гарантирует, что данные будут вводиться в модель в надлежащем временном порядке.
void getData(){ double rsi[]; double adx[]; CopyBuffer(handleAdx,0,1,5,adx); CopyBuffer(handleRsi,0,1,5,rsi); for (int i =0; i<5; i++){ data[0][i][0] = (float)adx[i]; data[0][i][1] = (float)rsi[i]; data[0][i][2] = (float)((iClose(_Symbol,PERIOD_CURRENT,5-i)-iOpen(_Symbol,PERIOD_CURRENT,5-i))/iClose(_Symbol,PERIOD_CURRENT,5-i)); } }
Наконец, в функции OnTick() мы реализуем торговую логику.
Эта функция гарантирует, что последующая торговая логика будет проверяться только после формирования нового бара. Это предотвращает ненужные пересчеты или торговые действия в течение одного и того же бара и гарантирует, что прогнозы модели основаны на полных данных для каждого нового временного шага.
int bars = iBars(_Symbol,PERIOD_CURRENT); if (barsTotal!= bars){ barsTotal = bars;
Этот код восстанавливает значения переменных buypos и sellpos до 0, когда не остается позиций с магическим номером советника. Переменные buypos и sellpos используются для обеспечения того, чтобы позиции на покупку и продажу были пустыми перед генерацией сигнала на вход. Сбрасывая эти переменные, когда ни одна позиция не открыта, мы гарантируем, что система случайно не попытается открыть новые позиции, если таковая уже существует.
if(buypos>0&&(!PositionSelectByTicket(buypos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){ buypos = 0; } if(sellpos>0&&(!PositionSelectByTicket(sellpos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){ sellpos = 0; }
Мы используем эту строку кода для запуска модели ONNX, где она принимает входные данные и выводит прогноз в массив out. Эта операция выполняется только при формировании начального сигнала на вход, а не на каждом новом баре. Такой подход помогает экономить вычислительные мощности и делает бэк-тестирование более эффективным, поскольку мы избегаем ненужных оценок модели в периоды отсутствия входного сигнала.
OnnxRun(lstmHandle, ONNX_NO_CONVERSION, data, out);
Теперь торговая логика такова: когда происходит пересечение скользящих средних и не открыто текущих позиций, мы запускаем модель для получения прогнозируемого значения ADX. Если значение ниже некоторого порогового значения, то мы расцениваем это как низкую трендовость и избегаем сделки, а если оно выше, то входим. Вот полная функция OnTick():
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { int bars = iBars(_Symbol,PERIOD_CURRENT); if (barsTotal!= bars){ barsTotal = bars; double maFast[]; double maSlow[]; double adx[]; CopyBuffer(handleMaFast,BASE_LINE,1,2,maFast); CopyBuffer(handleMaSlow,BASE_LINE,1,2,maSlow); CopyBuffer(handleAdx,0,1,1,adx); double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); double lastClose = iClose(_Symbol, PERIOD_CURRENT, 1); //The order below matters if(buypos>0&& lastClose<maSlow[1]) trade.PositionClose(buypos); if(sellpos>0 &&lastClose>maSlow[1])trade.PositionClose(sellpos); if(maFast[1]<maSlow[1]&&maFast[0]>maSlow[0]&&sellpos == buypos){ getData(); OnnxRun(lstmHandle, ONNX_NO_CONVERSION, data, out); if(out[0][0]>threshold)executeSell();} if(maFast[1]>maSlow[1]&&maFast[0]<maSlow[0]&&sellpos == buypos){ getData(); OnnxRun(lstmHandle, ONNX_NO_CONVERSION, data, out); if(out[0][0]>threshold)executeBuy();} if(buypos>0&&(!PositionSelectByTicket(buypos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){ buypos = 0; } if(sellpos>0&&(!PositionSelectByTicket(sellpos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){ sellpos = 0; } } }
Статистическое бэк-тестирование
После того, как все было реализовано, теперь мы можем скомпилировать советник и протестировать результаты в тестере стратегий. Мы проведем тест вне выборки для символа XAUUSD на 1-часовом таймфрейме с 1 января 2024 года по 1 января 2025 года. Во-первых, мы используем нашу первоначальную стратегию бэк-тестирования в качестве базовой. Мы ожидаем, что советник с реализацией LSTM в течение этого периода будет работать лучше, чем базовый уровень.
Теперь проведем бэк-тестирование советника с реализацией LSTM, используя пороговое значение 30, поскольку ADX, равный 30, широко известен как показатель мощной силы тренда.
Сравнивая эти два результата мы видим, что реализация LSTM отфильтровала около 70% первоначальных сделок и улучшила коэффициент прибыли с 1,48 до 1,52. Она также продемонстрировала более высокую корреляцию LR по сравнению с базовым уровнем, что позволяет предположить, что это способствовало более стабильной общей эффективности.
При бэк-тестировании моделей машинного обучения важно понимать, что внутренние параметры модели являются ключевыми определяющими факторами, в отличие от более простых стратегий, где параметры оказывают меньшее влияние. В результате различные данные обучения могут привести к очень разным результатам по параметрам. Кроме того, одновременное обучение по всему набору исторических данных не идеально, так как это привело бы к слишком большому количеству образцов, большинство из которых были бы недостаточно своевременными. По этой причине я рекомендую использовать метод скользящего окна для бэк-тестирования в таких случаях. Если у нас есть ограниченные образцы на протяжении всей истории бэк-тестирования, как обсуждалось в моей предыдущей статье о модели CatBoost, более подходящим является бэк-тестирование с расширяющимся окном.
Вот демонстрационные изображения:
Бэк-тестирование методом расширяющегося окна начинается с начального окна данных фиксированного размера, но по мере появления новых точек данных окно расширяется, включая новые данные, что позволяет тестировать стратегию на все большем наборе данных с течением времени.
Чтобы выполнить скользящий бэктест, мы просто повторяем описанный в настоящей статье процесс и объединяем результаты в единый набор данных. Вот результаты скользящего бэк-теста с 1 января 2015 года по 1 января 2025 года:
Показатели:
Коэффициент прибыльности: 1.24 Максимальная просадка: -250.56 Средний выигрыш: 12.02 Средний проигрыш: -5.20 Коэффициент выигрыша: 34.81%
Результат впечатляет, при этом есть куда развиваться.
Размышления
Эффективность советника напрямую коррелирует с предсказуемостью, демонстрируемой моделью. Чтобы улучшить свой советник, необходимо учитывать несколько ключевых факторов:
- Преимущество вашей стратегии бэк-тестирования: В конечном счете, большинство ваших исходных сигналов должно иметь преимущество, чтобы оправдать дальнейшую фильтрацию.
- Используемые данные: Неэффективность рынка выявляется путем анализа важности характеристик каждых входных данных и выявления менее известных характеристик, которые могли бы обеспечить преимущество.
- Модель, используемая вами для обучения: Подумайте о том, является ли проблема, которую вы пытаетесь решить, проблемой классификации или регрессии. А выбор правильных параметров тренировки тоже имеет решающее значение.
- То, что вы пытаетесь прогнозировать: Вместо того чтобы напрямую предсказывать исход сделки, сосредоточьтесь на чем-то косвенно связанном с конечным результатом, как я демонстрирую в этой статье.
В своих предыдущих статьях я экспериментировал с различными методами машинного обучения, доступными розничным трейдерам. Моя цель - вдохновить читателей перенять эти идеи и разработать свои собственные инновационные подходы, поскольку творческий потенциал в этой области безграничен. Машинное обучение по своей сути не является сложным или недосягаемым — это образ мышления. Речь идет о понимании преимуществ, построении прогностических моделей и тщательной проверке гипотез. По мере того как вы будете продолжать экспериментировать, это понимание постепенно будет становиться все яснее.
Заключение
В этой статье мы сначала представили мотивацию использования LSTM для прогнозирования трендов, а также объяснили концепции, лежащие в основе ADX и LSTM. Затем мы извлекли данные из MetaTrader 5, обработали их и обучили модель на Python. Затем мы прошлись по процессу создания советника и ознакомились с результатами бэк-тестирования. Наконец, мы представили концепции бэк-тестирования методом скользящего окна и бэк-тестирования методом расширяющегося окна и завершили статью некоторыми размышлениями.
Таблица файлов
Название файла | Использование файла |
---|---|
FileCSV.mqh | Включаемый файл для хранения данных в CSV |
LSTM_Demonstration.ipynb | Файл python для обучения модели LSTM |
LSTM-TF-XAU.mq5 | Торговый советник с реализацией LSTM |
OHLC Getter.mq5 | Советник для выборки данных |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/16940
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.





- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Не пойму, где в zip-архиве сама модель regression2024.onnx?
Здравствуйте, an_tar.
Как указано в статье, данный тип системы должен быть проверен через бэктест на скользящем окне. Я не хотел включать всю свою обученную модель с 2008 года, чтобы не утяжелять файл.
Рекомендуется использовать фреймворк, представленный в статье, для обучения собственной модели, чтобы она была совместима с вашим личным методом валидации.