English 中文 Español Deutsch 日本語 Português
Торговая стратегия 'Momentum Pinball'

Торговая стратегия 'Momentum Pinball'

MetaTrader 5Примеры | 17 ноября 2017, 10:03
7 536 2
Alexander Puzanov
Alexander Puzanov

Введение

В этой статье продолжим программирование торговых стратегий, описанных в разделе книги Л.Рашке и Л.Коннорса Street Smarts: High Probability Short-Term Trading Strategies, посвященном тестированию ценой границ диапазона. Последняя из полноценных ТС в разделе это 'Momentum Pinball', эксплуатирующая паттерн, состоящий из двух дневных баров. По первому бару определяется направление торговли на второй день, а движение цены в начале второго бара должно указать конкретные торговые уровни для входов и выходов в рынок.

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

Как и в предыдущей статье, сначала мы создадим модуль сигнального блока, затем — индикатор для ручной торговли и разметки истории, использующий этот модуль. Третьей программой станет советник для автоматической торговли, который тоже будет использовать сигнальный модуль. В заключение протестируем советник на свежих котировках, потому что авторы книги работали с котировками 20-летней давности.


Правила ТС 'Momentum Pinball'

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

Чтобы точнее определять направление торговли следующего дня, авторы воспользовались индикатором ROC (Rate Of Change — Индекс изменения цены). К его показаниям был применён осциллятор RSI (Relative Strenght Index — Индекс относительной силы) и стала хорошо видна цикличность показаний ROC. В завершение авторы ТС добавили сигнальные уровни — границы областей перекупленности и перепроданности на графике RSI. Нахождение линии такого индикатора (его назвали LBR/RSI, от Linda Bradford Raschke) в соответствующей зоне и призвано выявлять наиболее вероятные дни продаж и дни покупок. Ниже мы рассмотрим LBR/RSI подробнее.

Полные правила ТС Momentum Pinball для входов на покупку сформулированы так.

  1. На таймфрейме D1 значение индикатора LBR/RSI последнего закрывшегося дня должен находиться в зоне перепроданности — ниже 30.
  2. После закрытия первого часового бара нового дня установите отложенный ордер на покупку выше максимума этого бара.
  3. После срабатывания отложенного ордера установите Stop Loss позиции на минимум первого часового бара.
  4. Если позиция будет закрыта с убытком, повторно установите отложенный ордер на продажу на прежнем уровне.
  5. Если к концу дня позиция останется прибыльной, оставьте её на следующий день. На второй торговый день позиция обязательно должна быть закрыта.

Визуализация правил входа с помощью двух индикаторов, описанных ниже, будет выглядеть так:

— LBR/RSI на дневном таймфрейме находится в зоне перепроданности (см. 30 октября 2017)


— индикатор TS_Momentum_Pinball на произвольном таймфрейме (от M1 до D1) отображает торговые уровни и диапазон цен первого часа дня, на основе которых рассчитаны эти уровни:


Правила выхода из рынка в книге не определены явно: авторы говорят и об использовании трейлинга, и о закрытии на следующее утро, и о выходе выше максимума первого дня торговли.

Правила входов на продажу аналогичны — показатель LBR/RSI должен быть в зоне перекупленности (выше 70), отложенный ордер следует устанавливать у минимума первого часового бара.



Индикатор LBR/RSI

Конечно, все необходимые для получения сигнала расчёты можно производить в самом сигнальном модуле, но, кроме автоматической торговли, планом этой статьи предусмотрена и ручная. Для удобства визуальной идентификации паттерна ручной версии будет полезно иметь самостоятельный индикатор LBR/RSI с подсветкой зон перекупленности/перепроданности. А чтобы оптимизировать наши усилия, не станем программировать две раздельные версии расчёта LBR/RSI ('буферную' для индикатора и 'безбуферную' для робота). Воспользуемся возможностью подключить внешний индикатор к сигнальному модулю через штатную функцию iCustom. Этот индикатор не будет производить ресурсоёмких расчётов и его не нужно опрашивать на каждом тике — в ТС используется значение индикатора на закрывшемся дневном баре, постоянно меняющееся текущее значение нас не интересует. Поэтому никаких существенных препятствий для такого решения нет.

В этом индикаторе объединим алгоритмы вычисления ROC и RSI, которые будут рисовать результирующую кривую осциллятора, а для удобства выявления нужных значений добавим заливку зон перекупленности и перепроданности разными цветами. Для всего этого потребуются пять буферов для отображения и ещё четыре — для вспомогательных расчётов.

К стандартным настройкам (период RSI и значения границ двух зон) добавим ещё одну, не предусмотренную оригинальными правилами торговой системы. Она даст возможность использовать для расчётов не только цену закрытия дневного бара, но и более информативную медианную, типичную или средневзвешенную цену. Собственно, пользователь сможет выбрать для своих экспериментов любой из семи вариантов, предусмотренных типом ENUM_APPLIED_PRICE.

Объявление буферов, пользовательских полей ввода и блок инициализации будет выглядеть так:

#property indicator_separate_window
#property indicator_buffers  9
#property indicator_plots    3

#property indicator_label1  "Зона перекупленности"
#property indicator_type1   DRAW_FILLING
#property indicator_color1  C'255,208,234'
#property indicator_width1  1

#property indicator_label2  "Зона перепроданности"
#property indicator_type2   DRAW_FILLING
#property indicator_color2  C'179,217,255'
#property indicator_width2  1

#property indicator_label3  "RSI от ROC"
#property indicator_type3   DRAW_LINE
#property indicator_style3  STYLE_SOLID
#property indicator_color3  clrTeal
#property indicator_width3  2

#property indicator_minimum 0
#property indicator_maximum 100



input ENUM_APPLIED_PRICE  TS_MomPin_Applied_Price = PRICE_CLOSE;  // Цены для расчёта ROC
input uint    TS_MomPin_RSI_Period = 3;                           // Период RSI
input double  TS_MomPin_RSI_Overbought = 70;                      // Уровень перепроданности по RSI
input double  TS_MomPin_RSI_Oversold = 30;                        // Уровень перекупленности по RSI



double
  buff_Overbought_High[], buff_Overbought_Low[],                  // фон зоны перекупленности
  buff_Oversold_High[], buff_Oversold_Low[],                      // фон зоны перепроданности
  buff_Price[],                                                   // массив расчётных цен баров
  buff_ROC[],                                                     // массив ROC от рассчитанных цен
  buff_RSI[],                                                     // RSI от ROC
  buff_Positive[], buff_Negative[]                                // вспомогательные массивы для расчёта RSI
;



int OnInit() {
  // назначение буферов индикатора:
  
  // зона перекупленности
  SetIndexBuffer(0, buff_Overbought_High, INDICATOR_DATA);
    PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, EMPTY_VALUE);
    PlotIndexSetInteger(0, PLOT_SHOW_DATA, false);
  SetIndexBuffer(1, buff_Overbought_Low, INDICATOR_DATA);
  
  // зона перепроданности
  SetIndexBuffer(2, buff_Oversold_High, INDICATOR_DATA);
    PlotIndexSetDouble(1, PLOT_EMPTY_VALUE, EMPTY_VALUE);
    PlotIndexSetInteger(1, PLOT_SHOW_DATA, false);
  SetIndexBuffer(3, buff_Oversold_Low, INDICATOR_DATA);
  
  // кривая RSI
  SetIndexBuffer(4, buff_RSI, INDICATOR_DATA);
    PlotIndexSetDouble(2, PLOT_EMPTY_VALUE, EMPTY_VALUE);
  
  // вспомогательные буферы для расчёта RSI
  SetIndexBuffer(5, buff_Price, INDICATOR_CALCULATIONS);
  SetIndexBuffer(6, buff_ROC, INDICATOR_CALCULATIONS);
  SetIndexBuffer(7, buff_Negative, INDICATOR_CALCULATIONS);
  SetIndexBuffer(8, buff_Positive, INDICATOR_CALCULATIONS);
  
  IndicatorSetInteger(INDICATOR_DIGITS, 2);
  IndicatorSetString(INDICATOR_SHORTNAME, "LBR/RSI");
  
  return(INIT_SUCCEEDED);
}

В штатном обработчике события OnCalculate организуем два раздельных цикла — первый подготовит массив данных ROC, второй рассчитает значения осциллятора на основе данных этого массива.

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

int
  i_RSI_Period = int(TS_MomPin_RSI_Period),         // перевод периода RSI в тип int
  i_Bar, i_Period_Bar                               // два индекса баров для одновременного использования
;
double
  d_Sum_Negative, d_Sum_Positive,                   // вспомогательные переменные для расчёта RSI
  d_Change                                          // вспомогательная переменная для расчёта ROC
;

// Заполнить буфер ROC и залить фоном зоны:
i_Period_Bar = 1;
while(++i_Period_Bar < rates_total && !IsStopped()) {
// расчётная цена бара:
  switch(TS_MomPin_Applied_Price) {
    case PRICE_CLOSE:     buff_Price[i_Period_Bar] = Close[i_Period_Bar]; break;
    case PRICE_OPEN:      buff_Price[i_Period_Bar] = Open[i_Period_Bar]; break;
    case PRICE_HIGH:      buff_Price[i_Period_Bar] = High[i_Period_Bar]; break;
    case PRICE_LOW:       buff_Price[i_Period_Bar] = Low[i_Period_Bar]; break;
    case PRICE_MEDIAN:    buff_Price[i_Period_Bar] = 0.50000 * (High[i_Period_Bar] + Low[i_Period_Bar]); break;
    case PRICE_TYPICAL:   buff_Price[i_Period_Bar] = 0.33333 * (High[i_Period_Bar] + Low[i_Period_Bar] + Open[i_Period_Bar]); break;
    case PRICE_WEIGHTED:  buff_Price[i_Period_Bar] = 0.25000 * (High[i_Period_Bar] + Low[i_Period_Bar] + Open[i_Period_Bar] + Open[i_Period_Bar]); break;
  }
  // разница расчётных цен баров (значение ROC):
  if(i_Period_Bar > 1) buff_ROC[i_Period_Bar] = buff_Price[i_Period_Bar] - buff_Price[i_Period_Bar - 2];
  
  // заливка фона:
  buff_Overbought_High[i_Period_Bar] = 100;
  buff_Overbought_Low[i_Period_Bar] = TS_MomPin_RSI_Overbought;
  buff_Oversold_High[i_Period_Bar] = TS_MomPin_RSI_Oversold;
  buff_Oversold_Low[i_Period_Bar] = 0;
}

Второй цикл (расчёт RSI) не имеет никаких особенностей, он практически полностью повторяет алгоритм стандартного осциллятора этого типа:

i_Period_Bar = prev_calculated - 1;
if(i_Period_Bar <= i_RSI_Period) {
  buff_RSI[0] = buff_Positive[0] = buff_Negative[0] = d_Sum_Positive = d_Sum_Negative = 0;
  i_Bar = 0;
  while(i_Bar++ < i_RSI_Period) {
    buff_RSI[0] = buff_Positive[0] = buff_Negative[0] = 0;
    d_Change = buff_ROC[i_Bar] - buff_ROC[i_Bar - 1];
    d_Sum_Positive += (d_Change > 0 ? d_Change : 0);
    d_Sum_Negative += (d_Change < 0 ? -d_Change : 0);
  }
  buff_Positive[i_RSI_Period] = d_Sum_Positive / i_RSI_Period;
  buff_Negative[i_RSI_Period] = d_Sum_Negative / i_RSI_Period;
  
  if(buff_Negative[i_RSI_Period] != 0)
    buff_RSI[i_RSI_Period] = 100 - (100 / (1. + buff_Positive[i_RSI_Period] / buff_Negative[i_RSI_Period]));
  else
    buff_RSI[i_RSI_Period] = buff_Positive[i_RSI_Period] != 0 ? 100 : 50;
  
  i_Period_Bar = i_RSI_Period + 1;
}

i_Bar = i_Period_Bar - 1;
while(++i_Bar < rates_total && !IsStopped()) {
  d_Change = buff_ROC[i_Bar] - buff_ROC[i_Bar - 1];
  
  buff_Positive[i_Bar] = (buff_Positive[i_Bar - 1] * (i_RSI_Period - 1) + (d_Change> 0 ? d_Change : 0)) / i_RSI_Period;
  buff_Negative[i_Bar] = (buff_Negative[i_Bar - 1] * (i_RSI_Period - 1) + (d_Change <0 ? -d_Change : 0)) / i_RSI_Period;
  
  if(buff_Negative[i_Bar] != 0)
    buff_RSI[i_Bar] = 100 - 100. / (1. + buff_Positive[i_Bar] / buff_Negative[i_Bar]);
  else
    buff_RSI[i_Bar] = buff_Positive[i_Bar] != 0 ? 100 : 50;
}

Индикатор назовём LBR_RSI.mq5 и поместим его в штатную папку индикаторов каталога данных терминала. Именно оно будет прописано в функции iCustom сигнального модуля, поэтому менять его не следует.

Сигнальный модуль

В подключаемом к советнику и индикатору сигнальном модуле разместим пользовательские настройки торговой стратегии "Momentum Pinball". Авторы приводят фиксированные значения для расчёта индикатора LBR/RSI (период RSI = 3, уровень перекупленности = 30, уровень перепроданности = 70). Но для экспериментов мы, разумеется, сделаем их изменяемыми, как и методы закрытия позиции — в книге упомянуты целых три варианта. Запрограммируем их все, а пользователь получит возможность выбора нужной опции:

  • закрывать позицию по трейлингу уровня Stop Loss;
  • закрывать её утром следующего дня;
  • ждать на второй день пробития экстремума дня открытия позиции.

Понятие 'утро' довольно расплывчатое, для формализации правил надо иметь более точное определение. Рашке и Коннорс об этом не говорят, но логично предположить, что использованная в других правилах ТС привязка к первому бару нового дня и будет указывать на метку 'утро' суточной шкалы времени.

Не забудем ещё о двух настройках ТС — отступах от границ первого часа дня, которые должны определять уровни установки отложенного ордера и уровень StopLoss:

enum ENUM_EXIT_MODE {     // Список методов выхода
  CLOSE_ON_SL_TRAIL,      // только по тралу
  CLOSE_ON_NEW_1ST_CLOSE, // по закрытию 1го бара следующего дня
  CLOSE_ON_DAY_BREAK      // по пробитию экстремума дня открытия позиции
};

// пользовательские настройки

input ENUM_APPLIED_PRICE  TS_MomPin_Applied_Price = PRICE_CLOSE;     // Momentum Pinball: Цены для расчёта ROC
input uint    TS_MomPin_RSI_Period = 3;                              // Momentum Pinball: Период RSI
input double  TS_MomPin_RSI_Overbought = 70;                         // Momentum Pinball: Уровень перепроданности по RSI
input double  TS_MomPin_RSI_Oversold = 30;                           // Momentum Pinball: Уровень перекупленности по RSI
input uint    TS_MomPin_Entry_Offset = 10;                           // Momentum Pinball: Отступ уровня входа от границ H1 (в пунктах)
input uint    TS_MomPin_Exit_Offset = 10;                            // Momentum Pinball: Отступ уровня выхода от границ H1 (в пунктах)
input ENUM_EXIT_MODE  TS_MomPin_Exit_Mode = CLOSE_ON_SL_TRAIL;       // Momentum Pinball: Метод закрытия прибыльной позиции

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

ENUM_ENTRY_SIGNAL fe_Get_Entry_Signal(      // Анализ 2х-свечного паттерна (D1 + H1)
  datetime  t_Time,                         // текущее время
  double&    d_Entry_Level,                 // уровень входа (ссылка на переменную)
  double&    d_SL,                          // уровень StopLoss (ссылка на переменную)
  double&    d_TP,                          // уровень TakeProfit (ссылка на переменную)
  double&    d_Range_High,                  // максимум диапазона бара 1го часа (ссылка на переменную)
  double&    d_Range_Low                    // минимум диапазона бара 1го часа (ссылка на переменную)
) {
  // тело функции
}

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

static ENUM_ENTRY_SIGNAL se_Trade_Direction = ENTRY_UNKNOWN;   // направление торговли на сегодня
static double
  // переменные для хранения рассчитанных уровней между тиками
  sd_Entry_Level = 0,
  sd_SL = 0, sd_TP = 0,
  sd_Range_High = 0, sd_Range_Low = 0
;

if(t_Time < 0) {                                               // только для вызова из индикатора
  sd_Entry_Level = sd_SL = sd_TP = sd_Range_High = sd_Range_Low = 0;
  se_Trade_Direction = ENTRY_UNKNOWN;
}

// по умолчанию используем ранее сохранённые уровни входов/выходов:
  d_Entry_Level = sd_Entry_Level; d_SL = sd_SL; d_TP = sd_TP; d_Range_High = sd_Range_High; d_Range_Low = sd_Range_Low;

Дальше будет код получения хэндла индикатора LBR/RSI при первом вызове функции:

static int si_Indicator_Handle = INVALID_HANDLE;
if(si_Indicator_Handle == INVALID_HANDLE) {
  // получить хэндл индикатора при первом вызове функции:
  si_Indicator_Handle = iCustom(_Symbol, PERIOD_D1, "LBR_RSI",
    TS_MomPin_Applied_Price,
    TS_MomPin_RSI_Period,
    TS_MomPin_RSI_Overbought,
    TS_MomPin_RSI_Oversold
  );
  
  if(si_Indicator_Handle == INVALID_HANDLE) { // хэндл индикатора не получен
    if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: ошибка получения хэндла индикатора LBR_RSI #%u", __FUNCTION__, _LastError);
    return(ENTRY_INTERNAL_ERROR);
  }
}

Один раз в сутки роботу нужно проанализировать значение индикатора на последнем закрывшемся дневном баре и определить разрешённое на сегодня направление торговли. Либо он должен зафиксировать запрет на торговлю, если значение LBR/RSI — в нейтральной зоне. Код извлечения этого значения из индикаторного буфера и его анализа, с функциями логирования, с учётом возможных ошибок и особенностей вызова из индикатора ручной торговли:

static int si_Indicator_Handle = INVALID_HANDLE;
if(si_Indicator_Handle == INVALID_HANDLE) {
  // получить хэндл индикатора при первом вызове функции:
  si_Indicator_Handle = iCustom(_Symbol, PERIOD_D1, "LBR_RSI",
    TS_MomPin_Applied_Price,
    TS_MomPin_RSI_Period,
    TS_MomPin_RSI_Overbought,
    TS_MomPin_RSI_Oversold
  );
  
  if(si_Indicator_Handle == INVALID_HANDLE) {       // хэндл не получен
    if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: ошибка получения хэндла индикатора LBR_RSI #%u", __FUNCTION__, _LastError);
    return(ENTRY_INTERNAL_ERROR);
  }
}

// узнать время дневного бара предыдущего дня:
datetime ta_Bar_Time[];
if(CopyTime(_Symbol, PERIOD_D1, fabs(t_Time), 2, ta_Bar_Time) < 2) {
  if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyTime: ошибка #%u", __FUNCTION__, _LastError);
  return(ENTRY_INTERNAL_ERROR);
}

// анализ вчерашнего дня, если это 1й вызов сегодня:
static datetime st_Prev_Day = 0;
if(t_Time < 0) st_Prev_Day = 0;                     // только для вызова из индикатора
if(st_Prev_Day < ta_Bar_Time[0]) {
  // обнуление параметров предыдущего дня:
  se_Trade_Direction = ENTRY_UNKNOWN;
  d_Entry_Level = sd_Entry_Level = d_SL = sd_SL = d_TP = sd_TP = d_Range_High = sd_Range_High = d_Range_Low = sd_Range_Low = 0;
  
  // извлечь значение LBR/RSI предыдущего дня:
  double da_Indicator_Value[];
  if(1 > CopyBuffer(si_Indicator_Handle, 4, ta_Bar_Time[0], 1, da_Indicator_Value)) {
    if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyBuffer: ошибка #%u", __FUNCTION__, _LastError);
    return(ENTRY_INTERNAL_ERROR);
  }
  
  // если что-то не так со значением LBR/RSI:
  if(da_Indicator_Value[0] > 100. || da_Indicator_Value[0] < 0.) {
    if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: Ошибка значения (%f) индикаторного буфера", __FUNCTION__, da_Indicator_Value[0]);
    return(ENTRY_UNKNOWN);
  }
  
  st_Prev_Day = ta_Bar_Time[0];                     // попытка засчитана
  
  // запомнить направление торговли на сегодня:
  if(da_Indicator_Value[0] > TS_MomPin_RSI_Overbought) se_Trade_Direction = ENTRY_SELL;
  else se_Trade_Direction = da_Indicator_Value[0] > TS_MomPin_RSI_Oversold ? ENTRY_NONE : ENTRY_BUY;
  
  // в лог:
  if(Log_Level == LOG_LEVEL_DEBUG) PrintFormat("%s: Направление торговли на %s: %s. LBR/RSI: (%.2f)",
    __FUNCTION__,
    TimeToString(ta_Bar_Time[1], TIME_DATE),
    StringSubstr(EnumToString(se_Trade_Direction), 6),
    da_Indicator_Value[0]
  );
}

Мы выяснили разрешенное направление торговли. Следующей задачей станет определение уровней входа и ограничения убытков (Stop Loss). Это тоже достаточно сделать один раз в сутки — сразу после закрытия первого бара дня на часовом таймфрейме. Но, с учётом особенностей работы индикатора ручной торговли, алгоритм придётся немного усложнить. Это вызвано тем, что индикатор должен не только выявлять сигнальные уровни в реальном времени, но и делать разметку на истории:

// сегодня сигнала не ищем
if(se_Trade_Direction == ENTRY_NONE) return(ENTRY_NONE);

// анализ сегодняшнего первого бара H1, если этого ещё не сделано:
if(sd_Entry_Level == 0.) {
  // получить данные 24х последних баров H1:
  MqlRates oa_H1_Rates[];
  int i_Price_Bars = CopyRates(_Symbol, PERIOD_H1, fabs(t_Time), 24, oa_H1_Rates);
  if(i_Price_Bars == WRONG_VALUE) {                      // обработка ошибки функции CopyRates
    if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyRates: ошибка #%u", __FUNCTION__, _LastError);
    return(ENTRY_INTERNAL_ERROR);
  }
  
  // найти среди 24х баров 1й бар сегодняшнего дня и запомнить High, Low:
  int i_Bar = i_Price_Bars;
  while(i_Bar-- > 0) {
    if(oa_H1_Rates[i_Bar].time < ta_Bar_Time[1]) break;      // последний бар H1 вчерашнего дня
    
    // границы диапазона 1го бара H1:
    sd_Range_High = d_Range_High = oa_H1_Rates[i_Bar].high;
    sd_Range_Low = d_Range_Low = oa_H1_Rates[i_Bar].low;
  }
  // 1й бар H1 ещё не закрыт:
  if(i_Price_Bars - i_Bar < 3) return(ENTRY_UNKNOWN);
  
  // рассчитать торговые уровни:
  
  // уровень входа в рынок: 
  d_Entry_Level = _Point * TS_MomPin_Entry_Offset;           // вспомогательные расчёты
  sd_Entry_Level = d_Entry_Level = se_Trade_Direction == ENTRY_SELL ? d_Range_Low - d_Entry_Level : d_Range_High + d_Entry_Level;
  // начальный уровень SL: 
  d_SL = _Point * TS_MomPin_Exit_Offset;                     // вспомогательные расчёты
  sd_SL = d_SL = se_Trade_Direction == ENTRY_BUY ? d_Range_Low - d_SL : d_Range_High + d_SL;
}

После этого останется лишь завершить работу функции возвратом выявленного направления торговли:

return(se_Trade_Direction);

Теперь запрограммируем анализ условий для сигнала на закрытие позиции. У нас есть три варианта, один из которых (трейлинг уровня Stop Loss) уже реализован в коде советника предыдущих версий. Два других варианта в сумме требуют для расчётов цену и время входа, направление позиции. Их вместе с текущим временем и выбранным методом закрытия и будем передавать функции fe_Get_Exit_Signal:

ENUM_EXIT_SIGNAL fe_Get_Exit_Signal(    // Выявление сигнала закрытия позиции
  double            d_Entry_Level,      // уровень входа
  datetime          t_Entry_Time,       // время входа
  ENUM_ENTRY_SIGNAL e_Trade_Direction,  // направление торговли
  datetime          t_Current_Time,     // текущее время
  ENUM_EXIT_MODE    e_Exit_Mode         // метод выхода
) {
  static MqlRates soa_Prev_D1_Rate[];   // данные бара D1 предыдущего дня
  static int si_Price_Bars = 0;         // вспомогательный счётчик
  if(t_Current_Time < 0) {              // отличить вызов из индикатора от вызова от советника
    t_Current_Time = -t_Current_Time;
    si_Price_Bars = 0;
  }
  double
    d_Curr_Entry_Level,
    d_SL, d_TP,
    d_Range_High,  d_Range_Low
  ;
  
  if(e_Trade_Direction < 1) {          // позиций нет, всё обнулить
    si_Price_Bars = 0;
  }
  
  switch(e_Exit_Mode) {
    case CLOSE_ON_SL_TRAIL:            // только по тралу
            return(EXIT_NONE);
                      
    case CLOSE_ON_NEW_1ST_CLOSE:       // по закрытию 1го бара следующего дня
            if((t_Current_Time - t_Current_Time % 86400)
              ==
              (t_Entry_Time - t_Current_Time % 86400)
            ) return(EXIT_NONE);       // день открытия позиции ещё не завершён
            
            if(fe_Get_Entry_Signal(t_Current_Time, d_Curr_Entry_Level, d_SL, d_TP, d_Range_High, d_Range_Low)
              < ENTRY_UNKNOWN
            ) {
              if(Log_Level > LOG_LEVEL_ERR) PrintFormat("%s: 1й бар следующего дня закрыт", __FUNCTION__);
              return(EXIT_ALL);
            }
            return(EXIT_NONE);         // не закрыт
            
    case CLOSE_ON_DAY_BREAK:           // по пробитию экстремума дня открытия позиции
            if((t_Current_Time - t_Current_Time % 86400)
              ==
              (t_Entry_Time - t_Current_Time % 86400)
            ) return(EXIT_NONE);       // день открытия позиции ещё на завершён
            
            if(t_Current_Time % 86400 > 36000) return(EXIT_ALL); // время вышло
            
            if(si_Price_Bars < 1) {
              si_Price_Bars = CopyRates(_Symbol, PERIOD_D1, t_Current_Time, 2, soa_Prev_D1_Rate);
              if(si_Price_Bars == WRONG_VALUE) { // обработка ошибки функции CopyRates
                if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyRates: ошибка #%u", __FUNCTION__, _LastError);
                return(EXIT_UNKNOWN);
              }
              
              if(e_Trade_Direction == ENTRY_BUY) {
                if(soa_Prev_D1_Rate[1].high < soa_Prev_D1_Rate[0].high) return(EXIT_NONE);        // не пробила
                
                if(Log_Level > LOG_LEVEL_ERR) PrintFormat("%s: цена пробила вчерашний High: %s > %s", __FUNCTION__, DoubleToString(soa_Prev_D1_Rate[1].high, _Digits), DoubleToString(soa_Prev_D1_Rate[0].high, _Digits));
                return(EXIT_BUY);
              } else {
                if(soa_Prev_D1_Rate[1].low > soa_Prev_D1_Rate[0].low) return(EXIT_NONE);          // не пробила
                
                if(Log_Level > LOG_LEVEL_ERR) PrintFormat("%s: цена пробила вчерашний Low: %s < %s", __FUNCTION__, DoubleToString(soa_Prev_D1_Rate[1].low, _Digits), DoubleToString(soa_Prev_D1_Rate[0].low, _Digits));
                return(EXIT_SELL);
              }
            }
            
            return(EXIT_NONE); // на всякий
  }
  
  return(EXIT_UNKNOWN);
}

Здесь стоит 'заглушка' на случай, если выбрана опция 'выход по трэйлингу' — функция возвращает отсутствие сигнала безо всяких анализов. Для двух других опций выявляется наступление событий 'настало утро' и 'пробит вчерашний экстремум'. Варианты возвращаемых функцией значений типа ENUM_EXIT_SIGNAL очень схожи с аналогичным списком значений сигналов на вход (ENUM_ENTRY_SIGNAL):

enum ENUM_EXIT_SIGNAL {  // Список сигналов на выход
  EXIT_UNKNOWN,          // не определено
  EXIT_BUY,              // закрыть покупки
  EXIT_SELL,             // закрыть продажи
  EXIT_ALL,              // закрыть всё
  EXIT_NONE              // ничего не закрывать
};

Индикатор для ручной торговли

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

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

Чтобы индикатор можно было использовать и для обычной ручной торговли, добавим в него систему оповещения трейдера в режиме реального времени. Такое оповещение будет содержать рекомендованное сигнальным модулем направление входа вместе с уровнями установки отложенного ордера и аварийного выхода (Stop Loss). Способов доставки оповещения будет три — стандартное всплывающее окно с текстом и звуковым сигналом, сообщение на электронную почту и push-уведомление на мобильное устройство.

Все требования к индикатору перечислены. Значит, можно приступать к программированию. Чтоб отрисовать на графике все запланированные нами объекты, в индикаторе должны быть один буфер типа DRAW_FILLING (для заливки диапазона баров первого часа дня) и три буфера для отображения торговых уровней (уровень входа, уровень фиксации прибыли, уровень ограничения убытка). Один из них (уровень установки отложенного ордера) должен иметь возможность менять цвет (тип DRAW_COLOR_LINE) в зависимости от направления торговли, а двум другим достаточно одноцветного типа DRAW_LINE:

#property indicator_chart_window
#property indicator_buffers  6
#property indicator_plots    4

#property indicator_label1  "1й час дня"
#property indicator_type1   DRAW_FILLING
#property indicator_color1  C'255,208,234', C'179,217,255'
#property indicator_width1  1

#property indicator_label2  "Уровень входа"
#property indicator_type2   DRAW_COLOR_LINE
#property indicator_style2  STYLE_DASHDOT
#property indicator_color2  clrDodgerBlue, clrDeepPink
#property indicator_width2  2

#property indicator_label3  "Stop Loss"
#property indicator_type3   DRAW_LINE
#property indicator_style3  STYLE_DASHDOTDOT
#property indicator_color3  clrCrimson
#property indicator_width3  1

#property indicator_label4  "Take Profit"
#property indicator_type4   DRAW_LINE
#property indicator_color4  clrGreen
#property indicator_width4  1

Теперь нужно объявить списки, часть из которых в индикаторе не нужны (используются только советником), но задействованы в функциях сигнального модуля. Эти переменные типа enum нужны для работы с логированием и разными методами закрытия позиций, которые мы опустим и в индикаторе — напомню, здесь будет только имитация простой фиксации прибыли на заданном уровне (Take Profit). Вслед за объявлением этих переменных можно подключать внешний модуль, перечислять пользовательские настройки и объявлять глобальные переменные:

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,          // статус не определён
  ENTRY_INTERNAL_ERROR    // внутренняя ошибка функции
};
enum ENUM_EXIT_SIGNAL {  // Список сигналов на выход
  EXIT_UNKNOWN,          // не определено
  EXIT_BUY,              // закрыть покупки
  EXIT_SELL,             // закрыть продажи
  EXIT_ALL,              // закрыть всё
  EXIT_NONE              // ничего не закрывать
};

#include <Expert\Signal\Signal_Momentum_Pinball.mqh>     // сигнальный модуль ТС 'Momentum Pinball'
input uint    TS_MomPin_Take_Profit = 10;                // Momentum Pinball: Take Profit (в пунктах)

input bool    Show_1st_H1_Bar = true;                    // Показывать диапазон 1го часового бара дня?
input bool    Alert_Popup = true;                        // Алерт: Показывать всплывающее окно?
input bool    Alert_Email = false;                       // Алерт: Отправлять eMail?
input string  Alert_Email_Subj = "";                     // Алерт: Тема eMail—сообщения
input bool    Alert_Push = true;                         // Алерт: Отправлять push—уведомление?

input uint  Days_Limit = 7;                              // Глубина разметки истории (календарных дней)

ENUM_LOG_LEVEL  Log_Level = LOG_LEVEL_DEBUG;             // Режим протоколирования
double
  buff_1st_H1_Bar[], buff_1st_H1_Bar_Zero[],             // буферы для заливки диапазона 1го часового бара дня
  buff_Entry[], buff_Entry_Color[],                      // буферы линии отложенного ордера
  buff_SL[],                                             // буфер линии StopLoss
  buff_TP[],                                             // буфер линии TakeProfit
  gd_Entry_Offset = 0,                                   // TS_MomPin_Entry_Offset в ценах инструмента
  gd_Exit_Offset = 0                                     // TS_MomPin_Exit_Offset в ценах инструмента
;

В функции инициализации нет ничего примечательного — здесь назначим индексы буферов индикатора массивам, объявленным поимённо для этих буферов. Также здесь мы переведём пользовательские настройки из пунктов в цены инструмента — чтобы хоть немного сократить расход ресурсов и не делать такого преобразования много тысяч раз по ходу работы основной программы:

int OnInit() {
  // перевод пунктов в цены инструмента:
  gd_Entry_Offset = TS_MomPin_Entry_Offset * _Point;
  gd_Exit_Offset = TS_MomPin_Exit_Offset * _Point;
  
  // назначение буферов индикатора:
  
  // прямоугольник диапазона 1го часового бара дня
  SetIndexBuffer(0, buff_1st_H1_Bar, INDICATOR_DATA);
    PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, 0);
  SetIndexBuffer(1, buff_1st_H1_Bar_Zero, INDICATOR_DATA);
    PlotIndexSetDouble(1, PLOT_EMPTY_VALUE, 0);
  
  // линия установки отложенного ордера
  SetIndexBuffer(2, buff_Entry, INDICATOR_DATA);
    PlotIndexSetDouble(1, PLOT_EMPTY_VALUE, 0);
  SetIndexBuffer(3, buff_Entry_Color, INDICATOR_COLOR_INDEX);
  
  // линия SL
  SetIndexBuffer(4, buff_SL, INDICATOR_DATA);
    PlotIndexSetDouble(2, PLOT_EMPTY_VALUE, 0);
  
  // линия TP
  SetIndexBuffer(5, buff_TP, INDICATOR_DATA);
    PlotIndexSetDouble(3, PLOT_EMPTY_VALUE, 0);
  
  IndicatorSetInteger(INDICATOR_DIGITS, _Digits);
  IndicatorSetString(INDICATOR_SHORTNAME, "Momentum Pinball");
  
  return(INIT_SUCCEEDED);
}

В коде индикатора предыдущей статьи этой серии была создана некая программная сущность, назначение которой — сохранять информацию любого типа между тиками. Подробнее о том, почему она понадобилась и как устроена, вы можете прочесть там, а здесь мы просто задействуем её без каких-либо изменений. В этой версии индикатора из всего функционала 'домового' будет задействован лишь флаг начала нового бара. Но если возникнет желание сделать индикатор для ручной торговли более продвинутым, другие функции 'домового' будут очень кстати. Полный код структуры go_Brownie можно посмотреть в конце файла исходного кода индикатора (TS_Momentum_Pinball.mq5) в приложении к этой статье. Там же можно увидеть и код функции рассылки оповещений f_Do_Alert — в ней тоже нет никаких изменений по сравнению с предыдущим индикатором этой серии статей, поэтому рассматривать подробно его нет необходимости.

Внутри штатного обработчика события поступления тика (OnCalculate), перед началом основного цикла программы, надо объявить необходимые переменные. Если это не первый вызов основного цикла, надо ограничить диапазон перерасчёта только актуальными на данный момент барами — для этой торговой стратегии это бары сегодняшнего и вчерашнего дней. Если же это первый после инициализации вызов цикла, то надо организовать зачистку буферов индикатора от остаточных данных. Если этого не сделать, то при переключении таймфрейма останутся закрашенными уже не актуальные области. Кроме того, стоит ограничить вызов основной функции одним разом за бар. Всё это удобно делать с помощью структуры go_Brownie (домовой):

go_Brownie.f_Update(prev_calculated, prev_calculated);     // "покормить" информацией домового

datetime t_Time = TimeCurrent();                           // последнее известное время сервера
int
  i_Period_Bar = 0,                                        // вспомогательный счётчик
  i_Current_TF_Bar = 0                                     // индекс бара начала цикла
;

if(go_Brownie.b_First_Run) {                               // если это 1й запуск
  i_Current_TF_Bar = rates_total — Bars(_Symbol, PERIOD_CURRENT, t_Time — t_Time % 8640086400 * Days_Limit, t_Time);
  // очистить буфера при переинициализации:
  ArrayInitialize(buff_1st_H1_Bar, 0); ArrayInitialize(buff_1st_H1_Bar_Zero, 0);
  ArrayInitialize(buff_Entry, 0); ArrayInitialize(buff_Entry_Color, 0);
  ArrayInitialize(buff_TP, 0);
  ArrayInitialize(buff_SL, 0);
} else if(!go_Brownie.b_Is_New_Bar) return(rates_total);   // ждём закрытия бара
else {                                                     // новый бар
  // минимальная глубина пересчёта — с начала дня:
  i_Current_TF_Bar = rates_total — Bars(_Symbol, PERIOD_CURRENT, t_Time — t_Time % 86400, t_Time);
}
ENUM_ENTRY_SIGNAL e_Entry_Signal = ENTRY_UNKNOWN;          // сигнал на вход
double
  d_SL = WRONG_VALUE,                                      // уровень SL
  d_TP = WRONG_VALUE,                                      // уровень TP
  d_Entry_Level = WRONG_VALUE,                             // уровень входа
  d_Range_High = WRONG_VALUE, d_Range_Low = WRONG_VALUE    // границы диапазона 1го бара паттерна
;
datetime
  t_Curr_D1_Bar = 0,                                       // время текущего бара D1 (2го бара паттерна)
  t_Last_D1_Bar = 0,                                       // время последнего бара D1, на котором был сигнал
  t_Entry_Bar = 0                                          // время бара установки отложенного ордера
;

// проконтролировать, чтобы индекс начального бара пересчёта был в допустимых рамках:
i_Current_TF_Bar = int(fmax(0, fmin(i_Current_TF_Bar, rates_total — 1)));

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

while(++i_Current_TF_Bar < rates_total && !IsStopped()) {                // перебор баров текущего ТФ
  // получить данные из сигнального модуля:
  e_Entry_Signal = fe_Get_Entry_Signal(-Time[i_Current_TF_Bar], d_Entry_Level, d_SL, d_TP, d_Range_High, d_Range_Low);
  if(e_Entry_Signal == ENTRY_INTERNAL_ERROR) {                           // ошибка копирования данных из буфера внешнего индикатора
    // придётся повторить расчёты и отрисовку на следующем тике:
    go_Brownie.f_Reset();
    return(rates_total);
  }
  if(e_Entry_Signal > 1) continue;                                       // активного сигнала на этом баре нет

Если же модуль выявил наличие сигнала на рассматриваемом баре и вернул расчётный уровень входа, то сначала вычислим уровень фиксации прибыли (Take Profit):

t_Curr_D1_Bar = Time[i_Current_TF_Bar] - Time[i_Current_TF_Bar] % 86400; // начало дня, к которому принадлежит этот бар

А затем разметим на истории этот трейд в развитии, если это первый бар нового дня:

t_Curr_D1_Bar = Time[i_Current_TF_Bar] - Time[i_Current_TF_Bar] % 86400;            // начало дня, к которому принадлежит этот бар
if(t_Last_D1_Bar < t_Curr_D1_Bar) {                                                 // это 1й бар дня, на котором есть сигнал
  t_Entry_Bar = Time[i_Current_TF_Bar];                                             // запомнить время начала торговли

Начнём с заливки фоном баров первого часа дня, использованных в расчётах уровней:

// Заливка фоном баров 1го часа:
if(Show_1st_H1_Bar) {
  i_Period_Bar = i_Current_TF_Bar;
  while(Time[--i_Period_Bar] >= t_Curr_D1_Bar && i_Period_Bar > 0)
    if(e_Entry_Signal == ENTRY_BUY) {                               // бычий паттерн
      
      buff_1st_H1_Bar_Zero[i_Period_Bar] = d_Range_High;
      buff_1st_H1_Bar[i_Period_Bar] = d_Range_Low;
    } else {                                                        // медвежий паттерн
      buff_1st_H1_Bar[i_Period_Bar] = d_Range_High;
      buff_1st_H1_Bar_Zero[i_Period_Bar] = d_Range_Low;
    }
}

Затем нарисуем линию установки отложенного ордера до того момента, когда отложенный ордер станет открытой позицией, т.е. до касания ценой этого уровня:

// Линия входа до пересёкшего её бара:
i_Period_Bar = i_Current_TF_Bar - 1;
if(e_Entry_Signal == ENTRY_BUY) {                               // бычий паттерн
  while(++i_Period_Bar < rates_total) {
    if(Time[i_Period_Bar] > t_Curr_D1_Bar + 86399) {            // конец дня
      e_Entry_Signal = ENTRY_NONE;                              // отложенный ордер не сработал
      break;
    }
    
    // продлить линию:
    buff_Entry[i_Period_Bar] = d_Entry_Level;
    buff_Entry_Color[i_Period_Bar] = 0;
    
    if(d_Entry_Level <= High[i_Period_Bar]) break;               // вход был на этом баре
  }
} else {                                                         // медвежий паттерн
  while(++i_Period_Bar < rates_total) {
    if(Time[i_Period_Bar] > t_Curr_D1_Bar + 86399) {             // конец дня
      e_Entry_Signal = ENTRY_NONE;                               // отложенный ордер не сработал
      break;
    }
    
    // продлить линию:
    buff_Entry[i_Period_Bar] = d_Entry_Level;
    buff_Entry_Color[i_Period_Bar] = 1;
    
    if(d_Entry_Level >= Low[i_Period_Bar]) break;               // вход был на этом баре
  }
}

Если цена не достигла расчётного уровня до конца дня, перейдём к следующему шагу основного цикла:

if(e_Entry_Signal == ENTRY_NONE) {                 // отложенный ордер не сработал до конца дня
  i_Current_TF_Bar = i_Period_Bar;                 // бары этого дня больше нас не интересуют
  continue;
}

Если же этот день ещё не завершён и судьба отложенного ордера ещё не определена, то нет смысла продолжать основной цикл программы:

if(i_Period_Bar >= rates_total - 1) break;        // текущий (незавершенный) день отработан

После этих двух фильтров останется только один возможный вариант развития событий — отложенный ордер сработал. Найдём бар исполнения отложенного ордера и, начиная с этого бара, отрисуем уровни Take Profit и Stop Loss до пересечения ценой одного из них, то есть до закрытия позиции. При этом надо предусмотреть ситуацию, при которой открытие и закрытие позиции произойдёт на одном и том же баре — в этом случае нужно продлить линию на один бар в прошлое, чтобы её можно стало видно на графике:

// ордер сработал, найти бар закрытия позиции:
i_Period_Bar = fmin(i_Period_Bar, rates_total - 1);
buff_SL[i_Period_Bar] = d_SL;

while(++i_Period_Bar < rates_total) {
  if(TS_MomPin_Exit_Mode == CLOSE_ON_SL_TRAIL) {
    if(Time[i_Period_Bar] >= t_Curr_D1_Bar + 86400) break;        // это бар следующего дня
    
    // Линии TP и SL до бара, пересёкшего одну из них:
    buff_SL[i_Period_Bar] = d_SL;
    buff_TP[i_Period_Bar] = d_TP;
    
    if((
      e_Entry_Signal == ENTRY_BUY && d_SL >= Low[i_Period_Bar]
      ) || (
      e_Entry_Signal == ENTRY_SELL && d_SL <= High[i_Period_Bar]
    )) {                                                          // выход по SL
      if(buff_SL[int(fmax(0, i_Period_Bar - 1))] == 0.) {
   // начало и конец на одном баре, продлим на 1 бар в прошлое
        buff_SL[int(fmax(0, i_Period_Bar - 1))] = d_SL;
        buff_TP[int(fmax(0, i_Period_Bar - 1))] = d_TP;
      }
      break;
    }
    
    if((
      e_Entry_Signal == ENTRY_BUY && d_TP <= High[i_Period_Bar]
      ) || (
      e_Entry_Signal == ENTRY_SELL && d_SL >= Low[i_Period_Bar]
    )) {                                                         // выход по TP
      if(buff_TP[int(fmax(0, i_Period_Bar - 1))] == 0.) {
        // начало и конец на одном баре, продлим на 1 бар в прошлое
        buff_SL[int(fmax(0, i_Period_Bar - 1))] = d_SL;
        buff_TP[int(fmax(0, i_Period_Bar - 1))] = d_TP;
      }
      break;
    }
  }
}

После закрытия позиции оставшиеся бары дня можно пропустить в основном цикле программы:

i_Period_Bar = i_Current_TF_Bar;
t_Curr_D1_Bar = Time[i_Period_Bar] - Time[i_Period_Bar] % 86400;
while(
  ++i_Period_Bar < rates_total
  &&
  t_Curr_D1_Bar == Time[i_Period_Bar] - Time[i_Period_Bar] % 86400
) i_Current_TF_Bar = i_Period_Bar;

На этом код основного цикла заканчивается. Осталось лишь организовать оповещение, если сигнал выявлен на текущем баре:

i_Period_Bar = rates_total - 1;                                            // текущий бар

if(Alert_Popup + Alert_Email + Alert_Push == 0) return(rates_total);       // всё отключено
if(t_Entry_Bar != Time[i_Period_Bar]) return(rates_total);                 // на этом баре сигнала нет

// текст сообщения:
string s_Message = StringFormat("ТС Momentum Pinball: нужен %s @ %s, SL: %s",
  e_Entry_Signal == ENTRY_BUY ? "BuyStop" : "SellStop",
  DoubleToString(d_Entry_Level, _Digits),
  DoubleToString(d_SL, _Digits)
);
// оповещение:
f_Do_Alert(s_Message, Alert_Popup, false, Alert_Email, Alert_Push, Alert_Email_Subj);

Полный код индикатора можно увидеть в файле TS_Momentum_Pinball.mq5 приложения к этой статье.

Советник для тестирования ТС 'Momentum Pinball'

Функциональность базового эксперта надо слегка расширить при подготовке к тестированию очередной торговой стратегии из книги Рашке и Коннорса. Исходный код, взятый за основу этой версии, вместе с подробным описанием можно найти в предыдущей статье. Повторяться здесь не будем, рассмотрим лишь существенные изменения и дополнения, которых всего два.

Первое дополнение — список сигналов на выход, которого не было в предыдущей версии торгового робота. Кроме этого, добавлено состояние ENTRY_INTERNAL_ERROR в список сигналов на вход. Эти нумерованные списки ничем не отличаются от таких же enum-списков в рассмотренном выше индикаторе. В коде робота мы разместим их перед строкой подключения класса торговых операций стандартной библиотеки. В файле Street_Smarts_Bot_MomPin.mq5 приложения к статье это строки 24..32.

Второе изменение связано с тем, что сигнальный модуль теперь выдаёт и сигналы на закрытие позиции. Добавим соответствующий блок кода для работы и с этим сигналом. В предыдущей версии робота есть условный оператор if для проверки, является ли существующая позиция новой (строка 139) — проверка используется для расчёта и установки начального уровня StopLoss. В этой версии добавим к оператору if через альтернативное else соответствующий блок кода для обращения к сигнальному модулю. Если результат обращения этого потребует, советник должен закрыть позицию:

} else {                       // не новая позиция
                               // сложились условия для закрытия позиции?
  ENUM_EXIT_SIGNAL e_Exit_Signal = fe_Get_Exit_Signal(d_Entry_Level, datetime(PositionGetInteger(POSITION_TIME)), e_Entry_Signal, TimeCurrent(), TS_MomPin_Exit_Mode);
  if((
      e_Exit_Signal == EXIT_BUY && e_Entry_Signal == ENTRY_BUY
    ) || (
      e_Exit_Signal == EXIT_SELL && e_Entry_Signal == ENTRY_SELL
    ) || e_Exit_Signal == EXIT_ALL
  ) {
                              // надо закрывать
    CTrade o_Trade;
    o_Trade.LogLevel(LOG_LEVEL_ERRORS);
    o_Trade.PositionClose(_Symbol);
    return;
  }
}

В исходном коде бота это строки 171..186.

Есть некоторые изменения в коде функции, контролирующей достаточность расстояния до торговых уровней fb_Is_Acceptable_Distance (строки 424..434).


Тестирование стратегии на исторических данных

Мы создали пару инструментов (индикатор и советник) для исследования торговой системы, получившей известность благодаря книге Л.Рашке и Л.Коннорса. Основная цель прогона советника на исторических данных — проверка работоспособности торгового робота, одного из этих инструментов. Поэтому оптимизации параметров я не делал, тестирование проводилось с настройками по умолчанию.

Полные результаты всех прогонов вы можете найти в приложенном архиве, а здесь приведу лишь графики изменения баланса. Только как иллюстрация второй по значимости цели тестирования — грубой (без оптимизации параметров) оценки работоспособности ТС в условиях современного рынка. Напомню — авторы иллюстрировали стратегию графиками конца прошлого века.

График изменения баланса при тестировании советника с начала 2014 года на котировках демо-сервера MetaQuotes. Инструмент — EURJPY, таймфрейм — H1:


Аналогичный график для инструмента EURUSD, того же таймфрейма и с тем же периодом тестирования:


При тестировании без изменения настроек на котировках одного из металлов (XAUUSD) за тот же период и на том же таймфрейме график изменения баланса становится таким:


Заключение

Перечисленные в книге Street Smarts: High Probability Short-Term Trading Strategies правила для торговой системы 'Momentum Pinball' перенесены в код индикатора и советника. К сожалению, описание не столь подробно, как хотелось бы и оставляет более одного варианта для правил сопровождения и закрытия позиций. Поэтому у тех, кто желает подробно исследовать особенности торговой системы, есть довольно широкое поле для подбора оптимальных параметров и алгоритмов действий робота. Созданный код даёт такую возможность, а кроме этого, надеюсь, исходники будут полезны при освоении объектно-ориентированного программирования.

Исходные коды, скомпилированные файлы и библиотека в архиве MQL5.zip расфасованы по соответствующим каталогам. Назначение каждого из них:

# Имя файла Тип  Описание 
 1  LBR_RSI.mq5  индикатор Индикатор, объединивший ROC и RSI. Используется для определения направления торговли (или её запрета) начавшегося дня
 2  TS_Momentum_Pinball.mq5  индикатор Индикатор для ручной торговли по этой ТС. Отображает расчётные уровни входов и выходов, подсвечивает диапазон первого часа, на основе которого производятся расчёты
 3   Signal_Momentum_Pinball.mqh  библиотека  Библиотека функций, структур и пользовательских настроек. Используется индикатором и советником
 4  Street_Smarts_Bot_MomPin.mq5   советник Советник для автоматической торговли по этой ТС 

Прикрепленные файлы |
MPtest.zip (692.56 KB)
MQL5.zip (164.76 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (2)
Vasily Belozerov
Vasily Belozerov | 3 июн. 2018 в 17:25
Цитирую Вас: "В этой статье продолжим программирование торговых стратегий, описанных в разделе книги Л.Рашке и Л.Коннорса, посвященном тестированию ценой границ диапазона". Смотрим справочник. Тестирование - это процесс нахождения ошибок. Так что является ошибкой, продолжение движения цены на границе диапазона или возврат цены от границы диапазона?
Alexander Puzanov
Alexander Puzanov | 3 июн. 2018 в 18:56

Лучше поменять справочник на более близкий к биржевой тематике. Здесь тестирование - проверка свойств, в контексте этого раздела книги - проверка свойств границ диапазона приложением к ним движения цены разной силы. В зависимости от соотношения этих свойств (величины изменения цены и сопротивления границ диапазона) цена может пробить уровни либо отбиться от них

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

Автоматический подбор перспективных сигналов Автоматический подбор перспективных сигналов
Статья посвящена изучению торговых сигналов для MetaTrader 5 с автоматическим исполнением на счетах подписчиков. Также рассматривается разработка инструментов для поиска перспективных торговых сигналов прямо в терминале.
Ночная торговля в азиатскую сессию: как оставаться в прибыли Ночная торговля в азиатскую сессию: как оставаться в прибыли
В статье рассматривается понятие ночной торговли, стратегии торговли, их реализация на MQL5. Проведено тестирование и сделаны выводы.
Торговля по уровням ДиНаполи Торговля по уровням ДиНаполи
В статье рассматривается один из вариантов практической реализации советника для торговли по уровням ДиНаполи при помощи стандартных инструментов MQL5. Протестированы результаты его работы и сделаны выводы.
Раскладываем входы по индикаторам Раскладываем входы по индикаторам
В жизни трейдера бывают разные ситуации. Часто по истории успешных сделок мы пытаемся восстановить стратегию, а глядя на историю убытков — доработать и улучшить ее. И в том, и в другом случае мы сопоставляем сделки с известными индикаторами. В этой статье предлагается методика пакетного сопоставления сделок с рядом индикаторов.