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

Торговая стратегия '80-20'

MetaTrader 5Торговые системы | 25 октября 2016, 15:56
17 317 0
Alexander Puzanov
Alexander Puzanov

  1. Введение
  2. Торговая система '80-20'
  3. Сигнальный модуль
  4. Индикатор для ручной торговли
  5. Советник для тестирования ТС '80-20'
  6. Тестирование стратегии на исторических данных
  7. Заключение.

Введение

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

Первая из целей этой статьи — описать создание сигнального модуля на языке MQL5, реализующего правила торговой стратегии '80-20'. Затем мы подключим этот модуль к базовому торговому советнику, созданному в предыдущей статье цикла, немного отредактировав его. Кроме того, этот же модуль без изменений мы используем при создании индикатора для ручной торговли.

Напомню, что создаваемый в этой серии статей код в первую очередь ориентирован на категорию программистов, которую можно определить как "слегка продвинутые новички". Поэтому, кроме основной задачи, программный код призван помочь перейти от процедурного программирования к объектно-ориентированному. В нём не будут создаваться классы, но будут в полном объёме использованы более простые для освоения аналоги — структуры.

Ещё одна цель статьи — создать инструменты, которые позволят проверить, насколько актуальна эта стратегия сегодня, ведь при её создании Рашке и Коннорс исходили из поведения рынка в конце прошлого века. Несколько тестов созданного советника на современных исторических данных будут приведены в конце статьи.


Торговая система '80-20'

В качестве теоретического обоснования авторы ссылаются на книгу The Taylor Trading Technique Джорджа Тейлора, а также на работы по компьютерному анализу фьючерсных рынков Стива Мура и практический опыт трейдера Дерека Джипсона. Положенную в основу ТС гипотезу кратко можно сформулировать так: если цены открытия и закрытия вчерашнего дня разнесены в противоположные области дневного диапазона, то сегодня с большой вероятностью можно ожидать разворота в сторону открытия вчерашнего дня. При этом важно, чтобы вчерашние цены открытия и закрытия находились достаточно близко к границам диапазона, а разворот начался именно сегодня, а не до закрытия вчерашнего дневного бара. Дополненный собственными поправками авторов набор правил ТС '80-20' для входа на покупку можно сформулировать так:

1. Убедитесь, что вчера рынок открылся в верхних 20% дневного диапазона, а закрылся в нижних 20% диапазона

2. Дождитесь, когда сегодняшний ценовой минимум пробьёт вчерашний минимум хотя бы на 5 тиков

3. Разместите отложенный ордер на покупку на нижней границе вчерашнего диапазона

4. Сразу после срабатывания отложенного ордера установите его начальный StopLoss у минимума этого дня

5. Используйте Трейлинг Стоп для защиты полученной прибыли

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

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

И не забудем, что ТС предназначена исключительно для внутридневной торговли — показанные в книге примеры используют графики 15-минутного таймфрейма.

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

Пятиминутный таймфрейм:

Паттерн из ТС 80-20

Результатом анализа этого паттерна должна стать установка отложенного ордера на покупку. Соответствующие торговые уровни лучше видны на минутном таймфрейме:

Паттерн по ТС 80-20: торговые уровни

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

Паттерн по ТС 80-20

Его торговые уровни (минутный таймфрейм):

Паттерн по ТС 80-20: торговые уровни

 

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

Чтобы показать пример добавления в авторскую ТС дополнительных опций, добавим расчёт уровня Take Profit. В оригинальной версии этого уровня нет, для закрытия позиции используется только трейлинг уровня Stop Loss. Take Profit сделаем зависимым от заданной пользователем минимальной величины пробоя (TS_8020_Extremum_Break) — будем умножать его на пользовательский коэффициент TS_8020_Take_Profit_Ratio.

От основной функции сигнального модуля fe_Get_Entry_Signal нам будут нужны: текущий статус сигнала, рассчитанные уровни входа и выходов (Stop Loss и Take Profit), а также границы диапазона вчерашнего дня. Все уровни мы получим по переданным функции ссылкам на переменные, а возвращённый статус сигнала будет использовать список вариантов из предыдущей статьи:

enum ENUM_ENTRY_SIGNAL {  // Список сигналов на вход
  ENTRY_BUY,              // сигнал на покупку
  ENTRY_SELL,             // сигнал на продажу
  ENTRY_NONE,             // нет сигнала
  ENTRY_UNKNOWN           // статус не определён
};

ENUM_ENTRY_SIGNAL fe_Get_Entry_Signal( // Анализ двухсвечного паттерна D1
  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-го бара паттерна (ссылка на переменную)
) {}

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

1. Размах бара (разность цен High и Low) должен быть больше, чем средний за последние XX дней (задаётся пользовательской настройкой TS_8020_D1_Average_Period)

2. Уровни открытия и закрытия бара должны относиться к противоположным 20% диапазона бара

Если эти условия будут выполнены, то для дальнейшего использования нужно запомнить цены High и Low. А так как параметры первого бара паттерна не изменятся в течение всего дня, не станем проверять их при каждом вызове функции, а запомним в статические переменные:

// пользовательские настройки
input uint  TS_8020_D1_Average_Period = 20;  // 80-20: Кол-во дней для вычисления среднего дневного диапазона
input uint  TS_8020_Extremum_Break = 50;     // 80-20: Мин. пробой вчерашнего экстремума (в пунктах)


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


// проверка 1го бара паттерна на D1:
if(se_Possible_Signal == ENTRY_UNKNOWN) { // сегодня ещё не проводилась
  st_Last_D1_Bar = t_Curr_D1_Bar; // в этот день 1-й бар больше не изменится
  
  // средний дневной диапазон
  double d_Average_Bar_Range = fd_Average_Bar_Range(TS_8020_D1_Average_Period, PERIOD_D1, t_Time);
  
  if(ma_Rates[0].high — ma_Rates[0].low <= d_Average_Bar_Range) {
    // 1й бар недостаточно велик
    se_Possible_Signal = ENTRY_NONE; // значит сегодня сигнала не будет
    return(se_Possible_Signal);
  }
  
  double d_20_Percents = 0.2 * (ma_Rates[0].high — ma_Rates[0].low); // 20% вчерашнего диапазона
  if((
      // медвежий бар:
      ma_Rates[0].open > ma_Rates[0].high — d_20_Percents // бар открылся в верхних 20%
      &&
      ma_Rates[0].close < ma_Rates[0].low + d_20_Percents // а закрылся в нижних 20%
    ) || (
      // бычий:
      ma_Rates[0].close > ma_Rates[0].high — d_20_Percents // бар закрылся в верхних 20%
      &&
      ma_Rates[0].open < ma_Rates[0].low + d_20_Percents // а открылся в нижних 20%
  )) {
    // 1-й бар соответствует условиям
    // определение направления торговли на сегодня по 1-му бару паттерна:
    se_Possible_Signal = ma_Rates[0].open > ma_Rates[0].close ? ENTRY_BUY : ENTRY_SELL;
    // уровень входа в рынок:
    sd_Entry_Level = d_Entry_Level = se_Possible_Signal == ENTRY_BUY ? ma_Rates[0].low : ma_Rates[0].high;
    // границы диапазона 1-го бара паттерна:
    sd_Range_High = d_Range_High = ma_Rates[0].high;
    sd_Range_Low = d_Range_Low = ma_Rates[0].low;
  } else {
    // уровни открытия/закрытия 1-го бара не соответствуют условиям
    se_Possible_Signal = ENTRY_NONE; // значит, сегодня сигнала не будет
    return(se_Possible_Signal);
  }
}

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

double fd_Average_Bar_Range(    // Расчёт среднего размера бара
  int i_Bars_Limit,             // сколько баров принимать во внимание
  ENUM_TIMEFRAMES e_TF = PERIOD_CURRENT,  // таймфрейм баров
  datetime t_Time = WRONG_VALUE  // с какого времени начинать расчёт
) {
  double d_Average_Range = 0; // переменная для суммирования значений
  if(i_Bars_Limit < 1) return(d_Average_Range);
  
  MqlRates ma_Rates[]; // массив для информации о барах
  
  // получение информации о барах с заданного уч-ка истории:
  if(t_Time == WRONG_VALUE) t_Time = TimeCurrent();
  int i_Price_Bars = CopyRates(_Symbol, e_TF, t_Time, i_Bars_Limit, ma_Rates);
  
  if(i_Price_Bars == WRONG_VALUE) { // обработка ошибки функции CopyRates
    if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyRates: ошибка #%u", __FUNCTION__, _LastError);
    return(d_Average_Range);
  }
  
  if(i_Price_Bars < i_Bars_Limit) { // функция CopyRates извлекла данные не в полном объёме
    if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyRates: скопировано %u баров из %u", __FUNCTION__, i_Price_Bars, i_Bars_Limit);
  }
  
  // сумма диапазонов:
  int i_Bar = i_Price_Bars;
  while(i_Bar-- > 0)
    d_Average_Range += ma_Rates[i_Bar].high — ma_Rates[i_Bar].low;
  
  // среднее значение:
  return(d_Average_Range / double(i_Price_Bars));
}

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

// проверка 2го (текущего) бара паттерна на D1:
if(se_Possible_Signal == ENTRY_BUY) {
  sd_SL = d_SL = ma_Rates[1].low; // StopLoss — на максимум сегодняшней цены
  if(TS_8020_Take_Profit_Ratio > 0) sd_TP = d_TP = d_Entry_Level + _Point * TS_8020_Extremum_Break * TS_8020_Take_Profit_Ratio; // TakeProfit
  return(
    // достаточно ли выражен пробой вниз?
    ma_Rates[1].close < ma_Rates[0].low — _Point * TS_8020_Extremum_Break ?
    ENTRY_BUY : ENTRY_NONE
  );
}

if(se_Possible_Signal == ENTRY_SELL) {
  sd_SL = d_SL = ma_Rates[1].high; // StopLoss — на минимум сегодняшней цены
  if(TS_8020_Take_Profit_Ratio > 0) sd_TP = d_TP = d_Entry_Level — _Point * TS_8020_Extremum_Break * TS_8020_Take_Profit_Ratio; // TakeProfit
  return(
    // достаточно ли выражен пробой вверх?
    ma_Rates[1].close > ma_Rates[0].high + _Point * TS_8020_Extremum_Break ?
    ENTRY_SELL : ENTRY_NONE
  );
}

Описанные выше две функции (fe_Get_Entry_Signal и fd_Average_Bar_Range) и пользовательские настройки, относящиеся к получению сигнала, сохраним в файле библиотеки mqh. Полный листинг есть в приложении к этой статье. Назовём файл Signal_80-20.mqh и поместим его в соответствующую папку (MQL5\Include\Expert\Signal) каталога данных терминала. 


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

Индикатор, как и советник, будет использовать описанный выше сигнальный модуль. Он (индикатор) должен оповестить трейдера о получении сигнала на установку отложенного ордера и сообщить расчётные уровни — уровень установки ордера, уровни Take Profit и Stop Loss. Методы оповещения пользователь может выбрать сам — это может быть стандартное всплывающее окно, сообщение на электронную почту или уведомление на мобильное устройство. Выбрать можно всё сразу или любую удобную комбинацию перечисленных опций.

Другое назначение индикатора — разметка на истории торговли по ТС '80-20'. Он будет подсвечивать дневные бары, соответствующие критериям системы, и рисовать расчётные торговые уровни. По линиям уровней будет можно оценивать, как развивалась ситуация во времени. Для большей наглядности сделаем так: при касании ценой сигнальной линии она закончится, начнётся линия отложенного ордера, а при срабатывании отложенного ордера закончится его линия и начнутся линии Take Profit и Stop Loss. Линии эти прервутся, когда цена коснется одной из них (ордер закроется). При такой разметке будет проще оценить эффективность правил торговой системы и понять, что именно можно усовершенствовать.

Начнём с объявления буферов и параметров их отображения. Во-первых, нам нужно объявить два буфера с заливкой вертикальной области (DRAW_FILLING). Один будет подсвечивать полный диапазон дневного бара предыдущего дня, другой — только внутреннюю область, чтобы отделить её от задействованных в ТС верхних и нижних 20% диапазона. Затем заявим два буфера для разноцветных сигнальной линии и линии отложенного ордера (DRAW_COLOR_LINE). Их цвет будет зависеть от направления торговли. И ещё две линии (Take Proft и Stop Loss) цвета менять не будут (DRAW_LINE) — они будут использовать те же стандартные цвета, которые им отведены в терминале. Все выбранные типы отображения, кроме простой линии, требуют по два буфера, поэтому код будет выглядеть так:

#property indicator_chart_window
#property indicator_buffers  10
#property indicator_plots    6

#property indicator_label1  "1й бар паттерна"
#property indicator_type1   DRAW_FILLING
#property indicator_color1  clrDeepPink, clrDodgerBlue
#property indicator_width1  1

#property indicator_label2  "1й бар паттерна"
#property indicator_type2   DRAW_FILLING
#property indicator_color2  clrDeepPink, clrDodgerBlue
#property indicator_width2  1

#property indicator_label3  "Сигнальный уровень"
#property indicator_type3   DRAW_COLOR_LINE
#property indicator_style3  STYLE_SOLID
#property indicator_color3  clrDeepPink, clrDodgerBlue
#property indicator_width3  2

#property indicator_label4  "Уровень входа"
#property indicator_type4   DRAW_COLOR_LINE
#property indicator_style4  STYLE_DASHDOT
#property indicator_color4  clrDeepPink, clrDodgerBlue
#property indicator_width4  2

#property indicator_label5  "Stop Loss"
#property indicator_type5   DRAW_LINE
#property indicator_style5  STYLE_DASHDOTDOT
#property indicator_color5  clrCrimson
#property indicator_width5  1

#property indicator_label6  "Take Profit"
#property indicator_type6   DRAW_LINE
#property indicator_style6  STYLE_DASHDOTDOT
#property indicator_color6  clrLime
#property indicator_width6  1

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

#include <Expert\Signal\Signal_80-20.mqh> // сигнальный модуль ТС '80-20'

input bool    Show_Outer = true;      // 1-й бар паттерна: Показывать полный диапазон?
input bool    Show_Inner = 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  Bars_Limit = 2000;  // Глубина разметки истории (в барах текущего ТФ)



ENUM_LOG_LEVEL  Log_Level = LOG_LEVEL_NONE;  // Режим протоколирования
double
  buff_1st_Bar_Outer[], buff_1st_Bar_Outer_Zero[], // буферы для отрисовки полного диапазона 1-го бара паттерна
  buff_1st_Bar_Inner[], buff_1st_Bar_Inner_Zero[], // буферы для отрисовки внутренних 60% 1-го бара паттерна
  buff_Signal[], buff_Signal_Color[], // буферы сигнальной линии
  buff_Entry[], buff_Entry_Color[], // буферы линии отложенного ордера
  buff_SL[], buff_TP[], // буферы линий StopLoss и TakeProfit
  gd_Extremum_Break = 0 // TS_8020_Extremum_Break в ценах инструмента
;
int
  gi_D1_Average_Period = 1, // корректное значение для TS_8020_D1_Average_Period
  gi_Min_Bars = WRONG_VALUE // минимальное обязательное кол-во баров для пересчёта
;



int OnInit() {
  // проверка введённого параметра TS_8020_D1_Average_Period:
  gi_D1_Average_Period = int(fmin(1, TS_8020_D1_Average_Period));
  // перевод пунктов в цены инструмента:
  gd_Extremum_Break = TS_8020_Extremum_Break * _Point;
  // минимальное обязательное кол-во баров для пересчёта = кол-ву баров текущего ТФ в сутках
  gi_Min_Bars = int(86400 / PeriodSeconds());
  
  // назначение буферов индикатора:
  
  // прямоугольник полного диапазона 1-го бара
  SetIndexBuffer(0, buff_1st_Bar_Outer, INDICATOR_DATA);
    PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, 0);
  SetIndexBuffer(1, buff_1st_Bar_Outer_Zero, INDICATOR_DATA);
  
  // прямоугольник внутренней области 1-го бара
  SetIndexBuffer(2, buff_1st_Bar_Inner, INDICATOR_DATA);
    PlotIndexSetDouble(1, PLOT_EMPTY_VALUE, 0);
  SetIndexBuffer(3, buff_1st_Bar_Inner_Zero, INDICATOR_DATA);
  
  // сигнальная линия
  SetIndexBuffer(4, buff_Signal, INDICATOR_DATA);
    PlotIndexSetDouble(2, PLOT_EMPTY_VALUE, 0);
  SetIndexBuffer(5, buff_Signal_Color, INDICATOR_COLOR_INDEX);
  
  // линия установки отложенного ордера
  SetIndexBuffer(6, buff_Entry, INDICATOR_DATA);
    PlotIndexSetDouble(3, PLOT_EMPTY_VALUE, 0);
  SetIndexBuffer(7, buff_Entry_Color, INDICATOR_COLOR_INDEX);
  
  // линия SL
  SetIndexBuffer(8, buff_SL, INDICATOR_DATA);
    PlotIndexSetDouble(4, PLOT_EMPTY_VALUE, 0);
  
  // линия TP
  SetIndexBuffer(9, buff_TP, INDICATOR_DATA);
    PlotIndexSetDouble(5, PLOT_EMPTY_VALUE, 0);
  
  IndicatorSetInteger(INDICATOR_DIGITS, _Digits);
  IndicatorSetString(INDICATOR_SHORTNAME, "ТС 80-20");
  
  return(INIT_SUCCEEDED);
}

В штатной функции OnCalculate поместим код основной программы — организуем цикл, который будет перебирать бары текущего таймфрейма в направлении из прошлого в будущее и проверять их на наличие сигнала с помощью функции из сигнального модуля. Предварительно объявим и инициализируем начальными значениями необходимые переменные. Самый старый бар цикла для первого расчёта определим с учётом заданного пользователем ограничения глубины истории (Bars_Limit). Для последующих же вызовов пересчитывать мы будем не один лишь последний бар, а все бары текущего дня, ведь двухбарный паттерн на самом деле принадлежит графику D1, независимо от текущего таймфрейма.

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

- флаг первого запуска функции OnCalculate;

- необнуляемый при изменении контрольной суммы счётчик обсчитанных баров;

- флаг изменения контрольной суммы;

- флаг начала нового бара;

- время начала текущего бара.

Объединяющая все эти данные структура будет объявлена на глобальном уровне и сможет собирать или предоставлять информацию в/из любых штатных или пользовательских функций. Такой программной сущности вполне подойдёт имя 'Домовой' (Brownie). Поместить её можно в конце кода индикатора. Там же объявим один глобальный объект-структуру типа с именем go_Brownie:

struct BROWNIE {                // Домовой: структура для хранения и обработки данных на глобальном уровне
  datetime  t_Last_Bar_Time;    // время последнего обрабатывавшегося бара
  int        i_Prew_Calculated; // кол-во посчитанных баров
  bool      b_First_Run;        // флаг первого запуска
  bool      b_History_Updated;  // флаг обновления истории
  bool      b_Is_New_Bar;       // флаг открытия нового бара
  
  BROWNIE() { // конструктор
    // значения по умолчанию:
    t_Last_Bar_Time = 0;
    i_Prew_Calculated = WRONG_VALUE;
    b_First_Run = b_Is_New_Bar = true;
    b_History_Updated = false;
  }
  
  void f_Reset(bool b_Reset_First_Run = true) { // обнуление переменных
    // значения по умолчанию:
    t_Last_Bar_Time = 0;
    i_Prew_Calculated = WRONG_VALUE;
    if(b_Reset_First_Run) b_First_Run = true; // обнуление, если есть разрешение
    b_Is_New_Bar = true;
    b_History_Updated = false;
  }
  
  void f_Update(int i_New_Prew_Calculated = WRONG_VALUE) { // обновление переменных
    // флаг первого вызова штатной функции OnCalculate
    if(b_First_Run && i_Prew_Calculated > 0) b_First_Run = false;
    
    // новый бар?
    datetime t_This_Bar_Time = TimeCurrent() - TimeCurrent() % PeriodSeconds();
    b_Is_New_Bar = t_Last_Bar_Time == t_This_Bar_Time;
    
    // обновить время текущего бара?
    if(b_Is_New_Bar) t_Last_Bar_Time = t_This_Bar_Time;
    
    if(i_New_Prew_Calculated > -1) {
      // есть какие-то изменения в истории?
      b_History_Updated = i_New_Prew_Calculated == 0 && i_Prew_Calculated > WRONG_VALUE;
      
      // prew_calculated использовать, если 1-й вызов OnCalculate
      if(i_Prew_Calculated == WRONG_VALUE) i_Prew_Calculated = i_New_Prew_Calculated;
      // или если не было обновления истории
      else if(i_New_Prew_Calculated > 0) i_Prew_Calculated = i_New_Prew_Calculated;
    }
  }
};
BROWNIE go_Brownie;

Предусмотрим информирование 'Домового' и о событии деинициализации индикатора:

void OnDeinit(const int reason) {
  go_Brownie.f_Reset(); // оповестить Домового
}

При необходимости коллекцию информации, хранимой 'Домовым' можно расширить, если пользовательские функции или классы будут нуждаться, например в ценах, объёмах или величине спреда текущего бара (Open, High, Low, Close, tick_volume, volume, spread). Взять готовые данные из функции OnCalculate и передать их через 'Домового' удобнее, чем использовать функции копирования таймсерий (CopyOpen, CopyHigh и т.д. или CopyRates) — это будет экономить ресурсы процессора и избавит от необходимости организовывать обработку ошибок этих функций языка.

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

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

int
  i_Period_Bar = 0, // вспомогательный счётчик
  i_Current_TF_Bar = rates_total - int(Bars_Limit) // индекс бара начала цикла текущего ТФ
;
static datetime st_Last_D1_Bar = 0; // время последнего из обработанной пары баров D1 (2-го бара паттерна)
static int si_1st_Bar_of_Day = 0; // индекс первого бара текущего дня

if(go_Brownie.b_First_Run) { // если это 1й запуск
  // очистить буфера при переинициализации:
  ArrayInitialize(buff_1st_Bar_Inner, 0); ArrayInitialize(buff_1st_Bar_Inner_Zero, 0);
  ArrayInitialize(buff_1st_Bar_Outer, 0); ArrayInitialize(buff_1st_Bar_Outer_Zero, 0);
  ArrayInitialize(buff_Entry, 0); ArrayInitialize(buff_Entry_Color, 0);
  ArrayInitialize(buff_Signal, 0); ArrayInitialize(buff_Signal_Color, 0);
  ArrayInitialize(buff_TP, 0);
  ArrayInitialize(buff_SL, 0);
  st_Last_D1_Bar = 0;
  si_1st_Bar_of_Day = 0;
} else { // это не 1й запуск
  datetime t_Time = TimeCurrent();
  // минимальная глубина пересчёта - с прошлого дня:
  i_Current_TF_Bar = rates_total - Bars(_Symbol, PERIOD_CURRENT, t_Time - t_Time % 86400, t_Time) - 1;
}
ENUM_ENTRY_SIGNAL e_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_D1_Bar_To_Fill = 0 // время бара D1, который надо закрасить (1-го бара паттерна)
;

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

while(++i_Current_TF_Bar < rates_total && !IsStopped()) { // перебор баров текущего ТФ
  // здесь будет основной цикл программы
}

При переборе баров текущего таймфрейма будем проверять наличие сигнала:

e_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_Signal > 1) continue; // в день, к которому принадлежит этот бар, сигнала нет

Если сигнал есть и это первый бар нового дня, то нужно организовать заливку диапазона предыдущего дневного бара. Флагом будет значение переменной t_D1_Bar_To_Fill типа datetime — если ей присвоено значение WRONG_VALUE, то на этом баре заливка не требуется. На этом же первом баре должна начинаться и сигнальная линия, но для лучшего восприятия разметки продлим её до последнего бара предыдущего дня. Так как расчёты сигнального уровня и цвета линий и закрашенных областей для бычьего и медвежьего бара различаются, сделаем два аналогичных друг другу блока:

t_Curr_D1_Bar = Time[i_Current_TF_Bar] — Time[i_Current_TF_Bar] % 86400; // начало дня, к которому принадлежит этот бар
if(st_Last_D1_Bar < t_Curr_D1_Bar) { // это бар нового дня
  t_D1_Bar_To_Fill = Time[i_Current_TF_Bar — 1] — Time[i_Current_TF_Bar — 1] % 86400;
  si_1st_Bar_of_Day = i_Current_TF_Bar;
}
else t_D1_Bar_To_Fill = WRONG_VALUE; // бар старого дня, новая заливка не требуется
st_Last_D1_Bar = t_Curr_D1_Bar; // запомнить

if(t_D1_Bar_To_Fill != WRONG_VALUE) { // новый бар D1
  // Заливка бара D1 предыдущего дня:
  i_Period_Bar = i_Current_TF_Bar;
  if(d_Entry_Level < d_Range_High) { // медвежий бар D1
    if(Show_Outer) while(--i_Period_Bar > 0) { // полный диапазон
      if(Time[i_Period_Bar] < t_D1_Bar_To_Fill) break;
      buff_1st_Bar_Outer_Zero[i_Period_Bar] = d_Range_Low;
      buff_1st_Bar_Outer[i_Period_Bar] = d_Range_High;
    }
    if(Show_Inner) { // внутренняя область
      i_Period_Bar = i_Current_TF_Bar;
      while(--i_Period_Bar > 0) {
        if(Time[i_Period_Bar] < t_D1_Bar_To_Fill) break;
        buff_1st_Bar_Inner_Zero[i_Period_Bar] = d_Range_Low + 0.2 * (d_Range_High — d_Range_Low);
        buff_1st_Bar_Inner[i_Period_Bar] = d_Range_High — 0.2 * (d_Range_High — d_Range_Low);
      }
    }
    // начало сигнальной линии — с последнего бара предыдущего дня
    buff_Signal[i_Current_TF_Bar] = buff_Signal[i_Current_TF_Bar — 1] = d_Range_Low — gd_Extremum_Break;
    buff_Signal_Color[i_Current_TF_Bar] = buff_Signal_Color[i_Current_TF_Bar — 1] = 0;
  } else { // бычий бар D1
    if(Show_Outer) while(--i_Period_Bar > 0) { // полный диапазон
      if(Time[i_Period_Bar] < t_D1_Bar_To_Fill) break;
      buff_1st_Bar_Outer_Zero[i_Period_Bar] = d_Range_High;
      buff_1st_Bar_Outer[i_Period_Bar] = d_Range_Low;
    }
    if(Show_Inner) { // внутренняя область
      i_Period_Bar = i_Current_TF_Bar;
      while(--i_Period_Bar > 0) {
        if(Time[i_Period_Bar] < t_D1_Bar_To_Fill) break;
        buff_1st_Bar_Inner_Zero[i_Period_Bar] = d_Range_High — 0.2 * (d_Range_High — d_Range_Low);
        buff_1st_Bar_Inner[i_Period_Bar] = d_Range_Low + 0.2 * (d_Range_High — d_Range_Low);
      }
    }
    // начало сигнальной линии — с последнего бара предыдущего дня
    buff_Signal[i_Current_TF_Bar] = buff_Signal[i_Current_TF_Bar — 1] = d_Range_High + gd_Extremum_Break;
    buff_Signal_Color[i_Current_TF_Bar] = buff_Signal_Color[i_Current_TF_Bar — 1] = 1;
  }
} else continue;

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

// Сигнальная линия до пересёкшего её бара:
i_Period_Bar = i_Current_TF_Bar;
if(d_Entry_Level < d_Range_High) { // медвежий бар D1
  while(++i_Period_Bar < rates_total) {
    if(Time[i_Period_Bar] > t_Curr_D1_Bar + 86399) break;
    buff_Signal[i_Period_Bar] = d_Range_Low — gd_Extremum_Break;
    buff_Signal_Color[i_Period_Bar] = 0;
    if(d_Range_Low — gd_Extremum_Break >= Low[i_Period_Bar]) break;
  }
} else { // бычий бар D1
  while(++i_Period_Bar < rates_total) {
    if(Time[i_Period_Bar] > t_Curr_D1_Bar + 86399) break;
    buff_Signal[i_Period_Bar] = d_Range_High + gd_Extremum_Break;
    buff_Signal_Color[i_Period_Bar] = 1;
    if(d_Range_High + gd_Extremum_Break <= High[i_Period_Bar]) break;
  }
}

// Линия входа до пересёкшего её бара:
if(d_Entry_Level < d_Range_High) { // медвежий бар D1
  while(++i_Period_Bar < rates_total) {
    if(Time[i_Period_Bar] > t_Curr_D1_Bar + 86399) break;
    buff_Entry[i_Period_Bar] = d_Range_Low;
    buff_Entry_Color[i_Period_Bar] = 0;
    if(d_Range_Low <= High[i_Period_Bar]) {
      if(buff_Entry[i_Period_Bar — 1] == 0.) {
        // начало и конец на одном баре, продлим на 1 бар в прошлое
        buff_Entry[i_Period_Bar — 1] = d_Range_Low;
        buff_Entry_Color[i_Period_Bar — 1] = 0;
      }
      break;
    }
  }
} else { // бычий бар D1
  while(++i_Period_Bar < rates_total) {
    if(Time[i_Period_Bar] > t_Curr_D1_Bar + 86399) break;
    buff_Entry[i_Period_Bar] = d_Range_High;
    buff_Entry_Color[i_Period_Bar] = 1;
    if(d_Range_High >= Low[i_Period_Bar]) {
      if(buff_Entry[i_Period_Bar — 1] == 0.) {
        // начало и конец на одном баре, продлим на 1 бар в прошлое
        buff_Entry[i_Period_Bar — 1] = d_Range_High;
        buff_Entry_Color[i_Period_Bar — 1] = 1;
      }
      break;
    }
  }
}

// Линии TP и SL до бара, пересёкшего одну из них:
if(d_Entry_Level < d_Range_High) { // медвежий бар D1
  // SL равен минимуму с начала дня:
  d_SL = Low[ArrayMinimum(Low, si_1st_Bar_of_Day, i_Period_Bar — si_1st_Bar_of_Day)];
  
  while(++i_Period_Bar < rates_total) {
    if(Time[i_Period_Bar] > t_Curr_D1_Bar + 86399) break;
    buff_SL[i_Period_Bar] = d_SL;
    buff_TP[i_Period_Bar] = d_TP;
    if(d_TP <= High[i_Period_Bar] || d_SL >= Low[i_Period_Bar]) {
      if(buff_SL[i_Period_Bar — 1] == 0.) {
        // начало и конец на одном баре, продлим на 1 бар в прошлое
        buff_SL[i_Period_Bar — 1] = d_SL;
        buff_TP[i_Period_Bar — 1] = d_TP;
      }
      break;
    }
  }
} else { // бычий бар D1
  // SL равен максимуму с начала дня:
  d_SL = High[ArrayMaximum(High, si_1st_Bar_of_Day, i_Period_Bar — si_1st_Bar_of_Day)];
  
  while(++i_Period_Bar < rates_total) {
    if(Time[i_Period_Bar] > t_Curr_D1_Bar + 86399) break;
    buff_SL[i_Period_Bar] = d_SL;
    buff_TP[i_Period_Bar] = d_TP;
    if(d_SL <= High[i_Period_Bar] || d_TP >= Low[i_Period_Bar]) {
      if(buff_SL[i_Period_Bar — 1] == 0.) {
        // начало и конец на одном баре, продлим на 1 бар в прошлое
        buff_SL[i_Period_Bar — 1] = d_SL;
        buff_TP[i_Period_Bar — 1] = d_TP;
      }
      break;
    }
  }
}

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

void f_Do_Alert(                  // Функция рассылки сигналов и оповещений
  string  s_Message,              // текст для алерта
  bool    b_Alert = true,         // показывать всплывающее окно?
  bool    b_Sound = false,        // проигрывать звуковой файл?
  bool    b_Email = false,        // отправлять eMail сообщение?
  bool    b_Notification = false, // отправлять push-уведомление?
  string  s_Email_Subject = "",   // тема для eMail сообщения
  string  s_Sound = "alert.wav"   // звуковой файл
) {
  static string ss_Prev_Message = "была тишина"; // текст предыдущего алерта
  static datetime st_Prev_Time; // время бара предыдущего алерта
  datetime t_This_Bar_Time = TimeCurrent() — PeriodSeconds() % PeriodSeconds(); // время текушего бара
  
  if(ss_Prev_Message != s_Message || st_Prev_Time != t_This_Bar_Time) {
    // алерт другой и/или 1-й на этом баре
    
    // запомнить:
    ss_Prev_Message = s_Message;
    st_Prev_Time = t_This_Bar_Time;
    
    // сформировать строку сообщения:
    s_Message = StringFormat("%s | %s | %s | %s",
      TimeToString(TimeLocal(), TIME_SECONDS), // локальное время
      _Symbol, // символ
      StringSubstr(EnumToString(ENUM_TIMEFRAMES(_Period)), 7), // ТФ
      s_Message // сообщение
    );
    
    // подать сигнал оповещения:
    if(b_Alert) Alert(s_Message);
    if(b_Email) SendMail(s_Email_Subject + " " + _Symbol, s_Message);
    if(b_Notification) SendNotification(s_Message);
    if(b_Sound) PlaySound(s_Sound);
  }
}

Код проверки необходимости вызова этой функции и формирования текста сообщения для неё, размещённый в теле программы, перед завершением обработчика события OnCalculate:

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

if(Alert_Popup + Alert_Email + Alert_Push == 0) return(rates_total); // всё отключено
if(buff_Signal[i_Period_Bar] == 0) return(rates_total); // уже или ещё нечего ловить
if(
  buff_Signal[i_Period_Bar] > High[i_Period_Bar]
  ||
  buff_Signal[i_Period_Bar] < Low[i_Period_Bar]
) return(rates_total); // нет касания сигнальной линии

// текст сообщения:
string s_Message = StringFormat("TS 80-20: нужен %s @ %s, TP: %s, SL: %s",
  buff_Signal_Color[i_Period_Bar] > 0 ? "BuyStop" : "SellStop",
  DoubleToString(d_Entry_Level, _Digits),
  DoubleToString(d_TP, _Digits),
  DoubleToString(d_SL, _Digits)
);
// оповещение:
f_Do_Alert(s_Message, Alert_Popup, false, Alert_Email, Alert_Push, Alert_Email_Subj);

return(rates_total); // завершение работы OnCalculate

Весь исходный код индикатора в сборе есть в прикреплённых файлах, его имя — TS_80-20.mq5. Что касается его использования — лучше всего видна разметка торговли по этой системе на минутных графиках.

Есть одно существенное примечание к этой разметке — индикатор использует данные баров, а не последовательности тиков внутри баров. Т.е. если на одном баре цена пересекала несколько линий разметки (например, линии Take Profit и Stop Loss), не всегда можно определить, которая из них была пересечена первой. Другая погрешность связана с тем, что бары начала и окончания линии не могут совпадать, иначе линии из буфера типа DRAW_LINE и DRAW_COLOR_LINE будут просто не видны пользователю. Эти особенности делают разметку не стопроцентно точной, но всё же весьма наглядной.


Советник для тестирования ТС '80-20'

Базовый советник для тестирования стратегий из книги Street Smarts: High Probability Short-Term Trading Strategies подробно описан в первой статье. Внесём в него два существенных изменения. Первое связано с тем, что сигнальный модуль будет использоваться и в индикаторе тоже, а значит, рационально будет вынести в него расчёт торговых уровней. Это мы уже сделали выше — функция fe_Get_Entry_Signal, помимо статуса сигнала, возвращает уровни установки ордера, Stop Loss и Take Profit. Поэтому уберём из предыдущей версии советника соответствующую часть кода, добавим переменные для приёма уровней из функции и отредактируем сам вызов этой функции. Я не буду приводить здесь листинги старого и нового блоков кода, вы можете посмотреть их в приложенном файле (строки со 128 по 141).

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

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

input bool  One_Trade = false;    // Одна позиция в день
input uint  Magic_Number = 2016;  // Идентификатор советника (Magic Number)

В блок определения глобальных переменных программы добавим объявление нужных для реализации опции 'один вход в день' переменных. В функции OnInit проинициализируем их:

string
  gs_Prefix // идентификатор имён (супер)глобальных переменных
;
bool
  gb_Position_Today = false,
  gb_Pending_Today = false
;

int OnInit() {

...

  // Создание префикса имён (супер)глобальных переменных:
  gs_Prefix = StringFormat("SSB %s %u %s", _Symbol, Magic_Number, MQLInfoInteger(MQL_TESTER) ? "t " : "");
  
  // Работал ли робот сегодня с рыночными или отложенными ордерами?
  gb_Position_Today = int(GlobalVariableGet(gs_Prefix + "Last_Position_Date")) == TimeCurrent() — TimeCurrent() % 86400;
  gb_Pending_Today = int(GlobalVariableGet(gs_Prefix + "Last_Pending_Date")) == TimeCurrent() — TimeCurrent() % 86400;

...
}

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

if(i_Try != -10) { // не удалось установить отложенный ордер
  if(Log_Level > LOG_LEVEL_NONE) Print("Ошибка установки отложенного ордера");
  // расстояние от текущей цены недостаточно :(
  if(Log_Level > LOG_LEVEL_ERR)
    PrintFormat("Нельзя выставить отложенный ордер на уровень %s. Bid: %s Ask: %s StopLevel: %s",
      DoubleToString(d_Entry_Level, _Digits),
      DoubleToString(go_Tick.bid, _Digits),
      DoubleToString(go_Tick.ask, _Digits),
      DoubleToString(gd_Stop_Level, _Digits)
    );
} else { // удалось
  // обновить флаг:
  GlobalVariableSet( // в глобальных переменных терминала
    gs_Prefix + "Last_Pending_Date",
    TimeCurrent() — TimeCurrent() % 86400
  );
  gb_Pending_Today = true; // в глобальных переменных программы
}

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

if(PositionSelect(_Symbol)) { // есть открытая позиция
        if(PositionGetDouble(POSITION_SL) == 0.) { // новая позиция
                
                if(!gb_Position_Today) { // это 1я позиция сегодня
                        // обновить флаг:
                        GlobalVariableSet( // в глобальных переменных терминала
                                gs_Prefix + "Last_Position_Date",
                                TimeCurrent() — TimeCurrent() % 86400
                        );
                        gb_Position_Today = true; // в глобальных переменных программы
                }
...

Других существенных изменений в коде предыдущей версии советника нет. Исходный код новой версии в окончательном виде есть в приложении к статье.

 

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

Авторы торговой системы в качестве подтверждения её жизнеспособности приводят паттерны на графиках конца прошлого века, а нам нужно проверить её актуальность в условиях современного рынка. Для тестирования я взял наиболее популярную на рынке форекс пару EURUSD, а также более волатильную USDJPY и один из металлов — XAUUSD. Указанные Рашке и Коннорсом отступы я увеличил в 10 раз, так как в те времена использовались четырёхзначные котировки, а я тестировал советник на пятизначных. В отсутствие каких-либо авторских указаний относительно параметров трала я выбрал те, что показались наиболее адекватными дневному таймфрейму и волатильности инструмента. Это же относится и к добавленному к оригинальным правилам алгоритму расчёта Take Profit — коэффициент для его расчёта был выбран произвольно, без глубокой оптимизации.

График изменения баланса при тестировании на пятилетней истории EURUSD с оригинальными правилами (без Take Profit):

EURUSD D1 5 лет

С теми же настройками и добавлением Take Profit:

EURUSD D1 5 лет

График изменения баланса при тестировании оригинальных правил на пятилетней истории USDJPY:

USDJPY D1 5 лет

Тот же инструмент и таймфрейм с теми же настройками, но с добавлением Take Profit:

USDJPY D1 5 лет

Оригинальные правила на дневных котировках золота за последние 4 года показывают такой график изменения баланса:

XAUUSD D1 4 года

Полную информацию об использованных в каждом тесте настройках робота можно узнать в приложенном к статье архиву — в нём есть полные отчёты каждого теста. 


Заключение

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


Прикрепленные файлы |
Reports.zip (597.76 KB)
MQL5.zip (124.99 KB)
LifeHack для трейдера: Сравнительный отчет нескольких тестирований LifeHack для трейдера: Сравнительный отчет нескольких тестирований
В статье рассматривается одновременный запуск тестирования советника сразу на четырёх разных символах. Итоговое сравнение четырёх отчётов тестирования приводится в одной таблице, как при выборе товаров в интернет-магазинах. Дополнительным бонусом идут автоматически создаваемые графики распределений для каждого символа.
Универсальный Зигзаг Универсальный Зигзаг
Зигзаг — один из самых популярных индикаторов среди пользователей MetaTrader 5. В статье были проанализированы возможности создания различных вариантов Зигзага. В результате мы получаем универсальный индикатор с широкими возможностями для расширения функциональности, который удобно использовать при разработке торговых советников и других индикаторов.
Статистические распределения в виде гистограмм без индикаторных буферов и массивов Статистические распределения в виде гистограмм без индикаторных буферов и массивов
В статье рассматривается возможность создания гистограмм статистических распределений характеристик рынка с использованием графической памяти, то есть без использования индикаторных буферов и массивов. Приведены подробные примеры построения таких гистограмм и показан так называемый "скрытый" функционал графических объектов языка MQL5.
Графические интерфейсы X: Элемент "Стандартный график" (build 4) Графические интерфейсы X: Элемент "Стандартный график" (build 4)
На этот раз мы рассмотрим такой элемент графического интерфейса, как Стандартный график. С его помощью можно будет создавать массивы объектов-графиков с возможностью синхронизированной горизонтальной прокрутки. Кроме этого, продолжим оптимизировать код библиотеки для уменьшения потребления ресурсов процессора.