Создание самооптимизирующихся советников на MQL5 (Часть 6): Предотвращение стоп-аутов
Трейдеры могут правильно предсказать будущее изменение цены на рынке, но закрыть свои позиции с убытком. В торговых кругах такую ситуацию обычно называют "выбить по стопу" (getting stopped out). Эта проблема возникает из-за того, что уровни цен не меняются по прямым и предсказуемым линиям.
На рис. 1 показан скриншот почасового изменения цены пары EURUSD. Белые пунктирные вертикальные линии отмечают начало и конец торгового дня. Трейдер, который был уверен, что уровень цен в тот день упадет, правильно предсказал бы будущее изменение цены. К несчастью, уровни цен значительно выросли, прежде чем упасть, то есть, если бы стоп-лосс нашего трейдера находился в пределах красной зоны, выделенной на рис. 1, он бы закрыл свою позицию с убытком, при этом правильно спрогнозировав будущее изменение цены.

Рис. 1. Пример выхода по стоп-ауту
На протяжении многих лет трейдеры предлагали различные решения этой проблемы, но, как мы увидим в этой статье, большинство из них не работают. Наиболее часто упоминаемое решение — просто "расширить стопы".
Это означает, что трейдерам следует расширять стоп-лоссы в особенно волатильные торговые дни, чтобы избежать преждевременного выхода из сделки. Это плохой совет, поскольку он способствует формированию у трейдеров привычки менять уровень риска без каких-либо четко определенных правил.
Другое часто упоминаемое решение — "ждать подтверждения, прежде чем входить в сделку". Опять же, это плохой совет, учитывая характер обсуждаемой проблемы. Ожидание подтверждения лишь откладывает процесс выхода по стопу, а не решает проблему полностью.
Таким образом, проблема преждевременного выхода по стопу усложняет для трейдеров соблюдение принципов разумного управления рисками, одновременно снижая прибыльность торговли и создавая для трейдеров другие поводы для беспокойства.
В статье будут приведены разумные и четкие правила, которые позволят свести к минимуму случаи выхода по стоп-ауту.
Предлагаемое нами решение будет идти вразрез с общепринятыми правилами и будет стимулировать вас вырабатывать разумные торговые привычки, например, сохранять фиксированный размер стоп-лосса, в отличие от общепринятого решения "расширять стоп-лосс".
Обзор торговой стратегии
Наша торговая стратегия будет представлять собой стратегию возврата к среднему значению, состоящую из комбинации уровней поддержки и сопротивления, а также технического анализа. Сначала мы отметим интересующие нас ценовые уровни, используя максимум и минимум предыдущего дня. Далее мы будем ждать и смотреть, будут ли в течение текущего дня преодолены уровни цен предыдущего дня. Например, если максимальная цена предыдущего дня будет пробита новой максимальной ценой в течение текущего дня, мы будем искать возможности занять короткие позиции на рынке, делая ставку на то, что уровни цен вернутся к своим средним значениям. Сигнал на вход на продажу станет для нас очевидным, когда мы увидим, что уровни цен закрываются выше индикатора скользящей средней, после успешного закрытия выше предыдущего максимума.

Рис. 2. Наша торговая стратегия в действии
Период тестирования на истории
Чтобы проанализировать эффективность предлагаемых нами изменений в торговой стратегии, нам сначала необходимо иметь фиксированный период, в течение которого мы будем сравнивать изменения, вносимые нами в нашу систему. В рамках этого обсуждения мы начнем наше тестирование с 1 января 2022 года и продлим его до 1 января 2025 года. Рассматриваемый период выделен на рис. 1. Обратите внимание, что на рисунке мы наблюдаем EURUSD на месячном графике. 
Рис. 3. Период, в течение которого мы будем проводить тестирование на истории
Наши реальные тесты будут проводиться на таймфрейме M30. На рис. 2 мы выделили предполагаемый рынок, который будем использовать для наших тестов, а также период времени, который мы обсуждали ранее. Эти настройки будут исправлены на протяжении всей статьи, поэтому и возникает необходимость обсудить их здесь. Для всех последующих тестов мы оставим эти настройки без изменений. Кроме того, не забудьте выбрать EURUSD, если вы хотите следить за нашим примером, или любой другой символ, которым вы предпочитаете торговать.

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

Рис. 5. Настройки счета, которые мы будем использовать для тестирования на истории
Начало работы с MQL5
Теперь, когда мы ознакомились с периодом тестирования на истории для нашего сегодняшнего теста, давайте сначала установим базовый показатель, который нам нужно превзойти. Начнем с создания торгового приложения для реализации торговой стратегии поддержки и сопротивления, направленной на торговлю на прорывах. Сначала нам нужно импортировать торговую библиотеку.
//+------------------------------------------------------------------+ //| Baseline Model.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" //+------------------------------------------------------------------+ //| Libraries | //+------------------------------------------------------------------+ #include <Trade/Trade.mqh> CTrade Trade;
Определим системные константы. Эти константы помогают нам гарантировать, что мы имеем определенный контроль над поведением нашего приложения во всех тестах.
//+------------------------------------------------------------------+ //| System constants | //+------------------------------------------------------------------+ #define MA_PERIOD 14 //--- Moving Average Period #define MA_TYPE MODE_EMA //--- Type of moving average we have #define MA_PRICE PRICE_CLOSE //---- Applied Price of Moving Average #define TF_1 PERIOD_D1 //--- Our time frame for technical analysis #define TF_2 PERIOD_M30 //--- Our time frame for managing positions #define VOL 0.1 //--- Our trading volume #define SL_SIZE 1e3 * _Point //--- The size of our stop loss
Нам также понадобятся несколько глобальных переменных, которые помогут нам отслеживать интересующие нас уровни цен за вчерашний день.
//+------------------------------------------------------------------+ //| Our global variables | //+------------------------------------------------------------------+ int ma_handler,system_state; double ma[]; double bid,ask,yesterday_high,yesterday_low; const string last_high = "LAST_HIGH"; const string last_low = "LAST_LOW";
После первой загрузки приложения настроим все наши технические индикаторы.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- setup(); //--- return(INIT_SUCCEEDED); }
Если приложение больше не работает, освободите неиспользуемые технические индикаторы.
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- release(); }
Всякий раз, когда мы получаем обновленные цены, мы сохраняем их и пересчитываем значения наших индикаторов.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- update(); }
Теперь мы определим каждую из функций, которые мы вызвали в нашем цикле событий. Во-первых, наша функция обновления будет работать на двух разных таймфреймах. У нас есть процедуры, которые необходимо выполнять один раз в день, а также те, которые необходимо выполнять через гораздо более короткие промежутки времени. Такое разделение задач обеспечивается двумя системными константами - TF_1 (D1) и TF_2 (M30). Такие задачи, как получение максимума и минимума за предыдущий день, нужно выполнять только один раз в день. С другой стороны, такие задачи, как поиск позиций, необходимо выполнять один раз для каждой новой 30-минутной свечи.
//+------------------------------------------------------------------+ //| Perform our update routines | //+------------------------------------------------------------------+ void update() { //--- Daily procedures { static datetime time_stamp; datetime current_time = iTime(Symbol(),TF_1,0); if(time_stamp != current_time) { yesterday_high = iHigh(Symbol(),TF_1,1); yesterday_low = iLow(Symbol(),TF_1,1); //--- Mark yesterday's levels ObjectDelete(0,last_high); ObjectDelete(0,last_low); ObjectCreate(0,last_high,OBJ_HLINE,0,0,yesterday_high); ObjectCreate(0,last_low,OBJ_HLINE,0,0,yesterday_low); } } //--- M30 procedures { static datetime time_stamp; datetime current_time = iTime(Symbol(),TF_2,0); if(time_stamp != current_time) { time_stamp = current_time; //--- Get updated prices bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); //--- Update our technical indicators CopyBuffer(ma_handler,0,0,1,ma); //--- Check for a setup if(PositionsTotal()==0) find_setup(); } } }
Это приложение использует только один технический индикатор. Поэтому наша процедура определения функции настройки проста.
//+------------------------------------------------------------------+ //| Custom functions | //+------------------------------------------------------------------+ void setup(void) { ma_handler = iMA(Symbol(),TF_2,MA_PERIOD,0,MA_TYPE,MA_PRICE); };
Наши условия открытия позиции будут выполнены, если мы обнаружим, что наша текущая экстремальная цена превышает противоположную экстремальную цену, которую мы наблюдали в предыдущий день. Помимо пробития уровней, установленных накануне, мы также хотим зафиксировать дополнительное подтверждение от соотношения цены со скользящей средней.
//+------------------------------------------------------------------+ //| Check if we have any trading setups | //+------------------------------------------------------------------+ void find_setup(void) { if(iHigh(Symbol(),TF_2,1) < yesterday_low) { if(iClose(Symbol(),TF_2,1) < ma[0]) { Trade.Buy(VOL,Symbol(),ask,(bid - (SL_SIZE)),(bid + (SL_SIZE))); } } if(iLow(Symbol(),TF_2,1) > yesterday_high) { if(iClose(Symbol(),TF_2,1) > ma[0]) { Trade.Sell(VOL,Symbol(),bid,(ask + (SL_SIZE)),(ask - (SL_SIZE))); } } }
Если мы не используем советник, нам следует освободить неиспользуемые системные ресурсы.
//+------------------------------------------------------------------+ //| Free resources we are no longer using up | //+------------------------------------------------------------------+ void release(void) { IndicatorRelease(ma_handler); }
Наконец, в конце цикла выполнения нашей программы мы удалим системные константы, которые мы определили ранее.
//+------------------------------------------------------------------+ //| Undefine the system constants we created | //+------------------------------------------------------------------+ #undef TF_1 #undef TF_2 #undef VOL #undef SL_SIZE #undef MA_PERIOD #undef MA_PRICE #undef MA_TYPE
Кривая эквити, полученная в результате применения нашей текущей торговой стратегии, нестабильна. Баланс, создаваемый нашей нынешней системой, со временем продемонстрировал тенденцию к снижению. Нам нужна стратегия, которая хотя и дает сбои периодически, но со временем имеет тенденцию к росту. Поэтому мы сохраним неизменными правила, которые мы использовали для открытия позиций, и попытаемся отсортировать сделки, которые, по нашему мнению, приведут к достижению стоп-лосса. Это определенно непростая задача. Однако лучше реализовать какое-то решение, чем не реализовать никакого.

Рис. 6. Кривая эквити, полученная с помощью нашей текущей версии торговой стратегии
При детальном анализе результатов нашей торговой стратегии мы можем заметить, что наш алгоритм потерял более USD 1000 за трехлетний период тестирования. Кроме того, наш средний и наибольший убыток превышает наш средний и наибольший доход. Перспективы такой стратегии нерадужные. Использовать стратегию в ее нынешнем виде в реальной торговле нельзя.

Рис. 7. Подробный анализ результатов, полученных с помощью нашей торговой стратегии
Улучшаем исходные условия
В основе нашей стратегии по предотвращению преждевременных выходов лежит наблюдение, которое мы сделали ранее в нашей серии статей. Подробности можно найти здесь. На более чем 200 различных символах в нашем терминале MetaTrader 5 скользящая средняя оказывается гораздо проще для прогнозирования, чем сама цена.
Мы можем с пользой использовать наши наблюдения, прогнозируя, превысит ли будущее значение скользящей средней наш уровень стоп-лосса. Если наше приложение ожидает превышения, то оно не должно размещать никаких сделок до достижения скользящей средней нашего стоп-лосса, в противном случае нашему приложению будет разрешено открывать сделки.
В этом суть нашего решения. Правила четко определены от начала до конца и основаны на разумных принципах и объективных рассуждениях. Мы можем быть даже более конкретными и потребовать, чтобы в дополнение к ожиданию того, что наш стоп-лосс будет достигнут, приложение также ожидало, что скользящая средняя превысит наш уровень тейк-профита, прежде чем совершать какую-либо сделку. В противном случае, зачем кому-либо открывать сделку, если у него нет оснований полагать, что его тейк-профит будет исполнен?
Для начала нам необходимо извлечь соответствующие рыночные данные из терминала MetaTrader 5, используя скрипт 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 //--- Define our moving average indicator #define MA_PERIOD 14 //--- Moving Average Period #define MA_TYPE MODE_EMA //--- Type of moving average we have #define MA_PRICE PRICE_CLOSE //---- Applied Price of Moving Average //--- Our handlers for our indicators int ma_handle; //--- Data structures to store the readings from our indicators double ma_reading[]; //--- File name string file_name = Symbol() + " Stop Out Prevention Market Data.csv"; //--- Amount of data requested input int size = 3000; //+------------------------------------------------------------------+ //| Our script execution | //+------------------------------------------------------------------+ void OnStart() { //---Setup our technical indicators ma_handle = iMA(_Symbol,PERIOD_M30,MA_PERIOD,0,MA_TYPE,MA_PRICE); //---Set the values as series CopyBuffer(ma_handle,0,0,size,ma_reading); ArraySetAsSeries(ma_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","Open","High","Low","Close","MA 14"); } else { FileWrite(file_handle, iTime(_Symbol,PERIOD_CURRENT,i), iOpen(_Symbol,PERIOD_CURRENT,i), iHigh(_Symbol,PERIOD_CURRENT,i), iLow(_Symbol,PERIOD_CURRENT,i), iClose(_Symbol,PERIOD_CURRENT,i), ma_reading[i]); } } //--- Close the file FileClose(file_handle); } //+------------------------------------------------------------------+
Анализ данных в Python
После того как вы примените скрипт на выбранном вами рынке, мы можем приступить к анализу наших финансовых данных с использованием библиотек Python. Наша цель — построить нейронную сеть, которая может помочь нам прогнозировать будущее значение индикатора скользящей средней и потенциально уберечь нас от убыточных сделок.
import pandas as pd import numpy as np import seaborn as sns import matplotlib.pyplot as plt
Теперь прочитаем данные, которые мы извлекли из нашего терминала.
data = pd.read_csv("EURUSD Stop Out Prevention Market Data.csv")
data Маркируем данные.
LOOK_AHEAD = 48 data['Target'] = data['MA 14'].shift(-LOOK_AHEAD) data.dropna(inplace=True) data.reset_index(drop=True,inplace=True)
Укажем временные периоды, которые совпадают с использованными в тестировании на истории.
#Let's entirely drop off the last 2 years of data data.iloc[-((48 * 365 * 2) + (48 * 31 * 2) + (48 * 14) - (3)):,:]
Перезапишем исходные рыночные данные новыми данными, которые не содержат наблюдений за период нашего тестирования на истории.
#Let's entirely drop off the last 2 years of data _ = data.iloc[-((48 * 365 * 2) + (48 * 31 * 2) + (48 * 14) - (3)):,:] data = data.iloc[:-((48 * 365 * 2) + (48 * 31 * 2) + (48 * 14) - (3)),:] data
Теперь загрузим наши библиотеки машинного обучения.
from sklearn.neural_network import MLPRegressor from sklearn.model_selection import train_test_split,TimeSeriesSplit,cross_val_scoreСоздадим объект разделения временного ряда, чтобы быстро проверить нашу модель.
tscv = TimeSeriesSplit(n_splits=5,gap=LOOK_AHEAD) Укажем входные данные и цель.
X = data.columns[1:-1] y = data.columns[-1:]Разделим данные пополам для обучения и тестирования новой модели.
train , test = train_test_split(data,test_size=0.5,shuffle=False)
Подготовим обучающие и тестовые разделы к нормализации и масштабированию.
train_X = train.loc[:,X] train_y = train.loc[:,y] test_X = test.loc[:,X] test_y = test.loc[:,y]
Рассчитаем параметры для наших z-оценок.
mean_scores = train_X.mean() std_scores = train_X.std()Нормализуем входные данные модели.
train_X = ((train_X - mean_scores) / std_scores) test_X = ((test_X - mean_scores) / std_scores)
Мы хотим выполнить линейный поиск для оптимального количества итераций обучения для нашей глубокой нейронной сети. Мы будем итерировать путем увеличения степеней числа 2. Начиная с 2 в степени 0 и до 2 в степени 14.
MAX_POWER = 15 results = pd.DataFrame(index=["Train","Test"],columns=[np.arange(0,MAX_POWER)])
Определим цикл for, который поможет нам оценить оптимальное количество итераций обучения, необходимых для адаптации нашей модели глубокой нейронной сети к имеющимся у нас данным.
#Classical Inputs for i in np.arange(0,MAX_POWER): print(i) model = MLPRegressor(hidden_layer_sizes=(5,10,4,2),solver="adam",activation="relu",max_iter=(2**i),early_stopping=False) results.iloc[0,i] = np.mean(np.abs(cross_val_score(model,train_X.loc[:,:],train_y.values.ravel(),cv=tscv))) results.iloc[1,i] = np.mean(np.abs(cross_val_score(model,test_X.loc[:,:],test_y.values.ravel(),cv=tscv))) results
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Обучение | 19675.492496 | 19765.297106 | 19609.7644 | 19511.588484 | 19859.734807 | 19942.30371 | 18831.617167 | 10703.554068 | 4930.771654 | 1639.952482 | 1389.615052 | 2938.371438 | 1.536765 | 2.193895 | 30.553918 |
| Тестирование | 13171.519137 | 14113.252994 | 14428.159203 | 13649.157525 | 13655.643066 | 12919.773346 | 11472.770729 | 5878.964564 | 11293.444345 | 3788.388634 | 2545.368419 | 3599.364028 | 2240.598518 | 1041.641869 | 882.696622 |
Визуальное представление данных показывает, что нам необходимо выполнить максимальное количество итераций, чтобы получить оптимальный результат от нашей модели. Однако читатель должен быть готов к тому, что наша процедура поиска могла быть прекращена преждевременно. Это значит, что, возможно, мы могли бы получить лучшие результаты, если бы использовали 2 в степени больше 14. Однако из-за вычислительных затрат на обучение этих моделей наш поиск не вышел за пределы 2 в 14-й степени.
plt.title("Neural Network RMSE Forecasting 14 Period MA") plt.ylabel("5 CV RMSE") plt.xlabel("Training Iterations As Powers of 2") plt.grid() sns.lineplot(np.array(results.iloc[1,:]).transpose()) plt.axhline(results.min(1)[1],linestyle='--',color='red') plt.axvline(14,linestyle='--',color='red')

Рис. 8. Результаты поиска оптимального количества итераций обучения для нашей модели глубокой нейронной сети
Теперь, когда наша модель обучена, мы можем подготовиться к экспорту нашей модели в формат ONNX.
import onnx import skl2onnx from skl2onnx.common.data_types import FloatTensorType
Подготовимся к настройке модели, используя оптимальное количество итераций обучения, которое мы оценили.
model = MLPRegressor(hidden_layer_sizes=(5,10,4,2),solver="adam",activation="relu",max_iter=(2**14),early_stopping=False)
Загрузим z-оценки для всего набора данных.
mean_scores = data.loc[:,X].mean() std_scores = data.loc[:,X].std() mean_scores.to_csv("EURUSD StopOut Mean.csv") std_scores.to_csv("EURUSD StopOut Std.csv")
Преобразуем весь набор данных.
data[X] = ((data.loc[:,X] - mean_scores) / std_scores)
Подготовим модель на основе всех имеющихся у нас данных, за исключением дат тестов.
model.fit(data.loc[:,X],data.loc[:,'Target'].values.ravel())
Укажем входную форму нашей модели.
initial_types = [("float_input",FloatTensorType([1,5]))]
Подготовим к конвертации модели в формат ONNX.
model_proto = skl2onnx.convert_sklearn(model,initial_types=initial_types,target_opset=12) Сохраним модель как файл ONNX.
onnx.save(model_proto,"EURUSD StopOut Prevention Model.onnx") Усовершенствованная версия стратегии
Усовершенствуем нашу торговую стратегию. Сначала загрузим модель ONNX, которую мы только что создали.
//+------------------------------------------------------------------+ //| Baseline Model.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" //+------------------------------------------------------------------+ //| Resources | //+------------------------------------------------------------------+ #resource "\\Files\\EURUSD StopOut Prevention Model.onnx" as uchar onnx_model_buffer[];
We shall create a few additional system constants for this version of our application.
//+------------------------------------------------------------------+ //| System constants | //+------------------------------------------------------------------+ #define MA_PERIOD 14 //--- Moving Average Period #define MA_TYPE MODE_EMA //--- Type of moving average we have #define MA_PRICE PRICE_CLOSE //---- Applied Price of Moving Average #define TF_1 PERIOD_D1 //--- Our time frame for technical analysis #define TF_2 PERIOD_M30 //--- Our time frame for managing positions #define VOL 0.1 //--- Our trading volume #define SL_SIZE 1e3 * _Point //--- The size of our stop loss #define SL_ADJUSTMENT 1e-5 * _Point //--- The step size for our trailing stop #define ONNX_MODEL_INPUTS 5 //---- Total model inputs for our ONNX model
Кроме того, наши глобальные z-оценки должны быть загружены в массивы.
//+------------------------------------------------------------------+ //| Our global variables | //+------------------------------------------------------------------+ int ma_handler,system_state; double ma[]; double mean_values[ONNX_MODEL_INPUTS] = {1.157641086508574,1.1581085911361018,1.1571729541088953,1.1576420747040126,1.157640521193191}; double std_values[ONNX_MODEL_INPUTS] = {0.04070388112283021,0.040730761156963606,0.04067819202368064,0.040703752648947544,0.040684857239172416}; double bid,ask,yesterday_high,yesterday_low; const string last_high = "LAST_HIGH"; const string last_low = "LAST_LOW"; long onnx_model; vectorf model_forecast = vectorf::Zeros(1);
Прежде чем использовать наши модели ONNX, необходимо соответствующим образом их настроить и проверить, правильно ли они сконфигурированы.
//+------------------------------------------------------------------+ //| Prepare the resources our EA requires | //+------------------------------------------------------------------+ bool setup(void) { onnx_model = OnnxCreateFromBuffer(onnx_model_buffer,ONNX_DEFAULT); if(onnx_model == INVALID_HANDLE) { Comment("Failed to create ONNX model: ",GetLastError()); return(false); } ulong input_shape[] = {1,ONNX_MODEL_INPUTS}; ulong output_shape[] = {1,1}; if(!OnnxSetInputShape(onnx_model,0,input_shape)) { Comment("Failed to set ONNX model input shape: ",GetLastError()); return(false); } if(!OnnxSetOutputShape(onnx_model,0,output_shape)) { Comment("Failed to set ONNX model output shape: ",GetLastError()); return(false); } ma_handler = iMA(Symbol(),TF_2,MA_PERIOD,0,MA_TYPE,MA_PRICE); if(ma_handler == INVALID_HANDLE) { Comment("Failed to load technical indicator: ",GetLastError()); return(false); } return(true); };
Наша процедура поиска торговых возможностей немного изменится. Сначала мы получим прогноз из нашей модели. В дальнейшем наши условия открытия и закрытия позиций остаются прежними. Однако, помимо выполнения этих условий, мы также будем проверять выполнение новых условий.
//+------------------------------------------------------------------+ //| Check if we have any trading setups | //+------------------------------------------------------------------+ void find_setup(void) { if(!model_predict()) { Comment("Failed to get a forecast from our model"); return; } if((iHigh(Symbol(),TF_2,1) < yesterday_low) && (iHigh(Symbol(),TF_2,2) < yesterday_low)) { if(iClose(Symbol(),TF_2,1) > ma[0]) { check_buy(); } } if((iLow(Symbol(),TF_2,1) > yesterday_high) && (iLow(Symbol(),TF_2,2) > yesterday_high)) { if(iClose(Symbol(),TF_2,1) < ma[0]) { check_sell(); } } }
Новые условия, которые нам необходимо указать, будут применяться как к нашим длинным, так и к коротким позициям. Сначала мы проверим, превышает ли наш прогноз скользящей средней текущее показание индикатора скользящей средней, которое у нас есть. Кроме того, мы также хотим проверить, превышает ли будущее ожидаемое значение индикатора скользящей средней текущее предлагаемое значение цены.
Это означает, что, по мнению системы, тренд, скорее всего, не изменится. Наконец, мы проверим, ожидает ли система, что скользящая средняя останется ниже стоп-лосса. Если все наши условия выполнены, то мы сразу же откроем позицию на рынке.
//+------------------------------------------------------------------+ //| Check if we have a valid buy setup | //+------------------------------------------------------------------+ void check_buy(void) { if((model_forecast[0] > ma[0]) && (model_forecast[0] > iClose(Symbol(),TF_2,0))) { if(model_forecast[0] > (bid - (SL_SIZE))) Trade.Buy(VOL,Symbol(),ask,(bid - (SL_SIZE)),(bid + (SL_SIZE))); } }
Наши условия на продажу работают в обратном порядке.
//+------------------------------------------------------------------+ //| Check if we have a valid sell setup | //+------------------------------------------------------------------+ void check_sell(void) { if((model_forecast[0] < ma[0]) && (model_forecast[0] < iClose(Symbol(),TF_2,0))) { if(model_forecast[0] < (ask + (SL_SIZE))) Trade.Sell(VOL,Symbol(),bid,(ask + (SL_SIZE)),(ask - (SL_SIZE))); } }
Открыв позицию, мы должны продолжать следить за ней. Наша функция обновления стоп-лосса будет служить двум целям в зависимости от того, как она вызывается. Она принимает параметр-флаг, который изменяет его поведение. Если флаг установлен на 0, мы просто ищем возможность сдвинуть наши стоп-уровни в сторону более выгодных цен. В противном случае, если флаг установлен на 1, мы сначала хотим получить новый прогноз из нашей модели и проверить, может ли будущее значение скользящей средней превысить наш текущий уровень стоп-лосса.
Если ожидается, что скользящая средняя превысит наш стоп-лосс, но все равно сформирует прибыльное движение, то мы скорректируем стоп-лосс до уровня, которого, как мы ожидаем, достигнет скользящая средняя в момент своего пика. В противном случае, если ожидается, что сделка упадет ниже цены открытия, то мы хотим дать команду приложению рисковать меньше в таких сделках, которые показывают небольшой потенциал прибыли.
//+------------------------------------------------------------------+ //| Update our stop loss | //+------------------------------------------------------------------+ void update_sl(int flag) { //--- First find our open position if(PositionSelect(Symbol())) { double current_sl = PositionGetDouble(POSITION_SL); double current_tp = PositionGetDouble(POSITION_TP); double open_price = PositionGetDouble(POSITION_PRICE_OPEN); //--- Flag 0 means we just want to push the stop loss and take profit forward if its possible if(flag == 0) { //--- Buy Setup if(current_tp > current_sl) { if((bid - SL_SIZE) > current_sl) Trade.PositionModify(Symbol(),(bid - SL_SIZE),(bid + SL_SIZE)); } //--- Sell setup if(current_tp < current_sl) { if((ask + SL_SIZE) < current_sl) Trade.PositionModify(Symbol(),(ask + SL_SIZE),(ask - SL_SIZE)); } } //--- Flag 1 means we want to check if the stop loss may be hit soon, and act accordingly if(flag == 1) { model_predict(); //--- Buy setup if(current_tp > current_sl) { if(model_forecast[0] < current_sl) { if((model_forecast[0] > ma[0]) && (model_forecast[0] > yesterday_low)) Trade.PositionModify(Symbol(),model_forecast[0],current_tp); } if(model_forecast[0] < open_price) Trade.PositionModify(Symbol(),model_forecast[0] * 1.5,current_tp); } //--- Sell setup if(current_tp < current_sl) { if(model_forecast[0] > current_sl) { if((model_forecast[0] < ma[0]) && (model_forecast[0] < yesterday_high)) Trade.PositionModify(Symbol(),model_forecast[0],current_tp); } if(model_forecast[0] > open_price) Trade.PositionModify(Symbol(),model_forecast[0] * 0.5,current_tp); } } } }
Наша процедура обновления немного изменена для вызова функции обновления стоп-лосса.
//+------------------------------------------------------------------+ //| Perform our update routines | //+------------------------------------------------------------------+ void update() { //--- Daily procedures { static datetime time_stamp; datetime current_time = iTime(Symbol(),TF_1,0); if(time_stamp != current_time) { yesterday_high = iHigh(Symbol(),TF_1,1); yesterday_low = iLow(Symbol(),TF_1,1); //--- Mark yesterday's levels ObjectDelete(0,last_high); ObjectDelete(0,last_low); ObjectCreate(0,last_high,OBJ_HLINE,0,0,yesterday_high); ObjectCreate(0,last_low,OBJ_HLINE,0,0,yesterday_low); } } //--- M30 procedures { static datetime time_stamp; datetime current_time = iTime(Symbol(),TF_2,0); if(time_stamp != current_time) { time_stamp = current_time; //--- Get updated prices bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); //--- Update our technical indicators CopyBuffer(ma_handler,0,0,1,ma); //--- Check for a setup if(PositionsTotal()==0) find_setup(); //--- Check for a setup if(PositionsTotal() > 0) update_sl(1); } } //--- Per tick procedures { //--- These function calls can become expensive and may slow down the speed of your back tests //--- Be thoughtful when placing any function calls in this scope update_sl(0); } }
Нам также нужна специальная функция, отвечающая за извлечение прогнозов из нашей модели нейронной сети. Сначала мы подготовим входные данные к типу вектора с плавающей точкой, а затем приступим к стандартизации входных данных, чтобы иметь возможность получить прогноз из нашей модели.
//+------------------------------------------------------------------+ //| Get a forecast from our deep neural network | //+------------------------------------------------------------------+ bool model_predict(void) { double ma_input[] = {0}; CopyBuffer(ma_handler,0,1,1,ma_input); vectorf model_inputs = { (float) iOpen(Symbol(),TF_2,1), (float) iHigh(Symbol(),TF_2,1), (float) iLow(Symbol(),TF_2,1), (float) iClose(Symbol(),TF_2,1), (float) ma_input[0] }; for(int i = 0; i < ONNX_MODEL_INPUTS;i++) { model_inputs[i] = (float)((model_inputs[i] - mean_values[i]) / std_values[i]); } if(!OnnxRun(onnx_model,ONNX_DEFAULT,model_inputs,model_forecast)) { Comment("Failed to obtain forecast: ",GetLastError()); return(false); } Comment(StringFormat("Expected MA Value: %f",model_forecast[0])); return(true); }
Lastly, when our application is no longer in use, we will release the indicator and the ONNX model.
//+------------------------------------------------------------------+ //| Free resources we are no longer using up | //+------------------------------------------------------------------+ void release(void) { OnnxRelease(onnx_model); IndicatorRelease(ma_handler); } //+------------------------------------------------------------------+
При анализе кривой эквити, полученной с помощью нашей новой усовершенствованной версии торгового алгоритма, мы можем быстро заметить, что характерный отрицательный наклон, который мы наблюдали при первой реализации стратегии, был исправлен, и теперь наша стратегия демонстрирует положительную тенденцию с периодическими провалами. Это более желательно, чем первоначальное состояние нашей стратегии.

Рис. 9. Визуализация кривой прибыли, полученной с помощью нашей усовершенствованной версии алгоритма предотвращения стоп-аута
При ближайшем рассмотрении мы обнаруживаем, что наша новая стратегия теперь прибыльна. Первоначальная версия нашей стратегии принесла убыток примерно в USD 1000, а наша текущая версия принесла прибыль чуть больше USD 1000. Результаты значительно улучшились. Наш первоначальный коэффициент Шарпа был -0,39, а новый составляет 0,79. Значение средней прибыльной сделки выросло с USD 98 до USD 130, в то время как значение средней убыточной сделки сократилась с USD 102 до USD 63. Таким образом, наша средняя прибыль растет значительно быстрее, чем наши средние убытки. С такими результатами уже можно задуматься об использовании этой версии стратегии в торговле.
Несмотря на то, что мы добились значительного прогресса, проблема преждевременного выхода из сделки по-прежнему остается сложной для решения. Об этом свидетельствует тот факт, что около 60% всех открытых нами позиций оказались убыточными. Сложно полностью отсортировать все сделки, которые приведут к закрытию позиции трейдера, однако на сегодня нам удалось отсортировать большинство крупных и убыточных сделок.

Рис. 10. Подробный анализ результатов, полученных с помощью нашего нового алгоритма предотвращения стоп-аутов
Заключение
В этой статье мы познакомились с потенциальным решением давней проблемы преждевременного выхода из прибыльных сделок. Эта проблема часто стоит на пути успешной торговли и, возможно, никогда не будет полностью решена. Каждое новое решение привносит в стратегию свой собственный набор уязвимостей. После прочтения этой статьи читатель получит более количественную структуру управления уровнями стоп-лосс. Выявление и отфильтровывание сделок, которые могут привести к неоправданным потерям, является важнейшим компонентом любой торговой стратегии.
| Имя файла | Описание файла |
|---|---|
| Baseline Model.mq5 | Наша изначальная торговая стратегия, которую мы стремились превзойти. |
| Stop Out Prevention Model.mq5 | Усовершенствованная версия торговой стратегии на основе глубокой нейронной сети. |
| EURUSD Stop Out Moving Average Model.ipynb | Jupyter Notebook, который мы использовали для анализа финансовых данных, извлеченных из нашего терминала MetaTrader 5. |
| EURUSD Stop Out Prevention Model.onnx | Наша глубокая нейронная сеть. |
| Fetch Data MA.mq5 | Скрипт MQL5 для получения необходимых рыночных данных. |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/17213
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Моделирование рынка (Часть 10): Сокеты (IV)
Разработка динамического советника на нескольких парах (Часть 3): Стратегии возврата к среднему и моментума
Быстрая интеграция большой языковой модели и MetaTrader 5 (Часть I): Создаем модель
Разработка инструментария для анализа движения цен (Часть 14): Parabolic Stop and Reverse
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования