Торговая система 'Turtle Soup' и её модификация 'Turtle Soup Plus One'
- Введение
- Торговая система 'Turtle Soup' и её модификация 'Turtle Soup Plus One'
- Определение параметров канала
- Функция генерации сигналов
- Базовый торговый советник для проверки ТС
- Тестирование стратегии на исторических данных
- Заключение
Введение
Авторы книги Street Smarts: High Probability Short-Term Trading Strategies Лоуренс Коннорс и Линда Рашке — пара успешных трейдеров, в активе которых в общей сложности 34 года трейдерской работы. Их богатый опыт включает торговлю на биржевых площадках, профильную работу в банках и хедж-фондах, в брокерских и консалтинговых фирмах. По их мнению, для стабильно прибыльной торговли вполне достаточно иметь в активе лишь одну торговую стратегию (ТС). Тем не менее, в книге приведено почти два десятка вариантов ТС, разбитых на четыре группы. Каждая группа относится к определённой фазе рыночных циклов и эксплуатирует один из устойчивых паттернов поведения цены.
Описанные в книге стратегии получили достаточно широкое распространение, но важно понимать, что авторы строили их исходя из поведения рынка 15..20-летней давности. Поэтому у этой статьи две цели — начнём мы с реализации на языке MQL5 первой из описанных Л.Рашке и Л.Коннорсом торговых стратегий, а затем попробуем оценить её эффективность с помощью тестера стратегий MT5. При этом будем использовать доступные через демо-сервер MetaQuotes исторические данные последних лет.
При написании кода я буду ориентироваться на пользователей MQL5, имеющих базовые знания языка — то есть, на слегка продвинутых новичков. Поэтому здесь не будет пояснений работы штатных функций, обоснований выбора типов переменных и всего того, в чём следует потренироваться до начала программирования торговых советников. С другой стороны, я не буду ориентироваться и на опытных роботостроителей — они, как правило уже имеют наработанные библиотеки собственных решений и не станут отказываться от них при реализации новой ТС.
Для большинства программистов, которым будет интересна эта статья, актуальна задача освоения объектно-ориентированного программирования. Поэтому я попробую сделать работу над этим советником полезной и для решения этой задачи. Чтобы сделать переход от процедурного подхода к объектно-ориентированному более простым, мы не будем использовать самое сложное в ООП — классы. Зато будет широко использоваться их более простой аналог — структуры. Структуры, объединяя логически связанные данные разных типов и функции для работы с ними, обладают почти всеми присущими классам признаками, включая и наследование. Но для их использования не требуется знания правил оформления кода классов, можно обойтись минимумом дополнений к тому, к чему вы уже привыкли при процедурном программировании.
Торговая система 'Turtle Soup' и её модификация 'Turtle Soup Plus One'
Торговая стратегия под названием "Черепаховый суп" (Turtle Soup) открывает набор стратегий из серии с лаконичным названием "Tests". Чтобы стало понятней, по какому признаку подобрана эта серия, озаглавить её стоило бы "Тестирование ценой границ диапазона или уровней поддержки/сопротивления". Turtle Soup строится на предположении, что цена не сможет пробить 20-дневный диапазон без отскока от границ этого диапазона. Из временного отката от границы либо из ложного пробоя нам и предстоит попытаться извлечь прибыль. Торговая позиция всегда будет направлена внутрь диапазона, а это даёт основание отнести ТС к категории "отбойных".
Кстати, схожесть названия "Turtle Soup" со знаменитой стратегией "Turtles" не случайна — обе отслеживают поведение цены у границ 20-дневного диапазона. По словам авторов книги, они некоторое время пытались использовать пару пробойных стратегий, включая "Turtles", однако обилие ложных пробоев и глубоких откатов делало такую торговлю неэффективной. Но зато выявленные паттерны помогли создать набор правил для извлечения прибыли из движения цены в направлении, противоположном пробою.
Полный набор правил ТС "Turtle Soup" для входа в сделку на покупку можно сформулировать так:
- Убедитесь, что со времени предыдущего 20-дневного минимума прошло не менее 3 торговых дней
- Дождитесь, когда цена инструмента упадёт ниже 20-дневного минимума
- Установите отложенный ордер на покупку на 5-10 пунктов выше только что пробитого вниз ценового минимума
- Сразу после срабатывания отложенного ордера установите его StopLoss на 1 пункт ниже минимума этого дня
- Используйте Трейлинг Стоп, когда позиция станет прибыльной
- Если позиция закрылась по стопу на первый или второй день, разрешён повторный вход на первоначальном уровне
Правила для входа в сделку на продажу аналогичны и применять их, как вы понимаете, надо к верхней границе диапазона — 20-дневному максимуму цен.
В Библиотеке исходных кодов есть индикатор, который при определённых настройках отображает границы канала на каждом баре истории. Его можно использовать для визуализации при ручной торговле.
В описании ТС нет прямого ответа на вопрос, как долго следует держать отложенный ордер, поэтому будем исходить из простой логики. А именно: при тестировании границы диапазона цена создаст новый экстремум, что на следующий день сделает невыполнимым первое из описанных выше условий. А поскольку сигнала в этот день нет, значит, отложенный ордер предыдущего дня мы должны отменить.
Модификация этой ТС, названная 'Turtle Soup Plus One', имеет лишь 2 отличия:
- Вместо выставления отложенного ордера сразу после пробоя 20-дневного диапазона, надо дождаться подтверждения сигнала — закрытия бара этого дня за пределами диапазона. Нас вполне устроит и ситуация, если день закроется точно на границе рассматриваемого горизонтального канала.
- Для определения уровня начального StopLoss используется соответствующий двухдневный экстремум (максимум или минимум) цены.
Определение параметров канала
Для проверки выполнения условий нам нужно знать максимальную и минимальную цены диапазона, а для их расчёта, в свою очередь, надо определить временные границы. Эти четыре переменные определяют канал на каждый конкретный момент времени, поэтому логично объединить их в одну общую структуру. Добавим в неё ещё две переменные, задействованные в ТС — количество дней (баров), прошедшее после достижения ценой максимума и минимума диапазона:
struct CHANNEL { double d_High; // цена верхней границы диапазона double d_Low; // цена нижней границы диапазона datetime t_From; // дата/время первого (самого старого) бара канала datetime t_To; // дата/время последнего бара канала int i_Highest_Offset; // кол-во баров справа от максимума цены int i_Lowest_Offset; // кол-во баров справа от минимума цены };
Все эти переменные будет своевременно обновлять функция с именем f_Set. Для этого ей нужно задать, начиная с какого бара следует строить виртуальный канал (i_Newest_Bar_Shift) и на какую глубину просматривать историю (i_Bars_Limit):
void f_Set(int i_Bars_Limit, int i_Newest_Bar_Shift = 1) { double da_Price_Array[]; // вспомогательный массив для цен High/Low всех баров канала // определение верхней границы диапазона: int i_Price_Bars = CopyHigh(_Symbol, PERIOD_CURRENT, i_Newest_Bar_Shift, i_Bars_Limit, da_Price_Array); int i_Bar = ArrayMaximum(da_Price_Array); d_High = da_Price_Array[i_Bar]; // верхняя граница диапазона определена i_Highest_Offset = i_Price_Bars - i_Bar; // возраст (в барах) максимума // определение нижней границы диапазона: i_Price_Bars = CopyLow(_Symbol, PERIOD_CURRENT, i_Newest_Bar_Shift, i_Bars_Limit, da_Price_Array); i_Bar = ArrayMinimum(da_Price_Array); d_Low = da_Price_Array[i_Bar]; // нижняя граница диапазона определена i_Lowest_Offset = i_Price_Bars - i_Bar; // возраст (в барах) минимума datetime ta_Time_Array[]; i_Price_Bars = CopyTime(_Symbol, PERIOD_CURRENT, i_Newest_Bar_Shift, i_Bars_Limit, ta_Time_Array); t_From = ta_Time_Array[0]; t_To = ta_Time_Array[i_Price_Bars - 1]; }
В этой функции всего 13 строк, но, если вы внимательно прочли справку по MQL-функциям извлечения данных из таймсерий (CopyHigh, CopyLow, CopyTime и др.), то знаете, что с ними не всё так просто. В ряде случаев функции возвращают не столько значений, сколько вы задали, потому что запрашиваемые данные могут быть не готовы при первом доступе к нужной таймсерии. Однако при правильной обработке результатов копирование данных из таймсерий работает так, как вы задумали.
Поэтому будем следовать хотя бы минимальным критериям качественного программирования и добавим в код простейшие обработчики ошибок. Чтобы проще было с ними разбираться, организуем печать в лог информации об ошибках. Логирование очень полезно и при отладке — это позволяет иметь подробную информацию о том, на основании чего робот принял то или иное решение. Поэтому введём новую переменную перечисляемого типа, которая будет определять насколько подробным должно быть логирование:
enum ENUM_LOG_LEVEL { // Список уровней логирования LOG_LEVEL_NONE, // логирование отключено LOG_LEVEL_ERR, // только информация об ошибках LOG_LEVEL_INFO, // ошибки + комментарии робота LOG_LEVEL_DEBUG // всё без исключений };
Нужный уровень будет выбирать пользователь, а соответствующие операторы вывода информации в лог мы разместим во многих функциях. Поэтому и список, и пользовательская переменная Log_Level должны быть размещены не в сигнальном блоке, а в начале основной программы.
Однако, вернёмся к функции f_Set — со всеми проверками она приобретёт такой вид (выделены добавленные строки):
void f_Set(int i_Bars_Limit, int i_Newest_Bar_Shift = 1) { double da_Price_Array[]; // вспомогательный массив для цен High/Low всех баров канала // определение верхней границы диапазона: int i_Price_Bars = CopyHigh(_Symbol, PERIOD_CURRENT, i_Newest_Bar_Shift, i_Bars_Limit, da_Price_Array); if(i_Price_Bars == WRONG_VALUE) { // обработка ошибки функции CopyHigh if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyHigh: ошибка #%u", __FUNCSIG__, _LastError); return; } if(i_Price_Bars < i_Bars_Limit) { // функция CopyHigh извлекла данные не в полном объёме if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyHigh: скопировано %u баров из %u", __FUNCSIG__, i_Price_Bars, i_Bars_Limit); return; } int i_Bar = ArrayMaximum(da_Price_Array); if(i_Bar == WRONG_VALUE) { // обработка ошибки функции ArrayMaximum if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: ArrayMaximum: ошибка #%u", __FUNCSIG__, _LastError); return; } d_High = da_Price_Array[i_Bar]; // верхняя граница диапазона определена i_Highest_Offset = i_Price_Bars - i_Bar; // возраст (в барах) максимума // определение нижней границы диапазона: i_Price_Bars = CopyLow(_Symbol, PERIOD_CURRENT, i_Newest_Bar_Shift, i_Bars_Limit, da_Price_Array); if(i_Price_Bars == WRONG_VALUE) { // обработка ошибки функции CopyLow if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyLow: ошибка #%u", __FUNCSIG__, _LastError); return; } if(i_Price_Bars < i_Bars_Limit) { // функция CopyLow извлекла данные не в полном объёме if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyLow: скопировано %u баров из %u", __FUNCSIG__, i_Price_Bars, i_Bars_Limit); return; } i_Bar = ArrayMinimum(da_Price_Array); if(i_Bar == WRONG_VALUE) { // обработка ошибки функции ArrayMinimum if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: ArrayMinimum: ошибка #%u", __FUNCSIG__, _LastError); return; } d_Low = da_Price_Array[i_Bar]; // нижняя граница диапазона определена i_Lowest_Offset = i_Price_Bars - i_Bar; // возраст (в барах) минимума datetime ta_Time_Array[]; i_Price_Bars = CopyTime(_Symbol, PERIOD_CURRENT, i_Newest_Bar_Shift, i_Bars_Limit, ta_Time_Array); if(i_Price_Bars < 1) t_From = t_To = 0; else { t_From = ta_Time_Array[0]; t_To = ta_Time_Array[i_Price_Bars - 1]; } }
При выявлении ошибки поступаем просто: прерываем выполнение с расчётом на то, что к следующему тику терминал подгрузит достаточно исторических данных для нормальной работы функций копирования. А чтобы другие пользовательские функции не использовали канал до полноценного завершения процедуры, добавим в структуру соответствующий флаг b_Ready (true = данные подготовлены, false = процесс не завершён). Заодно добавим и флаг изменения параметров канала (b_Updated) — для оптимальной работы полезно знать, не изменились ли четыре задействованных в ТС параметра. Для этого придётся ввести ещё одну переменную — сигнатуру канала (s_Signature), своего рода слепок параметров. Саму функцию f_Set тоже поместим в структуру, и она (структура CHANNEL) примет окончательный вид:
// информация о канале и функции для её сбора и обновления, собранные в структуру struct CHANNEL { // переменные double d_High; // цена верхней границы диапазона double d_Low; // цена нижней границы диапазона datetime t_From; // дата/время первого (самого старого) бара канала datetime t_To; // дата/время последнего бара канала int i_Highest_Offset; // кол-во баров справа от максимума цены int i_Lowest_Offset; // кол-во баров справа от минимума цены bool b_Ready; // процедура обновления параметров завершена? bool b_Updated; // параметры канала изменились? string s_Signature; // сигнатура последнего известного набора данных // функции: CHANNEL() { d_High = d_Low = 0; t_From = t_To = 0; b_Ready = b_Updated = false; s_Signature = "-"; i_Highest_Offset = i_Lowest_Offset = WRONG_VALUE; } void f_Set(int i_Bars_Limit, int i_Newest_Bar_Shift = 1) { b_Ready = false; // питстоп: вывешиваем флаг техобслуживания double da_Price_Array[]; // вспомогательный массив для цен High/Low всех баров канала // определение верхней границы диапазона: int i_Price_Bars = CopyHigh(_Symbol, PERIOD_CURRENT, i_Newest_Bar_Shift, i_Bars_Limit, da_Price_Array); if(i_Price_Bars == WRONG_VALUE) { // обработка ошибки функции CopyHigh if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyHigh: ошибка #%u", __FUNCSIG__, _LastError); return; } if(i_Price_Bars < i_Bars_Limit) { // функция CopyHigh извлекла данные не в полном объёме if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyHigh: скопировано %u баров из %u", __FUNCSIG__, i_Price_Bars, i_Bars_Limit); return; } int i_Bar = ArrayMaximum(da_Price_Array); if(i_Bar == WRONG_VALUE) { // обработка ошибки функции ArrayMaximum if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: ArrayMaximum: ошибка #%u", __FUNCSIG__, _LastError); return; } d_High = da_Price_Array[i_Bar]; // верхняя граница диапазона определена i_Highest_Offset = i_Price_Bars - i_Bar; // возраст (в барах) максимума // определение нижней границы диапазона: i_Price_Bars = CopyLow(_Symbol, PERIOD_CURRENT, i_Newest_Bar_Shift, i_Bars_Limit, da_Price_Array); if(i_Price_Bars == WRONG_VALUE) { // обработка ошибки функции CopyLow if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyLow: ошибка #%u", __FUNCSIG__, _LastError); return; } if(i_Price_Bars < i_Bars_Limit) { // функция CopyLow извлекла данные не в полном объёме if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyLow: скопировано %u баров из %u", __FUNCSIG__, i_Price_Bars, i_Bars_Limit); return; } i_Bar = ArrayMinimum(da_Price_Array); if(i_Bar == WRONG_VALUE) { // обработка ошибки функции ArrayMinimum if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: ArrayMinimum: ошибка #%u", __FUNCSIG__, _LastError); return; } d_Low = da_Price_Array[i_Bar]; // нижняя граница диапазона определена i_Lowest_Offset = i_Price_Bars - i_Bar; // возраст (в барах) минимума datetime ta_Time_Array[]; i_Price_Bars = CopyTime(_Symbol, PERIOD_CURRENT, i_Newest_Bar_Shift, i_Bars_Limit, ta_Time_Array); if(i_Price_Bars < 1) t_From = t_To = 0; else { t_From = ta_Time_Array[0]; t_To = ta_Time_Array[i_Price_Bars - 1]; } string s_New_Signature = StringFormat("%.5f%.5f%u%u", d_Low, d_High, t_From, t_To); if(s_Signature != s_New_Signature) { // данные канала изменились b_Updated = true; if(Log_Level > LOG_LEVEL_ERR) PrintFormat("%s: канал обновлён: %s .. %s / %s .. %s, мин: %u макс: %u ", __FUNCTION__, DoubleToString(d_Low, _Digits), DoubleToString(d_High, _Digits), TimeToString(t_From, TIME_DATE|TIME_MINUTES), TimeToString(t_To, TIME_DATE|TIME_MINUTES), i_Lowest_Offset, i_Highest_Offset); s_Signature = s_New_Signature; } b_Ready = true; // обновление данных успешно завершено } };
CHANNEL go_Channel;
Функция генерации сигналов
Сигнал на покупку по этой системе определяется всего по двум обязательным условиям:
1. После предыдущего 20-дневного минимума прошло не менее трёх торговых дней
2a. Цена инструмента упала ниже 20-дневного минимума (Turtle Soup)
2б. Дневной бар закрылся не выше 20-дневного минимума (Turtle Soup Plus One)
Все остальные перечисленные выше правила ТС относятся к параметрам торгового приказа и к сопровождению позиции, включать их в сигнальный блок не будем.
В модуле мы запрограммируем выявление сигналов по правилам обеих модификаций ТС (Turtle Soup и Turtle Soup Plus One), а в настройки советника добавим возможность выбора нужной версии правил. Соответствующую пользовательскую переменную назовём Strategy_Type. В списке стратегий пока будет всего два варианта, поэтому проще было бы обойтись выбором true/false (переменная типа bool). Но мы оставим себе возможность по окончании этого цикла статей свести все переведённые в код стратегии из книги в один советник, поэтому воспользуемся хоть и коротеньким, но нумерованным списком:
enum ENUM_STRATEGY { // Список стратегий TS_TURTLE_SOUP, // Turtle Soup TS_TURTLE_SOUP_PLUS_1 // Turtle Soup Plus One }; input ENUM_STRATEGY Strategy_Type = TS_TURTLE_SOUP; // Торговая стратегия:
Функции выявления сигнала из основной программы нужно передать тип стратегии, т.е., дать знать, следует ли ждать закрытия бара (дня) — переменная b_Wait_For_Bar_Close типа bool. Вторая необходимая переменная — длительность паузы после предыдущего экстремума i_Extremum_Bars. Функция должна вернуть статус сигнала — сложились ли условия для покупки/продажи или пока следует ждать. Соответствующий нумерованный список тоже будет размещён в основном файле эксперта:
enum ENUM_ENTRY_SIGNAL { // Список сигналов на вход ENTRY_BUY, // сигнал на покупку ENTRY_SELL, // сигнал на продажу ENTRY_NONE, // нет сигнала ENTRY_UNKNOWN // статус не определён };
Ещё одна структура, которую будут использовать и сигнальный модуль, и функции основной программы — глобальный объект go_Tick, содержащий информацию о самом свежем тике. Это стандартная структура типа MqlTick, которая будет объявлена в основном файле, а её обновление мы позже запрограммируем в теле основной программы (в функции OnTick).
MqlTick go_Tick; // информация о последнем известном тике
Теперь, наконец-то, можно переходить к самой главной функции модуля
ENUM_ENTRY_SIGNAL fe_Get_Entry_Signal( bool b_Wait_For_Bar_Close = false, int i_Extremum_Bars = 3 ) {}
Начнём с проверки выполнения условий для сигнала на продажу — прошло ли достаточное число дней (баров) со времени предыдущего максимума (первое условие) и пробила ли цена верхнюю границу диапазона (второе):
if(go_Channel.i_Highest_Offset > i_Extremum_Bars) // 1-е условие if(go_Channel.d_High < d_Actual_Price) // 2-е условие return(ENTRY_SELL); // оба условия входа на продажу выполнены
Проверка выполнения условий для сигнала на покупку выполняется аналогично:
if(go_Channel.i_Lowest_Offset > i_Extremum_Bars) // 1-е условие if(go_Channel.d_Low > d_Actual_Price) { // 2-е условие return(ENTRY_BUY); // оба условия входа на покупку выполнены
Здесь использована переменная d_Actual_Price, которая содержит актуальную для данной версии ТС текущую цену. Для Turtle Soup это последняя известная цена bid, для Turtle Soup Plus One — цена закрытия предыдущего дня (бара):
double d_Actual_Price = go_Tick.bid; // цена по умолчанию - для версии Turtle Soup if(b_Wait_For_Bar_Close) { // для версии Turtle Soup Plus One double da_Price_Array[1]; // вспомогательный массив CopyClose(_Symbol, PERIOD_CURRENT, 1, 1, da_Price_Array)); d_Actual_Price = da_Price_Array[0]; }
Функция, включающая минимум необходимого, выглядит так:
ENUM_ENTRY_SIGNAL fe_Get_Entry_Signal(bool b_Wait_For_Bar_Close = false, int i_Extremum_Bars = 3) { double d_Actual_Price = go_Tick.bid; // цена по умолчанию - для версии Turtle Soup if(b_Wait_For_Bar_Close) { // для версии Turtle Soup Plus One double da_Price_Array[1]; CopyClose(_Symbol, PERIOD_CURRENT, 1, 1, da_Price_Array)); d_Actual_Price = da_Price_Array[0]; } // верхняя граница: if(go_Channel.i_Highest_Offset > i_Extremum_Bars) // 1-е условие if(go_Channel.d_High < d_Actual_Price) { // 2-е условие // цена пробила верхнюю границу return(ENTRY_SELL); } // нижняя граница: if(go_Channel.i_Lowest_Offset > i_Extremum_Bars) // 1-е условие if(go_Channel.d_Low > d_Actual_Price) { // 2-е условие // цена пробила нижнюю границу return(ENTRY_BUY); } return(ENTRY_NONE); }
Теперь вспомним о том, что объект-канал может оказаться не подготовлен к чтению из него данных (флаг go_Channel.b_Ready = false). Значит, следует добавить проверку этого флага. В этой функции мы тоже используем одну из штатных функций копирования данных из таймсерий (CopyClose), поэтому позаботимся об обработке возможной ошибки. И не забудем об облегчающем отладку логировании значимых данных:
ENUM_ENTRY_SIGNAL fe_Get_Entry_Signal(bool b_Wait_For_Bar_Close = false, int i_Extremum_Bars = 3) { if(!go_Channel.b_Ready) { // данные канала не подготовлены к использованию if(Log_Level == LOG_LEVEL_DEBUG) PrintFormat("%s: параметры канала не подготовлены", __FUNCTION__); return(ENTRY_UNKNOWN); } double d_Actual_Price = go_Tick.bid; // цена по умолчанию - для версии Turtle Soup if(b_Wait_For_Bar_Close) { // для версии Turtle Soup Plus One double da_Price_Array[1]; if(WRONG_VALUE == CopyClose(_Symbol, PERIOD_CURRENT, 1, 1, da_Price_Array)) { // обработка ошибки функции CopyClose if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyClose: ошибка #%u", __FUNCSIG__, _LastError); return(ENTRY_NONE); } d_Actual_Price = da_Price_Array[0]; } // верхняя граница: if(go_Channel.i_Highest_Offset > i_Extremum_Bars) // 1-е условие if(go_Channel.d_High < d_Actual_Price) { // 2-е условие // цена пробила верхнюю границу if(Log_Level == LOG_LEVEL_DEBUG) PrintFormat("%s: цена (%s) пробила верхнюю границу (%s)", __FUNCTION__, DoubleToString(d_Actual_Price, _Digits), DoubleToString(go_Channel.d_High, _Digits)); return(ENTRY_SELL); } // нижняя граница: if(go_Channel.i_Lowest_Offset > i_Extremum_Bars) // 1-е условие if(go_Channel.d_Low > d_Actual_Price) { // 2-е условие // цена пробила нижнюю границу if(Log_Level == LOG_LEVEL_DEBUG) PrintFormat("%s: цена (%s) пробила нижнюю границу (%s)", __FUNCTION__, DoubleToString(d_Actual_Price, _Digits), DoubleToString(go_Channel.d_Low, _Digits)); return(ENTRY_BUY); } // если программа дошла до этой строки, значит, цена внутри диапазона, 2-е условие не выполнено return(ENTRY_NONE); }
Эта функция будет вызываться по каждому тику, а это сотни тысяч раз в сутки. Однако если первое условие (не меньше трёх дней от последнего экстремума) не выполнено, то вся эта работа после первой проверки становится бессмысленной. Правила хорошего стиля программирования требуют свести к минимуму расход ресурсов, поэтому научим функцию впадать в спячку до начала следующего бара (дня), т.е. до обновления параметров канала:
ENUM_ENTRY_SIGNAL fe_Get_Entry_Signal(bool b_Wait_For_Bar_Close = false, int i_Extremum_Bars = 3) { static datetime st_Pause_End = 0; // время следующей проверки if(st_Pause_End > go_Tick.time) return(ENTRY_NONE); st_Pause_End = 0; if(go_Channel.b_In_Process) { // данные канала не подготовлены к использованию if(Log_Level == LOG_LEVEL_DEBUG) PrintFormat("%s: параметры канала не подготовлены", __FUNCTION__); return(ENTRY_UNKNOWN); } if(go_Channel.i_Lowest_Offset < i_Extremum_Bars && go_Channel.i_Highest_Offset < i_Extremum_Bars) { // 1-е условие не выполнено if(Log_Level == LOG_LEVEL_DEBUG) PrintFormat("%s: 1-е условие не выполнено", __FUNCTION__); // поставим на паузу до обновления канала st_Pause_End = go_Tick.time + PeriodSeconds() - go_Tick.time % PeriodSeconds(); return(ENTRY_NONE); } double d_Actual_Price = go_Tick.bid; // цена по умолчанию - для версии Turtle Soup if(b_Wait_For_Bar_Close) { // для версии Turtle Soup Plus One double da_Price_Array[1]; if(WRONG_VALUE == CopyClose(_Symbol, PERIOD_CURRENT, 1, 1, da_Price_Array)) { // обработка ошибки функции CopyClose if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyClose: ошибка #%u", __FUNCSIG__, _LastError); return(ENTRY_NONE); } d_Actual_Price = da_Price_Array[0]; } // верхняя граница: if(go_Channel.i_Highest_Offset > i_Extremum_Bars) // 1е условие if(go_Channel.d_High < d_Actual_Price) { // 2е условие // цена пробила верхнюю границу if(Log_Level == LOG_LEVEL_DEBUG) PrintFormat("%s: цена (%s) пробила верхнюю границу (%s)", __FUNCTION__, DoubleToString(d_Actual_Price, _Digits), DoubleToString(go_Channel.d_High, _Digits)); return(ENTRY_SELL); } // нижняя граница: if(go_Channel.i_Lowest_Offset > i_Extremum_Bars) // 1-е условие if(go_Channel.d_Low > d_Actual_Price) { // 2-е условие // цена пробила нижнюю границу if(Log_Level == LOG_LEVEL_DEBUG) PrintFormat("%s: цена (%s) пробила нижнюю границу (%s)", __FUNCTION__, DoubleToString(d_Actual_Price, _Digits), DoubleToString(go_Channel.d_Low, _Digits)); return(ENTRY_BUY); } // если программа дошла до этой строки, значит цена внутри диапазона, 2-е условие не выполнено if(b_Wait_For_Bar_Close) // для версии Turtle Soup Plus One // поставим на паузу до закрытия текущего бара st_Pause_End = go_Tick.time + PeriodSeconds() - go_Tick.time % PeriodSeconds(); return(ENTRY_NONE); }
Это окончательный код функции. Файл сигнального модуля назовём Signal_Turtle_Soup.mqh, поместим в него код, относящийся к каналу и сигналам, а в начало файла добавим поля ввода пользовательских настроек стратегии:
enum ENUM_STRATEGY { // Вариант стратегии TS_TURTLE_SOUP, // Turtle Soup TS_TURTLE_SOUP_PLIS_1 // Turtle Soup Plus One }; // пользовательские настройки input ENUM_STRATEGY Turtle_Soup_Type = TS_TURTLE_SOUP; // Turtle Soup: Вариант стратегии input uint Turtle_Soup_Period_Length = 20; // Turtle Soup: Глубина поиска экстремумов (в барах) input uint Turtle_Soup_Extremum_Offset = 3; // Turtle Soup: Пауза после последнего экстремума (в барах) input double Turtle_Soup_Entry_Offset = 10; // Turtle Soup: Вход: Отступ от уровня экстремума (в пунктах) input double Turtle_Soup_Exit_Offset = 1; // Turtle Soup: Выход: Отступ от противоположного экстремума (в пунктах)
Этот файл надо сохранить в каталог данных терминала — сигнальным библиотекам в нём отведена папка MQL5\Include\Expert\Signal.
Базовый торговый советник для проверки ТС
Ближе к началу кода советника поместим поля пользовательских настроек, а перед ними — используемые в настройках списки перечисляемого типа enum. Поля настроек разделим на две группы — "Настройки стратегии" и "Открытие и сопровождение позиций". Настройки первой группы будут включены из сменного файла сигнальной библиотеки при компиляции. Пока мы создали один такой файл, но в последующих статьях будут формализованы и запрограммированы другие стратегии из книги и появится возможность заменять (или добавлять) сигнальные модули вместе с необходимыми им пользовательскими настройками.
Здесь же, в начале кода, подключим файл стандартной библиотеки MQL5 для совершения торговых операций:
enum ENUM_LOG_LEVEL { // Список уровней логирования LOG_LEVEL_NONE, // логирование отключено LOG_LEVEL_ERR, // только информация об ошибках LOG_LEVEL_INFO, // ошибки + комментарии робота LOG_LEVEL_DEBUG // всё без исключений }; enum ENUM_ENTRY_SIGNAL { // Список сигналов на вход ENTRY_BUY, // сигнал на покупку ENTRY_SELL, // сигнал на продажу ENTRY_NONE, // нет сигнала ENTRY_UNKNOWN // статус не определён }; #include <Trade\Trade.mqh> // класс для совершения торговых операций input string _ = "** Настройки стратегии:"; // . #include <Expert\Signal\Signal_Turtle_Soup.mqh> // сигнальный модуль input string __ = "** Открытие и сопровождение позиций:"; // . input double Trade_Volume = 0.1; // Объём сделки input uint Trail_Trigger = 100; // Трал: Дистанция включения трала (в пунктах) input uint Trail_Step = 5; // Трал: Шаг перемещения SL (в пунктах) input uint Trail_Distance = 50; // Трал: Макс. дистанция от цены до SL (в пунктах) input ENUM_LOG_LEVEL Log_Level = LOG_LEVEL_INFO; // Режим протоколирования:
Никакой специальной схемы управления рисками или управления капиталом для этой стратегии авторы не упоминают, поэтому будем использовать фиксированный размер лота для всех сделок.
Настройки трала вводятся в пунктах. С появлением пятизначных котировок появилась и некоторая путаница с этими единицами измерения, поэтому не помешает уточнить: один пункт соответствует минимальному изменению цены инструмента. Это значит, что при котировках с пятью знаками после запятой один пункт равен 0.00001, а при четырёхзначных котировках — 0.0001. Не следует путать пункты с пипсами — пипсы игнорируют реальную точность котировок, всегда переводя их в четырёхзначные. Т.е. если минимальное изменение цены инструмента (пункт) равен 0.00001, то один пипс равен 10 пунктам, а при цене пункта 0.0001 цены пипса и пункта совпадают.
При работе трала эти настройки используются на каждом тике, а пересчёт введённых пользователем пунктов в реальные цены инструмента, хоть и не отнимает существенных ресурсов процессора, но всё же производится сотни тысяч раз в сутки. Правильнее будет один раз пересчитать введённые пользователем значения при инициализации эксперта и сохранить их в глобальные переменные для последующего использования. Так же можно поступить и с переменными, которые будут задействованы при нормализации размера лота — ограничения сервера на максимальный и минимальный размер, а также шаг изменения остаются постоянными в течение всего времени работы робота, поэтому нет необходимости считывать их каждый раз заново. Объявление глобальных переменных и функция инициализации будут выглядеть так:
int gi_Try_To_Trade = 4, // кол-во попыток отправить торговый приказ gi_Connect_Wait = 2000 // пауза между попытками (в миллисекундах) ; double gd_Stop_Level, // StopLevel из настроек сервера, переведённая в цену инструмента gd_Lot_Step, gd_Lot_Min, gd_Lot_Max, // ограничения размера лота из настроек сервера gd_Entry_Offset, // вход: отступ от экстремума в ценах инструмента gd_Exit_Offset, // выход: отступ от экстремума в ценах инструмента gd_Trail_Trigger, gd_Trail_Step, gd_Trail_Distance // параметры трала, переведённые в цену инструмента ; MqlTick go_Tick; // информация о последнем известном тике int OnInit() { // Перевод настроек из пунктов в цены инструмента: double d_One_Point_Rate = pow(10, _Digits); gd_Entry_Offset = Turtle_Soup_Entry_Offset / d_One_Point_Rate; gd_Exit_Offset = Turtle_Soup_Exit_Offset / d_One_Point_Rate; gd_Trail_Trigger = Trail_Trigger / d_One_Point_Rate; gd_Trail_Step = Trail_Step / d_One_Point_Rate; gd_Trail_Distance = Trail_Distance / d_One_Point_Rate; gd_Stop_Level = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL) / d_One_Point_Rate; // Инициализация ограничений лота: gd_Lot_Min = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); gd_Lot_Max = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); gd_Lot_Step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); return(INIT_SUCCEEDED); }
Надо отметить, что в стандартной библиотеке MQL5 есть модуль трала нужного нам типа (TrailingFixedPips.mqh) и его можно было бы включить в код так же, как мы это сделали с классом для совершения торговых операций. Но он не полностью соответствует особенностям этого конкретного советника, поэтому трал мы напишем сами и вставим в тело робота в формате самостоятельной пользовательской функции:
bool fb_Trailing_Stop( // Функция перемещения SL позиции текущего инструмента double d_Trail_Trigger, // дистанция включения трала (в ценах инструмента) double d_Trail_Step, // шаг перемещения SL (в ценах инструмента) double d_Trail_Distance // мин. дистанция от цены до SL (в ценах инструмента) ) { if(!PositionSelect(_Symbol)) return(false); // позиции не существует, тралить нечего // базовое значение для расчёта нового уровня SL - текущее значение цены: double d_New_SL = PositionGetDouble(POSITION_PRICE_CURRENT); if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { // для длинной позиции if(d_New_SL - PositionGetDouble(POSITION_PRICE_OPEN) < d_Trail_Trigger) return(false); // цена ещё не прошла достаточного для включения трала расстояния if(d_New_SL - PositionGetDouble(POSITION_SL) < d_Trail_Distance + d_Trail_Step) return(false); // изменение цены меньше заданного шага перемещения SL d_New_SL -= d_Trail_Distance; // новый уровень SL } else if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { // для короткой позиции if(PositionGetDouble(POSITION_PRICE_OPEN) - d_New_SL < d_Trail_Trigger) return(false); // цена ещё не прошла достаточного для включения трала расстояния if(PositionGetDouble(POSITION_SL) > 0.0) if(PositionGetDouble(POSITION_SL) - d_New_SL < d_Trail_Distance + d_Trail_Step) return(false); // цена ещё не прошла достаточного для включения трала расстояния d_New_SL += d_Trail_Distance; // новый уровень SL } else return(false); // разрешают ли настройки сервера поставить расчётный SL на такое расстояние от текущей цены? if(!fb_Is_Acceptable_Distance(d_New_SL, PositionGetDouble(POSITION_PRICE_CURRENT))) return(false); CTrade Trade; Trade.LogLevel(LOG_LEVEL_ERRORS); // переместить SL Trade.PositionModify(_Symbol, d_New_SL, PositionGetDouble(POSITION_TP)); return(true); } bool fb_Is_Acceptable_Distance(double d_Level_To_Check, double d_Current_Price) { return( fabs(d_Current_Price - d_Level_To_Check) > fmax(gd_Stop_Level, go_Tick.ask - go_Tick.bid) ); }
Здесь проверка допустимости размещения SL на рассчитанном уровне вынесена в отдельную функцию fb_Is_Acceptable_Distance, чтобы использовать её при валидации уровня установки отложенного ордера и при установке StopLoss открытой позиции.
Теперь перейдём к основной рабочей области в коде советника, которая вызывается функцией-обработчиком события поступления нового тика OnTick. Согласно правилам стратегии, при наличии открытой позиции новых сигналов искать не следует, поэтому начнём с соответствующей проверки. Если позиция существует, у робота будет два варианта действий: либо рассчитать и установить начальный StopLoss для новой позиции, либо активировать функцию трала, которая определит, есть ли необходимость перемещать StopLoss, и произведёт соответствующую операцию. С вызовом функции трала всё просто, а для расчёта уровня StopLoss будем использовать введённый пользователем и пересчитанный из пунктов в цену инструмента отступ от экстремума gd_Exit_Offset. Сам экстремум цены определим с помощью штатных функций MQL5 CopyHigh или CopyLow. Рассчитанные таким образом уровни надо будет проверить на валидность с помощью функции fb_Is_Acceptable_Distance и содержащегося в структуре go_Tick текущего значения цены. Эти расчёты и проверки для ордеров BuyStop и SellStop в коде будут разделены:
if(PositionSelect(_Symbol)) { // есть открытая позиция if(PositionGetDouble(POSITION_SL) == 0.) { // новая позиция double d_SL = WRONG_VALUE, // уровень SL da_Price_Array[] // вспомогательный массив ; // рассчитать уровень StopLoss: if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { // для длинной позиции if(WRONG_VALUE == CopyLow(_Symbol, PERIOD_CURRENT, 0, 1 + (Turtle_Soup_Type == TS_TURTLE_SOUP_PLIS_1), da_Price_Array)) { // обработка ошибки функции CopyLow if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyLow: ошибка #%u", __FUNCTION__, _LastError); return; } d_SL = da_Price_Array[ArrayMinimum(da_Price_Array)] - gd_Exit_Offset; // достаточно ли расстояние от текущей цены? if(!fb_Is_Acceptable_Distance(d_SL, go_Tick.bid)) { if(Log_Level > LOG_LEVEL_NONE) PrintFormat("Расчётный уровень SL %s заменён на минимально допустимый %s", DoubleToString(d_SL, _Digits), DoubleToString(go_Tick.bid + fmax(gd_Stop_Level, go_Tick.ask - go_Tick.bid), _Digits)); d_SL = go_Tick.bid - fmax(gd_Stop_Level, go_Tick.ask - go_Tick.bid); } } else { // для короткой позиции if(WRONG_VALUE == CopyHigh(_Symbol, PERIOD_CURRENT, 0, 1 + (Turtle_Soup_Type == TS_TURTLE_SOUP_PLIS_1), da_Price_Array)) { // обработка ошибки функции CopyHigh if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyHigh: ошибка #%u", __FUNCTION__, _LastError); return; } d_SL = da_Price_Array[ArrayMaximum(da_Price_Array)] + gd_Exit_Offset; // достаточно ли расстояние от теккущей цены? if(!fb_Is_Acceptable_Distance(d_SL, go_Tick.ask)) { if(Log_Level > LOG_LEVEL_NONE) PrintFormat("Расчётный уровень SL %s заменён на минимально допустимый %s", DoubleToString(d_SL, _Digits), DoubleToString(go_Tick.ask - fmax(gd_Stop_Level, go_Tick.ask - go_Tick.bid), _Digits)); d_SL = go_Tick.ask + fmax(gd_Stop_Level, go_Tick.ask - go_Tick.bid); } } CTrade Trade; Trade.LogLevel(LOG_LEVEL_ERRORS); // установить SL Trade.PositionModify(_Symbol, d_SL, PositionGetDouble(POSITION_TP)); return; } // трал fb_Trailing_Stop(gd_Trail_Trigger, gd_Trail_Step, gd_Trail_Distance); return; }
Кроме уже считанных новых параметров тика, в обновлении нуждаются и параметры канала — они используются при выявлении сигнала. Но вызывать ответственную за такое обновление функцию f_Set структуры go_Channel имеет смысл только после закрытия очередного бара, всё остальное время эти параметры неизменны. У робота есть и ещё одно действие, которое тоже привязано к началу нового дня (бара) — удаление ставшего неактуальным вчерашнего отложенного ордера. Запрограммируем эти два действия:
int i_Order_Ticket = WRONG_VALUE, // тикет отложенного ордера i_Try = gi_Try_To_Trade, // кол-во попыток совершить операцию i_Pending_Type = -10 // тип существующего отложенного ордера ; static int si_Last_Tick_Bar_Num = 0; // номер бара предыдущего тика (0 = началу летоисчисления по MQL) // обработка событий, привязанных к началу нового дня (бара): if(si_Last_Tick_Bar_Num < int(floor(go_Tick.time / PeriodSeconds()))) { // здравствуй, новый день :) si_Last_Tick_Bar_Num = int(floor(go_Tick.time / PeriodSeconds())); // есть ли устаревший отложенный ордер? i_Pending_Type = fi_Get_Pending_Type(i_Order_Ticket); if(i_Pending_Type == ORDER_TYPE_SELL_STOP || i_Pending_Type == ORDER_TYPE_BUY_STOP) { // убираем устаревший ордер: if(Log_Level > LOG_LEVEL_ERR) Print("Удаление вчерашнего отложенного ордера"); CTrade o_Trade; o_Trade.LogLevel(LOG_LEVEL_ERRORS); while(i_Try-- > 0) { // попытки удалить if(o_Trade.OrderDelete(i_Order_Ticket)) { // попытка удалась i_Try = -10; // флаг удачной операции break; } // попытка не удалась Sleep(gi_Connect_Wait); // выдержим паузу перед следующей попыткой } if(i_Try == WRONG_VALUE) { // не удалось удалить отложенный ордер if(Log_Level > LOG_LEVEL_NONE) Print("Ошибка удаления отложенного ордера"); return; // подождём до следующего тика } } // обновление параметров канала: go_Channel.f_Set(Turtle_Soup_Period_Length, 1 + (Turtle_Soup_Type == TS_TURTLE_SOUP_PLIS_1)); }
Использованная здесь функция fi_Get_Pending_Type возвращает тип отложенного ордера, а по полученной ссылке на переменную i_Order_Ticket помещает в неё номер тикета. Тип ордера позже будет нужен для сверки с актуальным на этом тике направлением сигнала, а тикет используется в том случае, если ордер придётся удалять. Если же отложенного ордера нет, оба значения будут равны WRONG_VALUE. Листинг этой функции:
int fi_Get_Pending_Type( // детектор наличия отложенного ордера по текущему символу int& i_Order_Ticket // ссылка на тикет выбранного отложенного ордера ) { int i_Order = OrdersTotal(), // общее кол-во ордеров i_Order_Type = WRONG_VALUE // переменная для типа ордера ; i_Order_Ticket = WRONG_VALUE; // возвращаемое по умолчанию значение тикета if(i_Order < 1) return(i_Order_Ticket); // ордеров нет while(i_Order-- > 0) { // перебор существующих ордеров i_Order_Ticket = int(OrderGetTicket(i_Order)); // чтение тикета if(i_Order_Ticket > 0) if(StringCompare(OrderGetString(ORDER_SYMBOL), _Symbol, false) == 0) { i_Order_Type = int(OrderGetInteger(ORDER_TYPE)); // нужны только отложенные ордера: if(i_Order_Type == ORDER_TYPE_BUY_LIMIT || i_Order_Type == ORDER_TYPE_BUY_STOP || i_Order_Type == ORDER_TYPE_SELL_LIMIT || i_Order_Type == ORDER_TYPE_SELL_STOP) break; // отложенный ордер найден } i_Order_Ticket = WRONG_VALUE; // ещё не найден } return(i_Order_Type); }
Теперь всё готово к определению статуса сигнала. Если условия ТС не выполнены (сигнал примет статус ENTRY_NONE или ENTRY_UNKNOWN), работу основной программы на этом тике можно завершать:
// получить статус сигнала: ENUM_ENTRY_SIGNAL e_Signal = fe_Get_Entry_Signal(Turtle_Soup_Type == TS_TURTLE_SOUP_PLIS_1, Turtle_Soup_Extremum_Offset); if(e_Signal > 1) return; // сигнала нет
Если сигнал есть, сравним с направлением существующего отложенного ордера, если тот уже установлен:
// выясним тип отложенного ордера и его тикет, если этого ещё не сделано: if(i_Pending_Type == -10) i_Pending_Type = fi_Get_Pending_Type(i_Order_Ticket); // нужен ли новый отложенный ордер? if( (e_Signal == ENTRY_SELL && i_Pending_Type == ORDER_TYPE_SELL_STOP) || (e_Signal == ENTRY_BUY && i_Pending_Type == ORDER_TYPE_BUY_STOP) ) return; // отложенный ордер в направлении сигнала уже есть // надо ли удалять отложенный ордер? if( (e_Signal == ENTRY_SELL && i_Pending_Type == ORDER_TYPE_BUY_STOP) || (e_Signal == ENTRY_BUY && i_Pending_Type == ORDER_TYPE_SELL_STOP) ) { // направление отложенного ордера не совпадает с направлением сигнала if(Log_Level > LOG_LEVEL_ERR) Print("Направление отложенного ордера не соотвтетствует направлению сигнала"); i_Try = gi_Try_To_Trade; while(i_Try-- > 0) { // попытки удалить if(o_Trade.OrderDelete(i_Order_Ticket)) { // попытка удалась i_Try = -10; // флаг удачной операции break; } // попытка не удалась Sleep(gi_Connect_Wait); // выдержим паузу перед следующей попыткой } if(i_Try == WRONG_VALUE) { // не удалось удалить отложенный ордер if(Log_Level > LOG_LEVEL_NONE) Print("Ошибка удаления отложенного ордера"); return; // подождём до следующего тика } }
Теперь, когда сомнений в необходимости нового отложенного ордера не осталось, рассчитаем его параметры. Согласно правилам стратегии, ордер надо установить с отступом внутрь от границ канала. StopLoss должен быть размещён с противоположной стороны границы, рядом с экстремумом сегодняшней или двухдневной (в зависимости от выбранной разновидности стратегии) цены. Но вычислять положение StopLoss следует только после срабатывания отложенного ордера — код для этой операции приведён выше.
Актуальные границы канала прочтём из структуры go_Channel, а введённый пользователем и пересчитанный в цены инструмента отступ для входа содержится в переменной gd_Entry_Offset. Рассчитанный уровень надо будет проверить на валидность с помощью функции fb_Is_Acceptable_Distance и содержащегося в структуре go_Tick текущего значения цены. Эти расчёты и проверки для ордеров BuyStop и SellStop в коде будут разделены:
double d_Entry_Level = WRONG_VALUE; // уровень выставления отложенного ордера if(e_Signal == ENTRY_BUY) { // для отложенного ордера на покупку // проверим возможность выставить ордер: d_Entry_Level = go_Channel.d_Low + gd_Entry_Offset; // уровень выставления ордера if(!fb_Is_Acceptable_Distance(d_Entry_Level, go_Tick.ask)) { // расстояние от текущей цены недостаточно if(Log_Level > LOG_LEVEL_ERR) PrintFormat("Нельзя выставить BuyStop на уровень %s. Bid: %s Ask: %s StopLevel: %s", DoubleToString(d_Entry_Level, _Digits), DoubleToString(go_Tick.bid, _Digits), DoubleToString(go_Tick.ask, _Digits), DoubleToString(gd_Stop_Level, _Digits) ); return; // подождём изменения текущей цены } } else { // проверим возможность выставить ордер: d_Entry_Level = go_Channel.d_High - gd_Entry_Offset; // уровень выставления ордера if(!fb_Is_Acceptable_Distance(d_Entry_Level, go_Tick.bid)) { // расстояние от текущей цены недостаточно if(Log_Level > LOG_LEVEL_ERR) PrintFormat("Нельзя выставить SellStop на уровень %s. Bid: %s Ask: %s StopLevel: %s", DoubleToString(d_Entry_Level, _Digits), DoubleToString(go_Tick.bid, _Digits), DoubleToString(go_Tick.ask, _Digits), DoubleToString(gd_Stop_Level, _Digits) ); return; // подождём изменения текущей цены } }
Если рассчитанный уровень установки отложенного ордера прошёл проверку, можно организовывать отправку нужного приказа на сервер с помощью класса стандартной библиотеки:
// приведём лот в соответствие требованиям сервера: double d_Volume = fd_Normalize_Lot(Trade_Volume); // выставим отложенный ордер: i_Try = gi_Try_To_Trade; if(e_Signal == ENTRY_BUY) { while(i_Try-- > 0) { // попытки выставить BuyStop if(o_Trade.BuyStop( d_Volume, d_Entry_Level, _Symbol )) { // попытка удалась Alert("Выставлен отложенный ордер на покупку!"); i_Try = -10; // флаг удачной операции break; } // не удалась Sleep(gi_Connect_Wait); // выдержим паузу перед следующей попыткой } } else { while(i_Try-- > 0) { // попытки выставить SellStop if(o_Trade.SellStop( d_Volume, d_Entry_Level, _Symbol )) { // попытка удалась Alert("Выставлен отложенный ордер на продажу!"); i_Try = -10; // флаг удачной операции break; } // не удалась Sleep(gi_Connect_Wait); // выдержим паузу перед следующей попыткой } } if(i_Try == WRONG_VALUE) // не удалось установить отложенный ордер if(Log_Level > LOG_LEVEL_NONE) Print("Ошибка установки отложенного ордера");
На этом программирование советника будет завершено, и после компиляции перейдём к анализу его работы в тестере стратегий.
Тестирование стратегии на исторических данных
В своей книге Коннорс и Рашке иллюстрируют стратегию графиками более чем двадцатилетней давности, поэтому основной целью тестирования была проверка её работоспособности на более современных данных. Использовались исходные параметры и дневной таймфрейм, указанные авторами. 20 лет назад пятизначные котировки не были распространены, а тестирование проводилось именно на пятизначных котировках демо-сервера MetaQuotes, поэтому оригинальные отступы в 1 и 10 пунктов были трансформированы в 10 и 100. Параметры трала в описании стратегии не упоминаются вообще, поэтому я использовал те, которые показались наиболее адекватными дневному таймфрейму.
График результатов тестирования стратегии Turtle Soup на USDJPY за последние пять лет:
График результатов тестирования стратегии Turtle Soup Plus One с теми же параметрами и на том же участке истории того же инструмента:
График результатов тестирования на котировках золота за последние пять лет. Стратегия Turtle Soup:
Turtle Soup Plus One:
График результатов тестирования на котировках нефти (crude oil) за последние четыре года. Стратегия Turtle Soup:
Полные отчёты всех тестов есть в приложенных файлах.
Выводы предоставляю делать вам, но обязан дать необходимое пояснение. Коннорс и Рашке предостерегают от чисто механического следования правилам любой из приведённых в книге стратегий. Они считают обязательным анализ того, как именно цена подходит к границам канала и как ведёт себя после их тестирования. К сожалению, на этом они не останавливаются сколько-нибудь подробно. Что касается оптимизации — разумеется, можно попробовать адаптировать параметры к другим таймфреймам, выбрать наиболее соответствующие этой ТС инструменты и параметры.
Заключение
Мы формализовали и запрограммировали правила первой пары из описанных в книге Street Smarts: High Probability Short-Term Trading Strategies торговых стратегий — Turtle Soup и Turtle Soup Plus One. Советник и сигнальная библиотека содержат все описанные Рашке и Коннорс правила, но в них нет некоторых важных подробностей авторской торговли, которые упомянуты лишь вскользь. Несложно предположить, что следует, как минимум, учитывать гэпы и границы торговых сессий. Кроме этого, кажется логичным попробовать ограничить торговлю одним входом в день или одним прибыльным входом, держать отложенный ордер более чем до начала следующих суток. Этим вы можете и заняться, если появится желание усовершенствовать описанный здесь советник.
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Было интересно читать статью. Спасибо автору (Александру Пузанову).
Ощущения - как будто полакомилась интересной, вкусной пищей. С решением в дальнейшем вернуться к ней и снова "лакомиться-смаковать", т.е. смотреть/опробовать для себя, позднее с "большим кайфом".
Вроде как все с детства знают про черепаху Тартилу.
Несколько интересных переводов слова "черепаха" на другие языки:
датский - skildpadde
корсиканский - marina
немецкий - schildkröte
шведский - hamba
В общем, самое классное название - Суп из хамбы.
Было интересно читать статью. Спасибо автору (Александру Пузанову).
И вам спасибо за прочтение :)
Затем я прочитал ровно одну строчку первого абзаца:
И дальше не читал и не смотрел статью вообще.
В общем, самое классное название - Суп из хамбы.
'Костенурка супа' получше будет. И kura-kura sup
Пожалуйста прекратите флудить. Эта ветка обсуждения статьи, а не факультет лингвистики.
Пожалуйста прекратите флудить. Эта ветка обсуждения статьи, а не факультет лингвистики.
Какое отношение "факультет лингвистики" имеет к двуязычному названию статьи на русском языке -- я не понял. Но это такое, не критично.
Собственно, моё обращение со словами "пожалуйста" было исключительно к Рошу и касалась практики беспереводного необоснованного двуязычия в русских статьях. И этот вопрос впервые был поднят ещё в обсуждении к этой статье https://www.mql5.com/ru/articles/1297 почти два года назад.
То что беспереводное необоснованное двуязычие в статьях делает проблемным прочтение и понимание публикуемых статей -- это совсем не трудно понять.
Если такие сообщения -- не относятся к обсуждаемым статьям и считаются флудом -- то без проблем, обсуждать статью более не мешаю.