Содержание
Всем привет. На этом уроке мы продолжим изучать теорию создания торговых советников. На прошлом занятии мы использовали систему рандомного входа в рынок, которая показала свою несостоятельность, ведь Форекс это не игра в казино и случайности не могут принести доход трейдеру.
Давайте же попробуем модифицировать уже изученный код и добавим условие входа по пересечению двух скользящих средних (МА) разного периода. Суть урока — научиться использовать сигналы классических индикаторов в торговой системе.
Алгоритм работы советника на MQL4
Представленная последовательность действий схематически показывает процесс работы кода будущего советника. Все действия эксперта также будут происходить в функции обработки событий OnTick. Попробую вкратце описать логику работы: в терминал поступил новый тик, функция OnTick запустился в работу. Советник циклом перебирает все открытые ордера, чтобы провести их подсчет, а также модифицировать те ордера, у которых по какой-либо причине нет выставленных целей. Дальнейшие действия происходят только раз в свечу, а именно: анализ показаний индикаторов, поиск сигнала и открытие ордера с последующим закрытием противоположного.
Торговая система по пересечению двух скользящих средних
Так как этот урок по созданию советника mql4 является вторым по счету, то особо мудрить с торговой системой мы не будем и обратимся к классике. Одной из самых первых стратегий, что я узнал, изучая мир Форекс, была торговля по пересечению скользящих средних. Суть ее логики проста, как две копейки: берутся показания двух Moving Average и сравниваются их положения на графике относительно друг друга. Если быстрая МА пересекла медленную сверху вниз, то стоит предположить, что тренд вниз и это сигнал на продажу. Соответственно при пересечении снизу вверх дает сигнал на покупку. Период торговли пусть будет М15.
На скриншоте медленная МА показана фиолетовым цветом, а быстрая желтым.
Основной минус при использовании скользящих средних заключается в том, что они запаздывают и показывают направление тренда уже после того, как тот сформировался. Плюс при наступлении флета она дает очень большое количество ложных сигналов. Что ж, попробуем проверить как себя покажет советник по этой тривиальной торговой системе.
Пишем программный код советника
Как я уже писал выше, мы будем модифицировать код, который изучили на прошлом уроке, так что можете использовать его в качестве подложки для редактирования, если вам так удобнее.
Начнем с включения библиотеки ошибок и объявления внешних переменных, необходимых для открытия ордеров. Это размер торгового лота, проскальзывание, СЛ, ТП, комментарий и магик номер. Из новых переменных у нас появится bool переключатель для закрытия существующего ордера при появлении противоположного сигнала индикатора, а также настройки скользящей средней. Для МА нам важно знать ее период, а также тип сглаживания.
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 |
//+------------------------------------------------------------------+ //| 3.1 Two MA | //| 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 version "1.00" #property strict #include <..\Libraries\stdlib.mq4> //библиотека для расшифровки ошибок extern string s0 = "<== General Settings ==>"; //> > > extern double Lot = 0.01; extern int Slippage = 5; extern double StopLoss = 30; extern double TakeProfit = 40; extern string Comments = "DaVinci EA"; extern int MagicNumber = 123123; extern bool CloseOppositeOrder = true; extern string s1 = "<== Indicator Settings ==>"; //> > > extern int ma1_period = 50; extern int ma2_period = 100; extern int ma1_method = MODE_EMA; extern int ma2_method = MODE_EMA; |
Функция OnInit() будет включать в себя только оператор, ответственный за умножение внешних параметров в пунктах на десять при условии, чтобы брокер использует пятизнак (трех- для JPY).
0 1 2 3 4 5 6 7 |
int OnInit() { if (Digits == 3 || Digits == 5) { Slippage *= 10; StopLoss *= 10; TakeProfit *= 10; } return(INIT_SUCCEEDED); } |
Функция OnDeinit() в нашем коде использоваться не будет, нет нужды.
Переходим к функции OnTick(). Сначала мы активируем цикл for, который будет подсчитывать и модифицировать уже открытые ордера. Подсчет нужен, чтобы не открыть второй ордер в таком же направлении, когда первый еще в рынке. Соответственно, переменная cnt_b будет хранить в себе актуальное количество открытых ордеров на покупку, а cnt_s на продажу. Перебор идет только по рыночным ордерам (выделяется по MODE_TRADES), начиная от максимального количества открытых ордеров OrdersTotal() в сторону уменьшения. После выделения ордера идет стандартная проверка на то, чтобы он был рыночным, его символ совпадал с текущим, как и магик номер соответствовал магику вашего советника.
Чтобы не возвращаться потом к этому циклу, мы сразу же добавим в него проверку на наличие целей у ордеров и последующую модификацию при их отсутствии. Тут все просто. Если у ордера нет ТП или СЛ, то идет его расчет в зависимости от внешних значений и происходит модификация с последующим принтом об успешности операции.
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 |
int cnt_b = 0, cnt_s = 0; int _OrdersTotal = OrdersTotal(); for(int pos = _OrdersTotal - 1; pos >= 0; pos--) { if(!OrderSelect(pos, SELECT_BY_POS, MODE_TRADES)) { Print(__FUNCTION__ + ": не удалось выделить ордер! " + ErrorDescription(GetLastError())); } else if(OrderType() <= 2 && OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber) { if(OrderType() == OP_BUY) cnt_b++; else cnt_s++; //проверка на наличие целей у ордеров и их модификация if(OrderTakeProfit() == 0 || OrderStopLoss() == 0) { double SL = 0, TP = 0; if(OrderType() == OP_BUY) { SL = OrderOpenPrice()-StopLoss*Point; TP = OrderOpenPrice()+TakeProfit*Point; } else { SL = OrderOpenPrice()+StopLoss*Point; TP = OrderOpenPrice()-TakeProfit*Point; } if(!OrderModify(OrderTicket(), OrderOpenPrice(), SL, TP, 0, clrNONE)) { int Error = GetLastError(); Print("Ошибка модификации ордера ",Error,": ",ErrorDescription(Error)); } else Print("Ордер #" + IntegerToString(OrderTicket()) + " успешно модифицирован"); } } } |
Если вы спрашиваете себя, зачем вначале модифицировать ордера, а уже после только отрывать, то тут все логично. Как я объяснял на прошлом уроке, для современных типов торговых счетов, таких как NDD, ECN, STP нельзя модифицировать ордер сразу при его открытии, поэтому действие выполняется поэтапно — открыли, потом выставили цели. В прошлом примере советника мы не учти возможность того, что сервер нам может не позволить модифицировать уже открытый ордер по какой-либо внутренней ошибке. В таком случае этот ордер останется болтаться в рынке без ТП и СЛ, пока вы этого не заметите сами. Это небезопасный подход. Поэтому в этой версии советника мы вводим проверку на наличие целей у ордеров каждый тик. Соответственно на первом тике ордер открывается, а на втором уже модифицируется. Если модификация не пройдет по какой-либо ошибке на этом тике, то на следующем будет предпринята очередная попытка. Данная вариация также полезна, если вдруг пользователь вашей программы или вы по случайности удалите заданные алгоритмом цели. В этом случае советник их просто восстановит, ибо нечего лезть руками в настроенную программу.
Открытие ордера по показаниям индикатора
Переходим к извлечению показаний из индикатора МА. Нам нужно найти тот момент, когда одна линия пересечет вторую и закрепится за ней. Это можно узнать только после закрытия текущей свечи. Соответственно нам нет никакого смысла каждый тик узнавать значение скользящих средних и тормозить тем самым работу советника. Получение данных индикатора и проверка условий для входа в рынок будут происходить один раз в момент открытия новой свечи. Для этого введем переменную Update_Time для хранения времени на глобальном уровне, которая будет сравнивать время открытия текущий свечи со значением, сохраненным в ней. Как только откроется новая свеча, время станет различным и запустится условие проверки с перезаписью значения этой переменной. Прописываем следующий код после цикла проверки ордеров:
0 1 2 3 4 |
//обновлять данные всех индикаторов раз в период if(Update_Time != iTime(NULL,0,0)) { Update_Time = iTime(NULL,0,0); //... последующий код } |
Для того, чтобы определить пересечение двух линий индикаторов МА, нам нужно знать их данные в момент закрытия последней [1] свечи, а также предыдущей [2] свечи. Эти значения будут импортироваться в отдельные переменные.
Импорт данных индикатора выполняется с помощью всего одной функции. Притом для классических индикаторов в редакторе Meta Editor уже предусмотрено порядка 37 функций для удобства импорта их данных. Название функций для работы с техническими индикаторами начинаются на букву i. То есть для получения данных индикатора ATR есть функция iATR, для Cтохастика это iStochastic, ну и для скользящей средней соответственно iMA.
Значения индикатора на каждой свече графика можно посмотреть в «Окне Данных», которое открывается сочетанием клавиш Ctrl+D. Установите любой индикатор на график и если он содержит в себе буфер данных, то они отобразятся в этом окне.
Для получения данных МА на [1] свече мы создадим переменную дробного типа (поскольку значение МА хранятся в цене) и воспользуемся функцией iMA.
0 |
double MA1_1 = iMA(NULL,0,ma1_period,0,ma1_method,PRICE_CLOSE,1); |
Функция iMA содержит в себе 7 параметров. Хорошая новость в том, что вам нет необходимости их все хранить в голове. Просто прописываете ее название в коде и нажимаете клавишу F1. Появится справка, где подробно описан каждый параметр.
По порядку мы заполняем:
- Символ, по которому нужно получить данные. В нашем случае это текущий, т.е. NULL
- Период графика. Тоже текущий, поэтому ставим 0.
- Период скользящей. Он берется с внешней переменной ma1_period.
- Сдвиг МА относительно графика. Нам он ни к чему, поэтому ставим 0.
- Метод усреднения. Берется также из внешней переменной ma1_method. По умолчания я выбрал EMA, в настройках при оптимизации вы сможете это изменить.
- Используемая цена расчета индикатора. В большинстве случаем берется цена закрытия, поэтому я не стал выносить ее значение во внешние настройки и просто прописал PRICE_CLOSE.
- Сдвиг получаемых данных. Этим числом обозначается свеча, по которой нужно получить искомое значение. Ноль значит текущая, [1] — первая закрытая, [2] — вторая при подсчете справа налево и т.д.
Также, если вы находитесь в процессе заполнения параметров, но запутались на каком вы по счету, просто установите на нем каретку и нажмите сочетание клавиш Ctrl+Shift+Space. Появится подсказка с выделенным данным параметром жирным текстом.
С параметрами функции разобрались, теперь нужно объявить три недостающих функции для получения полной картины.
0 1 2 3 4 5 6 7 8 |
if(Update_Time != iTime(NULL,0,0)) { Update_Time = iTime(NULL,0,0); double MA1_1 = iMA(NULL,0,ma1_period,0,ma1_method,PRICE_CLOSE,1); double MA1_2 = iMA(NULL,0,ma1_period,0,ma1_method,PRICE_CLOSE,2); double MA2_1 = iMA(NULL,0,ma2_period,0,ma2_method,PRICE_CLOSE,1); double MA2_2 = iMA(NULL,0,ma2_period,0,ma2_method,PRICE_CLOSE,2); } |
Мы получили два значения по быстрой МАшке и два по медленной на [1] и [2] свечах. Для нашей задачи этого достаточно. Для того, чтобы открыть ордер на покупку должно быть выполнено три условия:
- Отсутствие в рынке ордеров на покупку
- Быстрая скользящая средняя (меньше периода) на первой свече выше медленной, т.е. произошло пересечение снизу вверх.
- Быстрая скользящая средняя на второй свече ниже медленной, т.е. на предыдущей свече быстрая была еще под медленной.
Для продаж зеркально. Если условие соблюдено, то активируется функция открытия рыночного ордера. Если он успешно выставлен, то происходит проверка параметра CloseOppositeOrder — разрешение закрывать ордер в противоположном направлении после открытия текущего.
0 1 2 3 4 5 6 7 8 9 10 |
if(cnt_b == 0 && MA1_1 > MA2_1 && MA1_2 <= MA2_2) { //условие для открытия ордера на покупку if(OpenTrade(OP_BUY)) { //открытие ордера на покупку if(CloseOppositeOrder) CloseTrade(OP_SELL); //закрытие противоположного ордера } } if(cnt_s == 0 && MA1_1 < MA2_1 && MA1_2 >= MA2_2) { //условие для открытия ордера на продажу if(OpenTrade(OP_SELL)) { //открытие ордера на продажу if(CloseOppositeOrder) CloseTrade(OP_BUY); //закрытие противоположного ордера } } |
В итоге вся конструкция будет иметь вид:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
if(Update_Time != iTime(NULL,0,0)) { //обновлять данные всех индикаторов раз в период Update_Time = iTime(NULL,0,0); //перезаписываем значение переменной для хранения времени текущей свечи //импорт данных индикатора Moving Averages. double MA1_1 = iMA(NULL,0,ma1_period,0,ma1_method,PRICE_CLOSE,1); double MA1_2 = iMA(NULL,0,ma1_period,0,ma1_method,PRICE_CLOSE,2); double MA2_1 = iMA(NULL,0,ma2_period,0,ma2_method,PRICE_CLOSE,1); double MA2_2 = iMA(NULL,0,ma2_period,0,ma2_method,PRICE_CLOSE,2); if(cnt_b == 0 && MA1_1 > MA2_1 && MA1_2 <= MA2_2) { //условие для открытия ордера на покупку if(OpenTrade(OP_BUY)) { //открытие ордера на покупку if(CloseOppositeOrder) CloseTrade(OP_SELL); //закрытие противоположного ордера } } if(cnt_s == 0 && MA1_1 < MA2_1 && MA1_2 >= MA2_2) { //условие для открытия ордера на продажу if(OpenTrade(OP_SELL)) { //открытие ордера на продажу if(CloseOppositeOrder) CloseTrade(OP_BUY); //закрытие противоположного ордера } } } |
В самом конце функции OnTick() осталось добавить проверку на возможные ошибки:
0 1 |
int Error = GetLastError(); //поиск ошибок по завершению тика if(Error != 0) Print("OnTick() Error ",Error,": ",ErrorDescription(Error)); |
Теперь нам остается создать две пользовательские функции вне OnTick() для открытия текущего и закрытия противоположного ордера, а именно OpenTrade() и CloseTrade(). Функцию для открытия ордера мы рассматривали на прошлом уроке, поэтому останавливаться на ней подробно нет смысла. Вот она:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
bool 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 ? "на покупку" : "на продажу"); int ticket = OrderSend(Symbol(), OP_Type, Lot, price, Slippage, 0, 0, Comments, MagicNumber, 0, col_type); if(ticket > 0) { //Если ордер был открыт Print("Ордер #" + IntegerToString(ticket) + " успешно открыт"); return(true); } else { //при ошибке открытия ордера int Error = GetLastError(); Update_Time = 0; Print("Ошибка открытия ордера ",Error,": ",ErrorDescription(Error)); } return(false); } |
Функция для поиска и закрытия противоположного ордера представляет собой простой цикл по всем рыночным ордерам. Каждый ордер выделяется и идет проверка условия: его символ должен соответствовать текущему, на который установлен советник. Магики должны совпадать друг с другом и тип ордера должен быть противоположный тому, что был только что открыт. Если все эти условия соблюдены, запускается второй цикл из трех попыток на закрытие ордера. В данном случае этот цикл нам нужно только как дополнительная мера на случай, если с первого раза у советника не получится закрыть рыночный ордер. Не получилось — советник делает паузу в 3 секунды и обновляет данные в предопределенных переменных и массивах-таймсериях, далее пробудет закрыться снова. Если же все получилось — цикл прерывается, задача выполнена.
Само закрытие происходит с помощью функции OrderClose(). Она совершенно несложная, нужно только правильно задать ей параметры.
- Тикет ордера, который мы будем закрывать. В данном случае он хранится в функции OrderTicket().
- Лот для закрытия. Ордера можно закрывать частично, но в нашем примере мы используем полный лот ордера OrderLots().
- Цена закрытия. Тут нужно знать, что ордера, открытые в покупку закрываются строго по цене Bid, а ордера в продажу по цене Ask. Если вы перепутаете эти значения, то функция может просто не сработать и выдать ошибку.
- Максимальное проскальзывание. Оно берется из внешней переменной, но по практике должно быть не менее 5 пунктов.
- Цвет стрелочки, которая появится на графике. Если хотите, можете для разных типов ордеров задать свой цвет, я не стал заморачиваться.
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
void CloseTrade(int OP_Type) { //функция для закрытия противоположного ордера. 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(OP_Type != OrderType()) continue; for(int n = 0; n<3; n++) { //три попытки закрыть ордер if(OrderClose(OrderTicket(),OrderLots(),(OP_Type == OP_BUY ? Bid : Ask),Slippage,clrAqua)) { Print("Ордер #" + IntegerToString(OrderTicket()) + " успешно закрыт по фильтру выхода из позиции"); break; } else { //если ордер не закрылся Sleep(3000); RefreshRates(); } } } } |
Проверка советника в тестере
Давайте посмотрим, как советник демонстрирует свою работу на визуализации:
Все работает, как мы планировали: желая линия периодом 50 пересекает фиолетовую снизу вверх и ордер в покупку открывается, сверху вниз — открывается в продажу. Прочитаем журнал (снизу вверх). Комментарии дадут лучше понять то, что делает советник. Мы видим, что он вначале открывает, а потом модифицирует ордер.
Также нужно посмотреть, как ведет себя функция принудительного закрытия противоположного ордера в журнале.
Все работает как задумывалось, осталось только проверить насколько этот алгоритм является прибыльным в тестере. Для этого поставим советник на быструю оптимизации с генетическим алгоритмом по Профит Фактору. Из-за небольшого количества параметров данный опт не займет много времени. Выберем лучший вариант основываясь на прибыли и максимальной просадке.
Заключение
В этом уроке мы научились использовать встроенный технический индикатор Moving Average для МТ4 и определять условия на вход в сделку, а также принудительное закрытие противоположного ордера.
Как видно по графику доходности, данный пример показал себя лучше по сравнению с предыдущим нашим советником. Но не стоит сразу же ставить этот сов на реальный счет. Причин много. Во-первых, данный тест шел четыре года, а сделок всего 118, что очень мало. Качество тестирование не подразумевало в себе плавающий спред и имитацию проскальзывание. Да и вообще вы должны понимать, что из всей выборки 211 прогонов этот просто лучше всего подстроился под историю и показал наибольший профит фактор. Огромное значение проходок просто слилось.
Также стоит заметить, что торговля по показателям всего одного технического индикатора не рекомендуется никому. Я думаю, что если к данному коду приделать дополнительные индикаторы и вспомогательные условия на выход из сделки, фильтр времени или еще какие-либо приблуды, то у него появится шанс, а пока это просто пример для написания самого простого советника.
Далее же вы можете пробовать и экспериментировать с этим кодом, менять существующие условия, добавлять дополнительные и т.д.
На этом все, надеюсь данный урок был вам полезен. Всем профитов!
[download url=»http://www.davinci-fx.com/wp-content/uploads/2021/03/3.1-Two-MA.rar» title=»Скачать примеры урока»]