Первый этап создания торгового робота
Данная статья является продолжением начатого в 61 номере журнала ForTrader.org цикла «Создание торгового робота: от А до Я». Предполагается, что на данном этапе читатель уже знаком с торговой системой, которую мы преобразовываем в робота, поэтому в рамках данной статьи мы будем рассматривать начальную фазу написания эксперта. Данная версия робота будет несколько упрощенной, чтобы облегчить понимания исходного кода.
Логические модули робота
Для реализации торгового робота предварительно следует определить, из каких логических модулей он должен состоять.
Модуль определения торговых сигналов
Задача данного модуля – поиск торгового инструмента, по которому в данный момент времени существует сигнал. Модуль будет определять сигнал для каждого конкретного торгового инструмента. Таким образом, для определения общей тенденции по группе инструментов будет нужно воспользоваться данным модулем столько раз, сколько инструментов предусмотрено в установках робота.
С точки зрения программы это может выглядеть как заполнение двумерного массива, где нулевым индексом второго измерения будет название торгового инструмента, а первым – сигнал по инструменту. То есть для поиска общего сигнала (общего направления) в любой момент времени нужно будет лишь сделать обход массива, не выполняя определения сигналов по инструментам заново. Если обратиться к описанию предложенной торговой системы, то можно вспомнить, что также для каждого конкретного инструмента будет определяться торговый лот. В связи с этим есть смысл использовать во втором измерении массива второй индекс, где будет указываться лот данного торгового инструмента. В данной реализации робота этого модуля не будет (для упрощения восприятия кода), поэтому второй индекс второго измерения массива будет заполняться одним и тем же числом – рабочим лотом из настроек робота.
Вернемся к идее модуля определения сигнала по конкретному инструменту. Модуль будет представлять собой отдельную подпрограмму (функцию). Сам код модуля будет таким:
//+——————————————————————————————————————-+
//| Функция поиска торговых сигналов |
int find_signal(string Symb) {
if(iOpen(Symb,0,0)>iMA(Symb,0,MA_Period,MA_Shift,MA_Method,MA_Applied_Price,0))
/* если цена открытия по символу Symb на данном таймфрейме выше, чем средняя скользящая — сигнал повышения*/
return(1);
if(iOpen(Symb,0,0)<iMA(Symb,0,MA_Period,MA_Shift,MA_Method,MA_Applied_Price,0))
/* если цена открытия по символу Symb на данном таймфрейме ниже, чем средняя скользящая — сигнал понижения*/
return(-1);
/* если нет никакого сигнала, то возвращаем 0 */
return(0);
}
//| Функция поиска торговых сигналов |
//+——————————————————————————————————————-+
При этом параметры скользящей средней вынесены во входные параметры робота (наверху кода робота, далее будет видно):
extern int MA_Period = 8; //период усреднения скользящей средней
extern int MA_Shift = 0; //сдвиг скользящей средней
extern int MA_Method = 1; /* метод вычисления скользящего среднего:
0 — простое скользящее среднее,
1 — экспоненциальное скользящее среднее,
2 — сглаженное скользящее среднее,
3 — линейно-взвешенное скользящее среднее
*/
extern int MA_Applied_Price = 1; /* используемая цена для расчёта скользящей средней:
0 — цена закрытия,
1 — цена открытия,
2 — максимальная цена,
3 — минимальная цена,
4 — средняя цена, (high+low)/2,
5 — типичная цена, (high+low+close)/3,
6 — взвешенная цена закрытия, (high+low+close+close)/4
*/
Таким образом, при вызове функции find_signal (string Symb) будет получен сигнал по символу Symb: 1 – рост, -1 – падение и 0 – отсутствие сигнала.
1. Модуль проверки наличия открытого ордера
Необходимо понимать, есть ли открытый ордер данного робота на данном инструменте, или нет. Если сделки нет, то робот будет искать возможность ее открыть. Если она есть, будет сопровождать и искать возможность ее закрыть.
Перейдя к теме закрытия сделки (забегая вперед), следует обратить внимание, что закрытие сделки «по рынку» возможно в случае, когда общий сигнал изменится в противоположную сторону (относительно направления открытого ордера). Поскольку в торговом роботе будут использоваться инструменты и вида ***USD, и вида USD*** (например, GBPUSD и USDCHF), то следует ввести какие-то метки – как именно роботу оценивать тот или иной инструмент. В терминологии нашей команды (в силу опыта создания торговых роботов, оценивающих группы инструментов) данную метку принято называть «коэффициент инструмента», и для инструментов вида ***<BASE> он равен 1, а для <BASE>*** -1 (где <BASE> в нашем случае – USD). Т.е. в примере у GBPUSD он будет равен 1, а у USDCHF равен -1. Эти коэффициенты выносятся во входные параметры, а в коде робота добавляются в массив инструментов как ещё один индекс второго измерения. Таким образом, в нашем целевом двумерном массиве будет храниться и метка того, «куда смотрит» данный инструмент относительно всей группы.
Вернемся к данному модулю. Он также будет реализован в виде отдельной подпрограммы (функции). Код достаточно прост:
//+——————————————————————————————————————-+
//| Функция поиска открытого ордера |
int find_orders(int type=-1, int magic=-1, string Symb=NULL) {
int i = 0; //счётчик для цикла
for (i=OrdersTotal()-1; i>=0; i—) {
if (!OrderSelect(i,SELECT_BY_POS,MODE_TRADES)) continue;
//если ордер не выбран — переходим к следующему шагу цикла
if(((OrderType()==type) || (type==-1))
&& ((OrderMagicNumber()==magic) || (magic==-1))
&& (OrderSymbol()==Symb)) {
/* ЕСЛИ (((у ордера тип==type) ИЛИ (type==-1))
И ((магическое число ордера==magic) ИЛИ (magic==-1))
И (символ ордера==Symb))
ТО возвращаем тикет ордера */
return(OrderTicket());
break; //выходим из цикла
}
}
//если дошли до этого места — значит ордер не найден — возвращаем -1
return(-1);
}
//| Функция поиска открытого ордера |
//+——————————————————————————————————————-+
В случае обнаружения ордера функция вернет его тикет, иначе значение «-1»
2. Модуль открытия сделок
Торговый робот должен открывать сделки. Для их открытия следует использовать отдельный модуль (подпрограмму, функцию). Часто разработчики ограничиваются стандартной функцией открытия ордеров, что не всегда является удобным. Так, при применении отдельной подпрограммы можно изначально заложить обработчик ошибочных ситуаций, совершать расчеты рабочих уровней СтопЛосс и ТейкПрофит и т.п.
Код данного модуля включает в себя не только саму функцию открытия ордеров, но также и расчет уровней СтопЛосс и ТейкПрофит, а также функцию, которая используется для расшифровки кодов возможных ошибок (что часто бывает удобно – видеть в журнале пояснения ошибок):
//+——————————————————————————————————————-+
//| Функция открытия ордеров |
void open_positions(int signal, double lot, int magic, double price=0.0, string symb=»NONE») {
//signal=0 -> сигнал на открытие покупки
//signal=1 -> сигнал на открытие продажи
int i = 0; //переменная для счётчика цикла
int count = Count_Of_Trade_Try; //количество попыток открытия ордера в случае, если его не удаётся совершить сразу
int err=0;
if(symb==»NONE») symb=Symbol();
if(signal==0)
price=MarketInfo(symb,MODE_ASK); //цена открытия для покупок
if(signal==1)
price=MarketInfo(symb,MODE_BID); //цена открытия для продаж
while(i<=count) {
//сама функия открытия ордера (встроенная). Для удобства восприятия параметры разнесены на разные строки:
int ticket = OrderSend(Symbol(), //символ
signal, //тип ордера
lot, //объем
price, //цена открытия
Slipage, //уровень допустимого реквота
sl(SL,signal,price), //величина Stop Loss
tp(TP,signal, price), //величина Take Profit
Order_Comment, //комментарий ордера
magic, //магическое число
0, //срок истечения (используется при отложенных ордерах)
CLR_NONE); //цвет отображаемой стрелки на графике (CLR_NONE — стрелка не рисуется)
if(ticket!=-1) //если открытие произошло успешно, наносим графический объект и выходим из цикла
break;
err=GetLastError();
if(err!=0) Print(«Ошибка: «+Market_Err_To_Str(err));
i++;
Sleep(Pause_Of_Trade_Try*100); //в случае ошибки делаем паузу перед новой попыткой
} //end while(i<=count)
} //end void open_positions(int signal, double lot, int magic, double price=0.0, string symb=»NONE»)
//| Функция открытия ордеров |
//+——————————————————————————————————————-+
//+——————————————————————————————————————-+
//| Функция расчета величины Stop Loss для ордеров |
double sl(int sl_value, int type, string symb=»NONE», int rmode=1) {
//type=0 -> рыночные покупки
//type=1 -> рыночные продажи
if(symb==»NONE») symb=Symbol();
if(sl_value<=0) return(0);
if(rmode==1) {
if(type==0) return(MarketInfo(symb,MODE_ASK)-sl_value*MarketInfo(symb,MODE_POINT)); //для покупок
if(type==1) return(MarketInfo(symb,MODE_BID)+sl_value*MarketInfo(symb,MODE_POINT)); //для продаж
}
if(rmode==2) {
if(type==0) return(MarketInfo(symb,MODE_BID)-sl_value*MarketInfo(symb,MODE_POINT)); //для покупок
if(type==1) return(MarketInfo(symb,MODE_ASK)+sl_value*MarketInfo(symb,MODE_POINT)); //для продаж
}
} //end double sl(int sl_value, int type, double price=0.0, string symb=»NONE», int rmode=1)
//| функция расчета величины Stop Loss для ордеров |
//+——————————————————————————————————————-+
//+——————————————————————————————————————-+
//| Функция расчета величины Take Profit для ордеров |
double tp(int tp_value, int type, string symb=»NONE») {
//type=0 -> рыночные покупки
//type=1 -> рыночные продажи
if(symb==»NONE») symb=Symbol();
if(tp_value<=0) return(0);
if(type==0) return(MarketInfo(symb,MODE_ASK)+tp_value*MarketInfo(symb,MODE_POINT)); //для покупок
if(type==1) return(MarketInfo(symb,MODE_BID)-tp_value*MarketInfo(symb,MODE_POINT)); //для продаж
} //end double tp(int tp_value, int type, double price=0.0, string symb=»NONE»)
//| функция расчета величины Take Profit для ордеров |
//+——————————————————————————————————————-+
//+——————————————————————————————————-+
//| Функция расшифровки кодов ошибок |
string Market_Err_To_Str(int err) {
/* функция охватывает только коды ошибок торговых операций */
switch(err) {
case(0): return(«Нет ошибки»);
case(1): return(«Нет ошибки, но результат неизвестен»);
case(2): return(«Общая ошибка»);
case(3): return(«Неправильные параметры»);
case(4): return(«Торговый сервер занят»);
case(5): return(«Старая версия клиентского терминала»);
case(6): return(«Нет связи с торговым сервером»);
case(7): return(«Недостаточно прав»);
case(8): return(«Слишком частые запросы»);
case(9): return(«Недопустимая операция, нарушающая функционирование сервера»);
case(64): return(«Счет заблокирован»);
case(65): return(«Неправильный номер счета»);
case(128): return(«Истек срок ожидания совершения сделки»);
case(129): return(«Неправильная цена»);
case(130): return(«Неправильные стопы»);
case(131): return(«Неправильный объём»);
case(132): return(«Рынок закрыт»);
case(133): return(«Торговля запрещена»);
case(134): return(«Недостаточно денег для совершения операции»);
case(135): return(«Цена изменилась»);
case(136): return(«Нет цен»);
case(137): return(«Брокер занят»);
case(138): return(«Новые цены»);
case(139): return(«Ордер заблокирован и уже обрабатывается»);
case(140): return(«Разрешена только покупка»);
case(141): return(«Слишком много запросов»);
case(145): return(«Модификация запрещена, т.к. ордер слишком близок к рынку»);
case(146): return(«Подсистема торговли занята»);
case(147): return(«Использование даты истечения запрещено брокером»);
case(148): return(«Количество открытых и отложенных ордеров достигло предела, установленного брокером»);
case(149): return(«Попытка открыть противоположную позицию к уже существующей в случае, если хеджирование запрещено»);
case(150): return(«Попытка закрыть позицию по инструменту в противоречии с правилом FIFO»);
default: return(«»);
} //end switch(err)
} //end string Err_To_Str(int err)
//| Функция расшифровки кодов ошибок |
//+——————————————————————————————————-+
3. Модуль закрытия ордера
Робот должен закрывать сделку в случае, если общий тренд сменится на противоположный. Поскольку функция поиска открытого ордера возвращает тикет ордера, то в рамках этого модуля будет разумно закрытие ордера прямо по его тикету. Код подобного модуля также весьма прост и будет вполне понятен даже для новичков в MQL4:
//+——————————————————————————————————————-+
//| Функция закрытия ордера по её номеру (тикету) |
bool close_by_ticket(int c_ticket, int slipage) {
/*
функция закрытия сделки по её номеру (тикету).
При закрытии рыночного ордера учитывается уровень максимального допустимого проскальзывания (slipage)
*/
int i = 0, //переменная для счетчика цикла
err = 0;
bool ticket = false; //перменная для обозначения (не)успешности факта закрытия сделки
double price = 0.0; //цена для закрываемой сделки (для рыночных ордеров)
if(OrderSelect(c_ticket,SELECT_BY_TICKET,MODE_TRADES)) { //выбираем ордер по тикету
if(OrderType()==OP_BUY) price = Bid; //цена для покупок
if(OrderType()==OP_SELL) price = Ask; //цена для продаж
for(i=0;i<=Count_Of_Trade_Try;i++) {
if(OrderType()<=1) //если рыночный ордер — закрываем его, если отложенный — удаляем
ticket=OrderClose(OrderTicket(),OrderLots(),price,slipage,CLR_NONE); //закрытие рыночного ордера
else
ticket=OrderDelete(OrderTicket()); //удаление отложенного ордера
if(ticket) { //если закрытие или удаление прошло успешно — возвращаем true и выходим из цикла
return(true);
break;
} //end if(ticket)
err=GetLastError();
if(err!=0) Print(«Ошибка: «+Market_Err_To_Str(err));
Sleep(Pause_Of_Trade_Try*100); //в случае ошибки делаем паузу перед новой попыткой
} //end for(i=0;i<=Count_Of_Trade_Try;i++)
} //end if(OrderSelect(c_ticket,SELECT_BY_TICKET,MODE_TRADES))
return(false); //возвращаем false (в случае, если закрыть или удалить ордер так и не удалось)
} //end bool close_by_ticket(int c_ticket)
//| Функция закрытия ордера по её номеру (тикету) |
//+——————————————————————————————————————-+
4. Конечный код робота
Для простоты восприятия мы не стали добавлять в работу данной версии робота модуль управления капиталом (см. предыдущую статью), он появится в следующей статье. Также в данную версию робота не включена функция трейлинг СтопЛосса, которая тоже будет включена в код следующей статьи.
Настало время собрать получившиеся модули в единое целое и дописать недостающие связи между ними. Это все будет сделано в предопределённой функции языка start().
Сам код представлен ниже, он снабжен комментариями и особых трудностей в его понимании быть не должно.
Исходный код получившегося эксперта с подробным описанием и недостающими функциональными частями вы можете скачать и проанализировать тут.
5. Тестирование робота
Робот будет протестирован на H4 тамфрейме. Используются параметры, которые вписаны по умолчанию. В случае внесения изменений, об этом будет сообщаться отдельно.