Содержание
Всем привет! Добро пожаловать в четвертый раздел по программированию на языке MQL4.
В третьем разделе мы научились писать советники и даже переводить Стоп Лосс в безубыток. Пришло время попробовать ввести Траллинг Стоп в наш код mql4. Что такой тралл? Если простыми словами, то это последовательный перенос Стоп Лосса вслед за ценой. Цель этого действия — сократить убытки, вначале перенеся СЛ на уровень безубытка, а затем на положительную отметку, постепенно ее увеличивая.
О плюсах и минусах trailing stop в торговли можно спорить бесконечно, но факт в том, что вы не узнаете нужен ли он в вашей торговой системе, пока не попробуете. Так давайте же напишем отдельную функцию траллинга всех открытых ордеров.
Я сказал одну функцию? Нет уж, давайте напишем две… хотя, лучше три разных вариации тралла в одном советнике с возможностью выбора одного из них в настройках.
Создаем простой советник
Чтобы иметь возможность что-то тралить, нужен советник. Создавать его с нуля мы не будем, ведь это не тема текущего урока. Поэтому без лишних объяснений и подробностей просто возьмем уже подготовленный вариант на тему торговли по сигналам двух скользящих средних.
Суть советника крайне проста — быстрая МА пересекла медленную снизу вверх — входим в покупки. Продажи зеркально. О прибыльности данного метода философствовать мы не будем, советник нужен только для примера.
Создаем новый проект в Meta Editor, выбираем тип «Советник». Указываем ему свойства и внешние переменные. Сразу добавим перечисление enum trail, которое позволит выбрать один из трех траллов. Настройки самого тралла внесем позже.
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
//+------------------------------------------------------------------+ //| 4.0 Trailing stop | //| Copyright (c) DaVinci FX Group | //| https://www.davinci-fx.com/ | //+------------------------------------------------------------------+ #property copyright "Copyright (c) DaVinci FX Group" #property link "https://www.davinci-fx.com/" #property description "Trailing stop EA" #property version "1.00" #property strict #include <..\Libraries\stdlib.mq4> //библиотека для расшифровки ошибок enum trail { Classic = 1, Complex = 2, Parabolic = 3 }; extern string s0 = "<== General Settings ==>"; //> > > > > > > extern double Lot = 0.01; extern int Slippage = 5; extern double StopLoss = 20; extern double TakeProfit = 30; extern string Comments = "DaVinci EA"; extern int MagicNumber = 123123; extern string s1 = "<== Indicator Settings ==>"; //> > > > > > > extern int ma1_period = 50; extern int ma2_period = 100; datetime Update_Time = 0; int PipsDivided = 1; |
Добавляем в функцию обработки событий OnInit условие для перевода в пятизнак старых пунктов. Функция OnDeinit нам не нужна.
0 1 2 3 4 5 6 7 8 9 10 |
int OnInit() { if (Digits == 3 || Digits == 5) { Slippage *= 10; StopLoss *= 10; TakeProfit *= 10; PipsDivided = 10; } return(INIT_SUCCEEDED); } |
Основной код в функции OnTick будет проверять раз в свечу сигнал на вход и открывать ордера. Ну и не забываем про проверку на ошибки.
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { if(Update_Time != iTime(NULL,0,0)) { //обновлять данные всех индикаторов раз в период Update_Time = iTime(NULL,0,0); //перезаписываем значение переменной для хранения времени текущей свечи //импорт данных индикатора Moving Averages. double MA1_1 = iMA(NULL,0,ma1_period,0,MODE_EMA,PRICE_CLOSE,1); double MA1_2 = iMA(NULL,0,ma1_period,0,MODE_EMA,PRICE_CLOSE,2); double MA2_1 = iMA(NULL,0,ma2_period,0,MODE_EMA,PRICE_CLOSE,1); double MA2_2 = iMA(NULL,0,ma2_period,0,MODE_EMA,PRICE_CLOSE,2); if(CountOrder(OP_BUY) == 0 && MA1_1 > MA2_1 && MA1_2 <= MA2_2) { //условие для открытия ордера на покупку OpenTrade(OP_BUY); //открытие ордера на покупку } if(CountOrder(OP_SELL) == 0 && MA1_1 < MA2_1 && MA1_2 >= MA2_2) { //условие для открытия ордера на продажу OpenTrade(OP_SELL); //открытие ордера на продажу } } int Error = GetLastError(); //поиск ошибок по завершению тика if(Error != 0) Print("OnTick() Error ",Error,": ",ErrorDescription(Error)); } |
Подсчет количества ордеров в рынке в заданном направлении будет выполнять функция CountOrder():
0 1 2 3 4 5 6 7 8 9 10 11 |
//+------------------------------------------------------------------+ int CountOrder(int OP_TYPE) { int orders=0; for(int i=OrdersTotal()-1;i>=0;i--){ if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES)==false) continue; if(OrderSymbol() !=_Symbol || OrderMagicNumber() != MagicNumber) continue; if(OrderType() != OP_TYPE) continue; orders++; } return orders; } |
Открытие и модификацию ордеров выполняет функция OpenTrade():
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
void OpenTrade(int OP_Type) { //Функция открытия ордера double price = (OP_Type == OP_BUY ? Ask : Bid); color col_type = (OP_Type == OP_BUY ? clrBlue : clrRed); string op_str = (OP_Type == OP_BUY ? "на покупку" : "на продажу"); Print("Открываем ордер " + op_str); int ticket = OrderSend(Symbol(), OP_Type, Lot, price, Slippage, 0, 0, Comments, MagicNumber, 0, col_type); if(ticket > 0) { //Если ордер был открыт if(OrderSelect(ticket, SELECT_BY_TICKET, MODE_TRADES)) { double SL = 0, TP = 0; //определяем цели для ордера if(OP_Type == OP_BUY) { if(StopLoss > 0) SL = OrderOpenPrice()-StopLoss*Point; if(TakeProfit > 0) TP = OrderOpenPrice()+TakeProfit*Point; } else { if(StopLoss > 0) SL = OrderOpenPrice()+StopLoss*Point; if(TakeProfit > 0) TP = OrderOpenPrice()-TakeProfit*Point; } Print("Модифицируем ордер " + op_str); for(int n = 0; n<3; n++) { //три попытки на модификацию ордера if(!OrderModify(ticket, price, SL, TP, 0, clrNONE)) { int Error = GetLastError(); Print("Ошибка модификации ордера ",Error,": ",ErrorDescription(Error)); Sleep(3000); RefreshRates(); } else break; } } } else { int Error = GetLastError(); Print("Ошибка открытия ордера ",Error,": ",ErrorDescription(Error)); } } |
Все, советник готов. Просто вставьте этот код в ваш редактор, скомпилируйте и вы увидите, что в нем нет абсолютно ничего сложного.
«Простота есть необходимое условие прекрасного» © Лев Николаевич Толстой
Добавляем настройки trailing stop
Мы определились, что у нас будет три Трайлинг Стопа, а именно:
- Классический тралл по канонам отцов основателей.
- Усложненный тралл, где отдельно рассчитывается расстояние до активации, дистанция и шаг, и все это в процентах.
- Тралл по индикатору терминала Parabolic SAR.
Для каждого из них нужно добавить внешние переменные на глобальном уровне, а именно вариант выбора ChooseTrail, отвечающий за то, какой тралл будет сейчас активен, настройки дистанции, шага и индикатора:
0 1 2 3 4 5 6 7 8 9 10 |
extern string s2 = "<== Trailing Stop Settings ==>"; //> > > > > > > extern trail ChooseTrail = 1; //Choose Trailing Stop extern string s21 = " < Classic >"; //> extern double TrailingDist = 0; //Trailing Distance pips extern string s22 = " < Complex >"; //> extern double TrailingStartPercent = 0; //Trailing Start Percent extern double TrailingDistancePercent = 0; //Trailing Distance Percent extern double TrailingStep = 1; //Trailing Step extern string s23 = " < Parabolic >"; //> extern double InpSARStep = 0.02; //Parabolic Step extern double InpSARMaximum = 0.2; //Parabolic Maximum |
Не забудем значения в пунктах перевести в пятизнак в функции OnInit:
0 1 2 3 |
if (Digits == 3 || Digits == 5) { TrailingDist *= 10; TrailingStep *= 10; } |
Создаем классический Траллинг Стоп
Стандартный Трайлинг Стоп разработан компанией MetaQuotes и встроен в терминал MetaTrader 4.
Его цель проста: указываете необходимое значение в пунктах и как только цена его проходит в вашу сторону, стоп начинает передвигать вслед за ней. Соответственно модификация по возможности происходит каждый тик и шаг равен минимально допустимому, т.е. пипсу. Дистанция равна расстоянию активации.
Давайте добавим в самый вверх OnTick условие активации функции для этого первого варианта:
0 |
if(ChooseTrail == 1 && TrailingDist > 0) ClassicTrail(); //если тралл классический |
Соответственно каждый тик советник будет запускать функцию ClassicTrail() и проверять, нужно ли передвинуть Стоп Лосс.
Создаем пользовательскую функцию ClassicTrail(). Ее суть — перебором пройтись по всем ордерам и сравнить расстояние от текущей цены до установленного Стоп Лосса.
0 1 2 3 4 5 6 7 8 9 10 |
void ClassicTrail() { //1 Вариант: Классический тралл int Order_total = OrdersTotal(); for(int i=Order_total-1; i>=0; i--) { //цикл по всем открытым ордерам if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue; //если не получилось выделить ордер - пропускаем if(OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber && OrderProfit() > 0 && OrderStopLoss() != 0 && OrderType() < 2) { //...дальнейший код } } } |
В коде выше видно, что цикл for выделяет каждый открытый ордер по очереди и если выделение невозможно, то пропускает его. Далее идет проверка, чтобы символ ордера был таким же, как символ текущего графика, на котором установлен советник. Потом следует проверка на магик номер ордера, условие, чтобы ордер был в плюсе, его Стоп Лосс был выставлен и тип данного ордера был рыночным.
Теперь в зависимости от типа ордера нужно дописать дополнительные условия, чтобы в итоге вся функция выглядела так:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
void ClassicTrail() { //1 Вариант: Классический тралл int Order_total = OrdersTotal(); for(int i=Order_total-1; i>=0; i--) { //цикл по всем открытым ордерам if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue; //если не получилось выделить ордер - пропускаем if(OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber && OrderProfit() > 0 && OrderStopLoss() != 0 && OrderType() < 2) { if(OrderType() == OP_BUY) { //если ордер на покупку double NewSL = NormalizeDouble(Ask - TrailingDist*Point,Digits); //узнаем расстояние для нового СЛ if(NewSL > OrderStopLoss()) { //Если новый Стоп Лосс больше предыдущего, значит цена прошла в нашу сторону if(OrderModify(OrderTicket(), OrderOpenPrice(), NewSL, OrderTakeProfit(), 0)) { //модификация СЛ ордера Print("Trailing Stop: Стоп Лосс ордера на покупку #",OrderTicket()," перенесен на цену ", DoubleToString(NewSL,Digits)); } } } else if(OrderType() == OP_SELL) { //если ордер на продажу double NewSL = NormalizeDouble(Bid + TrailingDist*Point,Digits); if(NewSL < OrderStopLoss()) { if(OrderModify(OrderTicket(), OrderOpenPrice(), NewSL, OrderTakeProfit(), 0)) { Print("Trailing Stop: Стоп Лосс ордера на продажу #",OrderTicket()," перенесен на цену ", DoubleToString(NewSL,Digits)); } } } } } } |
Рассмотрим условие выше для ордера на покупку. Вначале мы высчитываем новый Стоп Лосс NewSL, он должен быть на расстоянии TrailingDist от текущей цены. Нужно заметить, что для покупок мы учитываем цену Ask, тогда как для продаж цену Bid, так же, как и при открытии ордеров. Далее происходит сравнение расчетного и текущего Стоп Лосса. Если расчетный получился выше фактического, то мы понимаем, что текущая цена уже сместились выше и нам необходимо передвинуть наш Стоп Лосс.
Продажи модифицируются зеркально: если расчетный СЛ ниже существующего, значит пора передвигать текущий.
Все, теперь как только цена пройдет расстояние TrailingDist от цены открытия ордера — произойдет модификация, которая будет продолжаться каждый тик, пока рынок это позволит делать. В итоге ордер будет зажат между Тейк Профитом и Стоп Лоссом, и один из них сработает.
Усложненный Трайлинг Стоп по процентам
Первый вариант тралла уже можно считать устаревшим, ведь он абсолютно не гибкий в настройках. Следующий complex тралл содержит уже три параметра: расстояние для активации, дистанция самого тралла и его шаг.
Мы долгое время думали с командой, как лучше рассчитывать тралл и пришли к мнению, что правильнее использовать не пункты, а значение в процентах от Тейк Профита. Тут все просто. При оптимизации советника генетический алгоритм прогоняет тысячи вариантов в тестере и если указывать ТП и тралл в пунктах, то очень часто попадаются варианты, когда цена активации тралла оказывается больше значения Тейка. В таком случае данный тралл просто не будет работать, он не сможет активироваться, соответственно такой прогон будет в пустую. Да и варианты, когда ТП в 100 пунктов, а тралл активируется при прохождении 15 пунктов не очень привлекательные. По этой причине мы будем задавать его в процентах.
Добавляем в функцию OnTick второе условия для активации данного тралла:
0 1 2 |
//активируем выбрранный тралл каждый тик if(ChooseTrail == 1 && TrailingDist > 0) ClassicTrail(); //если тралл классический else if(ChooseTrail == 2 && TrailingStartPercent > 0 && TrailingDistancePercent > 0) ComplexTrail(); |
Я сразу выложу полный код функции ComplexTrail() и по порядку опишу, что происходит в нем.
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
void ComplexTrail() { //2 Вариант: Тралл по расстоянию в процентах int Order_total = OrdersTotal(); for(int i=Order_total-1; i>=0; i--) { if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue; //если не получилось выделить ордер if(OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber && OrderProfit() > 0 && OrderStopLoss() != 0 && OrderType() < 2) { double TrailingStart = MathAbs((OrderTakeProfit()-OrderOpenPrice())*TrailingStartPercent/100); double TrailingDistance = MathAbs((OrderTakeProfit()-OrderOpenPrice())*TrailingDistancePercent/100); if(OrderType() == OP_BUY) { //если ордер на покупку if(Ask > OrderOpenPrice()+TrailingStart) { if(Ask - OrderStopLoss() >= TrailingDistance + TrailingStep*Point) { if(PrintTrailingMessageB) { //пишем первое сообщение при активации тралла Print("Trailing Stop для покупок активирован по текущей цене " + DoubleToStr(Ask,Digits) + ". Дистанция " + DoubleToStr(TrailingDistance/Point/PipsDivided,1)); PrintTrailingMessageB = false; //отключаем возможность писать это сообщение до появления нового ордера } double NewSL = NormalizeDouble(Ask - TrailingDistance,Digits); //расчет нового по нашей дистанции if(OrderModify(OrderTicket(), OrderOpenPrice(), NewSL, OrderTakeProfit(), 0)) { //модификация ордера/тралл Print("Trailing Stop: Стоп Лосс ордера на покупку #",OrderTicket()," перенесен на цену ", DoubleToString(NewSL,Digits)); } } } } else if(OrderType() == OP_SELL) { //если ордер на продажу if(Bid < OrderOpenPrice()-TrailingStart) { if(OrderStopLoss() - Bid >= TrailingDistance + TrailingStep*Point) { if(PrintTrailingMessageS) { Print("Trailing Stop для продаж активирован по текущей цене " + DoubleToStr(Bid,Digits) + ". Дистанция " + DoubleToStr(TrailingDistance/Point/PipsDivided,1)); PrintTrailingMessageS = false; } double NewSL = NormalizeDouble(Bid + TrailingDistance,Digits); if(OrderModify(OrderTicket(), OrderOpenPrice(), NewSL, OrderTakeProfit(), 0)) { //модификация ордера/тралл Print("Trailing Stop: Стоп Лосс ордера на продажу #",OrderTicket()," перенесен на цену ", DoubleToString(NewSL,Digits)); } } } } } } } |
В данной функции мы аналогично запускаем цикл for по всем открытым ордерам. Чтобы перевести проценты в пункты мы должны взять установленный ТП и цену открытия, и рассчитать от них расстояние начала тралла TrailingStart и дистанцию TrailingDistance.
Далее на примере покупок. Если текущая цена Ask уже ушла вверх на расстояние больше, чем TrailingStart от цены открытия ордера, значит пришло время активировать тралл. Следующее условие проверяет, чтобы расстояние от текущей цены до установленного СЛ стало больше, чем рассчитанная нами дистанция плюс шаг.
Если оба условия соблюдены в первый раз, то советник выдает нам принт в журнал об этом и далее блокирует функцию принта с помощью bool переменной PrintTrailingMessageB. Далее следует расчет нового СЛ, который равен разнице между текущей ценой и нашей дистанцией. Значение переменной NewSL будет точно выше существующего СЛ из-за проверки, что была до этого, поэтому просто модифицируем ордер с новым СЛ и выдаем об этом принт в журнал.
Все, следующий раз ордер будет модифицирован только, когда цена пройдет указанную дистанцию плюс шаг. То есть при таком варианте модификация будет происходить с заданным шагом, а не каждый тик.
Нужно заметить, что тралл для покупок не может опускаться назад за ценой, если она идет вниз, он либо стоит на месте, либо растет вслед за ней, когда расстояние увеличивается. То же самое зеркально для продаж.
Давайте посмотрим в журнале, что у нас получилось:
В настройках ТП = 30пп, тралл активируется на расстоянии 50% до ТП, дистанция его от цены равна 40%, шаг 1 пункт.
- Советник открыл ордер на покупку по цене 0.94778.
- Через какое-то время тралл активировался по цене 0.94928, т.к. цена прошла (0.94928-0.94778)*10000 = 15 старых пунктов (50% от ТП). Дистанция установки СЛ равна 40% от 30пп, т.е. 12 пунктам.
- Первый СЛ был выставлен по цене 0.94808, потому что расстояние от цены активации (0.94928) до данного СЛ как раз равно 12 пунктам. Далее мы видим, что как только цена проходила расстояние больше или равное 1 пункту Стоп Лосс подтягивался ближе к ней.
- Цена не дошла до Тейк Профита и сработал Стоп Лосс по цене 0.94889, которая и была рассчитана при последней модификации тралла.
Трайлинг Стоп по Параболик САР
Остается последний пример, но в своей логике он не сильно отличается от предыдущих. Для данного тралла нам не нужно рассчитывать расстояние, дистанцию и шаг, все это сделает индикатор, который рисует точки на графике:
Соответственно, когда у нас ордер на покупку, мы будем переносить Стоп Лосс по точкам, которые находятся ниже свечей и текущей цены и наоборот для продаж.
Точки параболика хранятся в данных буфера, плюс это стандартный индикатор Meta Trader, поэтому мы просто не можем не попробовать его в нашем коде.
Так как значения индикатора закрепляются только после закрытия свечи, то данный тралл должен работать только один раз при открытии новой свечи. Поэтому в функцию обработки событий OnTick в условие, которые соблюдается только раз в свечу добавляем следующий код:
0 1 2 3 4 |
if(Update_Time != iTime(NULL,0,0)) { //обновлять данные всех индикаторов раз в период Update_Time = iTime(NULL,0,0); //перезаписываем значение переменной для хранения времени текущей свечи if(ChooseTrail == 3 && InpSARStep > 0 && InpSARMaximum > 0) ParabolicTrail(); //активируем тралл по параболику //...дальнейшие код |
Создаем функцию третьего тралла:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
void ParabolicTrail() { //3 Вариант: Тралл по параболику double PSAR = iSAR(NULL,0,InpSARStep,InpSARMaximum,1); int Order_total = OrdersTotal(); for(int i=Order_total-1; i>=0; i--) { if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue; //если не получилось выделить ордер if(OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber && OrderProfit() > 0 && OrderStopLoss() != 0 && OrderType() < 2) { if(OrderType() == OP_BUY) { //если ордер на покупку if(PSAR < Ask && PSAR > OrderStopLoss()) { if(PrintTrailingMessageB) { Print("Trailing Stop для продаж активирован по текущей цене " + DoubleToStr(Bid,Digits)); PrintTrailingMessageB = false; } if(OrderModify(OrderTicket(), OrderOpenPrice(), PSAR, OrderTakeProfit(), 0)) { Print("Trailing Stop: Стоп Лосс ордера на покупку #",OrderTicket()," перенесен на цену ", DoubleToString(PSAR,Digits)); } } } else if(OrderType() == OP_SELL) { //если ордер на продажу if(PSAR > Bid && PSAR < OrderStopLoss()) { if(PrintTrailingMessageS) { //пишем первое сообщение при активации тралла Print("Trailing Stop для покупок активирован по текущей цене " + DoubleToStr(Ask,Digits)); PrintTrailingMessageS = false; } if(OrderModify(OrderTicket(), OrderOpenPrice(), PSAR, OrderTakeProfit(), 0)) { Print("Trailing Stop: Стоп Лосс ордера на продажу #",OrderTicket()," перенесен на цену ", DoubleToString(PSAR,Digits)); } } } } } } |
Смотрим код. Изначально нужно получить данные индикатора PSAR, для этого воспользуемся функцией iSAR. Все передаваемые параметры понятны: символ, ТФ, шаг изменения цены и максимальный шаг берется из внешних настроек. Импорт данных буфера будет происходить на первой закрытой свече.
Далее следует уже известный нам цикл по всем открытым ордерам. На примере покупок мы должны убедиться, что точки индикатора находятся ниже текущей цены, а также, что новая точка образовалась выше установленного Стоп Лосса. Если эти два простых условия соблюдены — выдаем принты и модифицируем ордер с СЛ равным значению параболика.
Соответственно на этой свече больше никаких модификаций функция тралла производить не будет. Как только откроется новая свеча, функция запустится заново и если новая точка будет выше предыдущей и ниже текущей цены, то произойдет новое изменение Стоп Лосса.
Заключение
Как видите, функция Траллинг Стопа не такая сложная, как могло показаться. Мы рассмотрели пример трех распространенных траллов, но стоит заменить, что их огромное множество. Существуют трайлинг стопы по фракталам, по теням свечей, раз в заданный период, по скользящим средним и так далее. Но, зная пройденные выше основы, вам не составит труда попробовать и другие вариации в своем коде.
Сам по себе трайлинг Стоп имеет смысл использовать в трендовых стратегиях, когда у ордера большой Тейк Профит и Стоп Лосс постепенно подтягивается за ценой при сильном трендовом движении. Так как мне для торговли больше подходят контртрендовые стратегии, то в моем случае, сколько бы я не пробовал внедрить тралл в свой код, улучшений результатов торговли с ним я не наблюдал.
На этом все, всем профитов!
[download url=»http://www.davinci-fx.com/wp-content/uploads/2021/05/4.0-Trailing-stop.rar» title=»Скачать пример урока»]Не забудьте ознакомиться с рекомендуемыми нами брокерами для торговли, ведь любой, даже хорошо написанный советник должен торговать в адекватных условиях с низким спредом и минимальным проскальзыванием.