
Торговая стратегия 'Momentum Pinball'
Введение
В этой статье продолжим программирование торговых стратегий, описанных в разделе книги Л.Рашке и Л.Коннорса 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 для входов на покупку сформулированы так.
- На таймфрейме D1 значение индикатора LBR/RSI последнего закрывшегося дня должен находиться в зоне перепроданности — ниже 30.
- После закрытия первого часового бара нового дня установите отложенный ордер на покупку выше максимума этого бара.
- После срабатывания отложенного ордера установите Stop Loss позиции на минимум первого часового бара.
- Если позиция будет закрыта с убытком, повторно установите отложенный ордер на продажу на прежнем уровне.
- Если к концу дня позиция останется прибыльной, оставьте её на следующий день. На второй торговый день позиция обязательно должна быть закрыта.
Визуализация правил входа с помощью двух индикаторов, описанных ниже, будет выглядеть так:
— 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_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, предлагаемой Линдой Рашке, сравнивать надо цены не идущих подряд баров, а с пропуском одного бара между ними. То есть в ТС используются изменения цен дней, отстоящих от дня торговли на один и три рабочих дня, соответственно. Сделать это несложно, а заодно в этом же цикле организуем фоновую заливку зон перекупленности и перепроданности. И не забудем реализовать возможность выбора типа цены:
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:
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 будет унифицирована с функцией сигнального модуля предыдущей торговой стратегии из книги Рашке и Коннорс, а также и с последующими аналогичными модулями других ТС, описанных в этом источнике. Это значит, что функция должна иметь такой набор передаваемых ей параметров, ссылок на переменные и такой же тип возвращаемого значения:
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 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 при первом вызове функции:
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 — в нейтральной зоне. Код извлечения этого значения из индикаторного буфера и его анализа, с функциями логирования, с учётом возможных ошибок и особенностей вызова из индикатора ручной торговли:
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;
}
После этого останется лишь завершить работу функции возвратом выявленного направления торговли:
Теперь запрограммируем анализ условий для сигнала на закрытие позиции. У нас есть три варианта, один из которых (трейлинг уровня Stop Loss) уже реализован в коде советника предыдущих версий. Два других варианта в сумме требуют для расчётов цену и время входа, направление позиции. Их вместе с текущим временем и выбранным методом закрытия и будем передавать функции 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):
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_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). Вслед за объявлением этих переменных можно подключать внешний модуль, перечислять пользовательские настройки и объявлять глобальные переменные:
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 в ценах инструмента
;
В функции инициализации нет ничего примечательного — здесь назначим индексы буферов индикатора массивам, объявленным поимённо для этих буферов. Также здесь мы переведём пользовательские настройки из пунктов в цены инструмента — чтобы хоть немного сократить расход ресурсов и не делать такого преобразования много тысяч раз по ходу работы основной программы:
// перевод пунктов в цены инструмента:
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 (домовой):
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 % 86400 — 86400 * 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)));
Теперь запрограммируем основной рабочий цикл. В начале каждой итерации надо получить данные из модуля сигналов, проконтролировать результат наличие ошибок и организовать переход к следующей итерации цикла, если сигнала нет:
// получить данные из сигнального модуля:
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):
А затем разметим на истории этот трейд в развитии, если это первый бар нового дня:
if(t_Last_D1_Bar < t_Curr_D1_Bar) { // это 1й бар дня, на котором есть сигнал
t_Entry_Bar = Time[i_Current_TF_Bar]; // запомнить время начала торговли
Начнём с заливки фоном баров первого часа дня, использованных в расчётах уровней:
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; // вход был на этом баре
}
}
Если цена не достигла расчётного уровня до конца дня, перейдём к следующему шагу основного цикла:
i_Current_TF_Bar = i_Period_Bar; // бары этого дня больше нас не интересуют
continue;
}
Если же этот день ещё не завершён и судьба отложенного ордера ещё не определена, то нет смысла продолжать основной цикл программы:
После этих двух фильтров останется только один возможный вариант развития событий — отложенный ордер сработал. Найдём бар исполнения отложенного ордера и, начиная с этого бара, отрисуем уровни 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;
}
}
}
После закрытия позиции оставшиеся бары дня можно пропустить в основном цикле программы:
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;
На этом код основного цикла заканчивается. Осталось лишь организовать оповещение, если сигнал выявлен на текущем баре:
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 соответствующий блок кода для обращения к сигнальному модулю. Если результат обращения этого потребует, советник должен закрыть позицию:
// сложились условия для закрытия позиции?
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 | советник | Советник для автоматической торговли по этой ТС |





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