English 中文 Español Deutsch 日本語 Português
Торговая система 'Turtle Soup' и её модификация 'Turtle Soup Plus One'

Торговая система 'Turtle Soup' и её модификация 'Turtle Soup Plus One'

MetaTrader 5Примеры | 7 октября 2016, 13:00
7 486 14
Alexander Puzanov
Alexander Puzanov
  1. Введение
  2. Торговая система 'Turtle Soup' и её модификация 'Turtle Soup Plus One'
  3. Определение параметров канала
  4. Функция генерации сигналов
  5. Базовый торговый советник для проверки ТС
  6. Тестирование стратегии на исторических данных
  7. Заключение

Введение

Авторы книги 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" для входа в сделку на покупку можно сформулировать так:

  1. Убедитесь, что со времени предыдущего 20-дневного минимума прошло не менее 3 торговых дней
  2. Дождитесь, когда цена инструмента упадёт ниже 20-дневного минимума
  3. Установите отложенный ордер на покупку на 5-10 пунктов выше только что пробитого вниз ценового минимума
  4. Сразу после срабатывания отложенного ордера установите его StopLoss на 1 пункт ниже минимума этого дня
  5. Используйте Трейлинг Стоп, когда позиция станет прибыльной
  6. Если позиция закрылась по стопу на первый или второй день, разрешён повторный вход на первоначальном уровне 

Правила для входа в сделку на продажу аналогичны и применять их, как вы понимаете, надо к верхней границе диапазона — 20-дневному максимуму цен.

В Библиотеке исходных кодов есть индикатор, который при определённых настройках отображает границы канала на каждом баре истории. Его можно использовать для визуализации при ручной торговле.

 

В описании ТС нет прямого ответа на вопрос, как долго следует держать отложенный ордер, поэтому будем исходить из простой логики. А именно: при тестировании границы диапазона цена создаст новый экстремум, что на следующий день сделает невыполнимым первое из описанных выше условий. А поскольку сигнала в этот день нет, значит, отложенный ордер предыдущего дня мы должны отменить.

Модификация этой ТС, названная 'Turtle Soup Plus One', имеет лишь 2 отличия:

  1. Вместо выставления отложенного ордера сразу после пробоя 20-дневного диапазона, надо дождаться подтверждения сигнала — закрытия бара этого дня за пределами диапазона. Нас вполне устроит и ситуация, если день закроется точно на границе рассматриваемого горизонтального канала.
  2. Для определения уровня начального 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, USDJPY, D1, 5 лет


График результатов тестирования стратегии Turtle Soup Plus One с теми же параметрами и на том же участке истории того же инструмента:

Turtle Soup Plus One, USDJPY, D1, 5 лет


График результатов тестирования на котировках золота за последние пять лет. Стратегия Turtle Soup:

Turtle Soup, XAUUSD, D1, 5 лет


Turtle Soup Plus One:

Turtle Soup Plus One, XAUUSD, D1, 5 лет

 


График результатов тестирования на котировках нефти (crude oil) за последние четыре года. Стратегия Turtle Soup:

Turtle Soup, OIL, D1, 4 года


Turtle Soup Plus One:

Turtle Soup Plus One, OIL, D1, 4 года


Полные отчёты всех тестов есть в приложенных файлах.

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

Заключение

Мы формализовали и запрограммировали правила первой пары из описанных в книге Street Smarts: High Probability Short-Term Trading Strategies торговых стратегий — Turtle Soup и Turtle Soup Plus One. Советник и сигнальная библиотека содержат все описанные Рашке и Коннорс правила, но в них нет некоторых важных подробностей авторской торговли, которые упомянуты лишь вскользь. Несложно предположить, что следует, как минимум, учитывать гэпы и границы торговых сессий. Кроме этого, кажется логичным попробовать ограничить торговлю одним входом в день или одним прибыльным входом, держать отложенный ордер более чем до начала следующих суток. Этим вы можете и заняться, если появится желание усовершенствовать описанный здесь советник.

Прикрепленные файлы |
Reports.zip (607.7 KB)
MQL5.zip (83.57 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (14)
Dina Paches
Dina Paches | 11 окт. 2016 в 16:12

Было интересно читать статью. Спасибо автору (Александру Пузанову).  

Ощущения - как будто полакомилась интересной, вкусной пищей. С решением в дальнейшем вернуться к ней и снова "лакомиться-смаковать", т.е. смотреть/опробовать для себя, позднее с "большим кайфом".

Dmitry Fedoseev
Dmitry Fedoseev | 11 окт. 2016 в 16:19

Вроде как все с детства знают про черепаху Тартилу.

Несколько интересных переводов слова "черепаха" на другие языки:

датский - skildpadde 

корсиканский - marina

немецкий - schildkröte

шведский - hamba

В общем, самое классное название - Суп из хамбы. 

Alexander Puzanov
Alexander Puzanov | 11 окт. 2016 в 16:57
Dina Paches:

Было интересно читать статью. Спасибо автору (Александру Пузанову).

И вам спасибо за прочтение :)

 

Andrey F. Zelinsky:

Затем я прочитал ровно одну строчку первого абзаца:

И дальше не читал и не смотрел статью вообще.

Да я же разве сомневаюсь? Где ваши комменты запомнил оно везде одинаково - всегда не в теме, но всех лечит своим мнением про буквы, про черепашек или ещё чё сверхактуальное в этой теме


Dmitry Fedoseev:

В общем, самое классное название - Суп из хамбы. 

'Костенурка супа' получше будет. И kura-kura sup

Vladimir Karputov
Vladimir Karputov | 11 окт. 2016 в 17:31
Andrey F. Zelinsky:



Пожалуйста прекратите флудить. Эта ветка обсуждения статьи, а не факультет лингвистики.

Andrey F. Zelinsky
Andrey F. Zelinsky | 11 окт. 2016 в 18:10
Karputov Vladimir:

Пожалуйста прекратите флудить. Эта ветка обсуждения статьи, а не факультет лингвистики.

Какое отношение "факультет лингвистики" имеет к двуязычному названию статьи на русском языке -- я не понял. Но это такое, не критично.

Собственно, моё обращение со словами "пожалуйста" было исключительно к Рошу и касалась практики беспереводного необоснованного двуязычия в русских статьях. И этот вопрос впервые был поднят ещё в обсуждении к этой статье https://www.mql5.com/ru/articles/1297 почти два года назад.

То что беспереводное необоснованное двуязычие в статьях делает проблемным прочтение и понимание публикуемых статей -- это совсем не трудно понять.

Если такие сообщения -- не относятся к обсуждаемым статьям и считаются флудом -- то без проблем, обсуждать статью более не мешаю.

Основы программирования на MQL5: Глобальные переменные терминала MetaTrader 5 Основы программирования на MQL5: Глобальные переменные терминала MetaTrader 5
Глобальные переменные терминала — незаменимое средство при разработке сложных и надежных экспертов. Освоив работу с глобальными переменными терминала, вы уже не сможете представить себе создание экспертов на MQL5 без их использования.
Статистические распределения в MQL5 - берем лучшее из R и делаем быстрее Статистические распределения в MQL5 - берем лучшее из R и делаем быстрее
Рассмотрены функции для работы с основными статистическими распределениями, реализованными в языке R. Это распределения Коши, Вейбулла, нормальное, логнормальное, логистическое, экспоненциальное, равномерное, гамма-распределение, центральное и нецентральные распределения Бета, хи-квадрат, F-распределения Фишера, t-распределения Стьюдента, а также дискретные биномиальное и отрицательное биномиальные распределения, геометрическое, гипергеометрическое и распределение Пуассона. Есть функции расчета теоретических моментов распределений, которые позволяют оценить степень соответствия реального распределения модельному.
Графические интерфейсы X: Элемент "Стандартный график" (build 4) Графические интерфейсы X: Элемент "Стандартный график" (build 4)
На этот раз мы рассмотрим такой элемент графического интерфейса, как Стандартный график. С его помощью можно будет создавать массивы объектов-графиков с возможностью синхронизированной горизонтальной прокрутки. Кроме этого, продолжим оптимизировать код библиотеки для уменьшения потребления ресурсов процессора.
Нейросеть: Самооптимизирующийся советник Нейросеть: Самооптимизирующийся советник
Возможно ли создать советник, который согласно командам кода автоматически оптимизировал бы критерии открытия и закрытия позиций с определенной периодичностью? Что произойдет, если реализовать в советнике нейросеть (многослойный персептрон), которая, будучи модулем, анализировала бы историю и оценивала стратегию? Можно дать коду команду на ежемесячную (еженедельную, ежедневную или ежечасную) оптимизацию нейросети с последующим продолжением работы. Таким образом возможно создать самооптимизирующийся советник.