Понравилась статья?
Поставьте ссылку на нее -
пусть другие почитают
Используй новые возможности MetaTrader 5

Набор инструментов для ручной разметки графиков и торговли (Часть III). Оптимизация и новые инструменты

3 ноября 2021, 08:43
Oleh Fedorov
0
889

Введение

В предыдущих статьях (1, 2) я описал библиотеку Shortcuts и показал пример использования этой библиотеки в виде советника. В какой-то степени библиотека похожа на живой организм. Она, родившись, "выходит в свет" и встречается со средой, где ей предстоит "жить". Среда переменчива и диктует некие законы. Один из них: "Развивайся" :-) И приходится развиваться, чтобы соответствовать... В этой статье представлены некоторые результаты данного процесса.

Напомню, библиотека состоит из пяти файлов.

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

В файл GlobalVariables.mqh вынесены все глобальные переменные. В основном, это настройки, позволяющие задать цвета, толщину линий, коэффициенты удлинения разных прямых - ну, и т.д.

Файл Mouse.mqh содержит описание класса для движения мыши. Там хранятся текущие координаты курсора — как в пикселах, так и в координатах "цена-время", текущий номер бара.

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

И файл Graphics.mqh занимается, собственно, рисованием, опираясь на данные остальных файлов, и именно его функции в основном вызывает файл Shortcuts.mqh.

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

Данная реализация демонстрирует, как можно использовать код библиотеки в индикаторе.


Оптимизация быстродействия библиотеки

В данном случае изменения минимальны.

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

Однако предназначение советника — торговать, а данный программный продукт этого не делает. К тому же, когда на графике стоИт индикатор, поставить другой советник гораздо проще. Посему в какой-то момент было решено, что иникатору быть, и встал вопрос быстродействия. Особенно это важно, если у пользователя открыто множество окон — если, скажем, у пользователя открыто 40 вкладок (далеко не предел, в общем-то), то даже обработать проверки нажатия клавиш становится проблемой, если эти нажатия  обрабатывают сразу все графики.

И вот мне пришла в голову светлая мысль: а зачем, собственно? Если работает только активное окно, то и проверки должны быть только в нём.

Код очень прост.

/* Shortcuts.mqh */

void CShortcuts::OnChartEvent(
  const int id,
  const long &lparam,
  const double &dparam,
  const string &sparam
)
 {
 //...

  if(ChartGetInteger(0,CHART_BRING_TO_TOP)==false)
   {
    return;
   }
 
 //...

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

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

Тут стоит учитывать еще и назначение самого приложения.

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

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

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

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


Рефакторинг кода: управление связанностью

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

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

Неправильно это... Неудобно.

Именно поэтому я решил провести небольшую модификацию кода. Все глобальные переменные, естественно, остались. Куда им деваться, если это — настройки?

Но вот в основных классах были добавлены приватные поля, содержащие копии этих переменных. Чтобы эти значения сохранить, необходимо добавить специальные "публичные" (общедоступные) функции. Чтобы прочесть — тоже.

Выглядит это примерно так:

private:
  /* Fields */
  //---
  static int          m_TrendLengthCoefficient;

public:
  /* Methods */
  //---
  static int          TrendLengthCoefficient(void) {return m_TrendLengthCoefficient;}
  //---
  static void         TrendLengthCoefficient(int _coefficient) {m_TrendLengthCoefficient=_coefficient;}

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

Однако выгода есть. Во-первых, как уже сказал, класс становится независимым от внешних файлов. Если кто-то хочет использовать этот класс, он может установить эти переменные так, как ему угодно, и только те из них, которые действительно необходимы.

Во-вторых, такие переменные можно менять в процессе выполнения. Например, кто-то хочет написать функцию, строящую веер линий из одной точки. Каждая линия в 2 раза длиннее предыдущей, и расходятся они под разными углами. Как это можно сделать? Используя класс CUtilites в текущей реализации — просто перед каждым рисованием задавать вот этот параметр, описанный для примера — TrendLengthCoefficient, расположив начальные точки в одних координатах, а конечные — по некоторой окружности произвольного радиуса.

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

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

И поэтому в текущей версии библиотеки всё, до чего дотянулся, я переписал именно в этом стиле, и теперь могу использовать файл утилит "как есть" в любом проекте, в котором захочу.

Класс мыши исходно инкапсулировал в себе всё своё, поэтому исправлять его не пришлось. Класс рисования бессмысленен без утилит, однако все внешние настройки в нём приведены в ту же форму полей.

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


Инструмент "Перекрестье"

В прошлой версии библиотеки при рисовании трендовой линии в её конечной точке рисовалось перекрестье для обозначения уровней времени и цены. Для этого я использовал две простые линии — вертикаль и горизонталь. Однако для того, чтобы обозначить подобным образом произвольную точку графика, мне было необходимо нажать две клавиши: H и I. Иногда это удобно, иногда хочется сокращения работы... Поэтому появился инструмент "Перекрестье".

Работает инструмент аналогично всему, что уже известно. Подвели курсор к нужному месту, нажали клавишу X — и получили красивенький ровненький крестик из вертикальной и горизонтальной линий. Ниже приведен код функции.


/* Graphics.mqh */

//+------------------------------------------------------------------+
//| Рисует перекрестье в заданных координатах. Если координаты       |
//|   не заданы, используются координаты указателя мыши.             |
//+------------------------------------------------------------------+
//| Параметры:                                                       |
//|   datetime _time - время перекрестья                             |
//|   double _price - ценовой уровень                                |
//+------------------------------------------------------------------+
void CGraphics::DrawCross(datetime _time=-1,double _price=-1)
 {
  datetime time;
  double price;
//---
  if(_time==-1)
   {
    time=CMouse::Time();
   }
  else
   {
    time=_time;
   }

  if(_price==-1)
   {
    price=CMouse::Price();
   }
  else
   {
    price=NormalizeDouble(_price,Digits());
   }
  DrawSimple(OBJ_HLINE,time,price);
  DrawSimple(OBJ_VLINE,time,price);
  
 }

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

Дальше просто происходит рисование с помощью функции, описанной во второй статье.

И чтобы эта функция заработала именно так, как нужно, её нужно вызвать из файла Shortcuts.mqh при наступлении определённого события - нажатия клавиши X.

/* GlobalVariables.mqh */
  
  // ...
  
  input string   Cross_Key="X";                       // Крест в месте клика мышью
  
  // ...
  /* Shortcuts.mqh */
  
  void CShortcuts::OnChartEvent( /* ... */ )
    switch(id)
     {
       case CHARTEVENT_KEYDOWN:
       
       // ... 
       
       //--- Нарисовать перекрестье
       if(CUtilites::GetCurrentOperationChar(Cross_Key) == lparam)
        {
         m_graphics.DrawCross();
        }
     }

Инструмент "Трендовая по произвольным вершинам"

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

Пример работы этой команды показан на анимации ниже.

Пример рисования линии по произволльным вершинам

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

Рисование происходит в два этапа. На первом этапе нужно нажать клавишу Q. Она активизирует режим рисования произвольной линии и маркирует первую точку, чтобы было понятно, что команда сработала.

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

Если первая вершина выбрана правильно, то следующая точка выбирается простым кликом вблизи второго экстремума. В этом случае, если всё сработало, маркер больше не нужен — соответственно, он удаляется, зато рисуется собственно линия тренда.

Параметры "произвольной" линии независимы от режима "T", поэтому можно настроить, скажем, что "Т" рисует жирную линию, толщиной четыре пиксела, в четыре раза длиннее, чем интервал между вершинами, а "Q" — тонкая и длиннее только в два раза...

Как обычно, код разнесён по нескольким файлам.

Начнём с конца, то есть с момента отработки события CHARTEVENT_KEYDOWN:

/* Shortcuts.mqh */

void CShortcuts::OnChartEvent(
  const int id,
  const long &lparam,
  const double &dparam,
  const string &sparam
)
 {
   //...
   
   switch(id)
   {
   
   //...
   
     case CHARTEVENT_KEYDOWN:
      if(CUtilites::GetCurrentOperationChar(Free_Line_Key) == lparam)
       {
        m_graphics.ToggleFreeLineMode();
        if(m_graphics.IsFreeLineMode()){
          m_graphics.DrawFreeLine(CMouse::Bar(),CMouse::Above());
        }
       } 
    
    //...

Если программа определяет, что нажата клавиша Q (буква хранится во внешней переменной Free_Line_Key), то переключает режим рисования. И если после переключения режима оказывается, что режим включен, отдаётся команда выполнить функцию рисования линии.

Клик обрабатывается в событии

/* Shortcuts.mqh */

        //...
        
    case CHARTEVENT_CLICK:
        ChartClick_Handler();
      break;
      
      //...
      
}

/+------------------------------------------------------------------+
//| Обработка клика на свободном поле графика                        |
//+------------------------------------------------------------------+
void CShortcuts::ChartClick_Handler()
 {
  
//---
  if(m_graphics.IsFreeLineMode()){
    m_graphics.DrawFreeLine(
      CMouse::Bar(),CMouse::Above()
    );
  }
  
 }

Еще раз обращаю Ваше внимание на тот факт, что после нажатия на клавишу режим рисования переключается сразу - до того, как начнётся любое рисование (название моей функции начинается со слова Toggle — переключить), и состояние сохраняется до тех пор, пока его снова не переключат с помощью клавиши или после рисования линии.  При клике сначала проверяется, надо ли рисовать, потом, если надо, рисуем, и только потом режим будет переключен на "нейтральный".

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

Возвращаясь в настоящее, продолжаем рассматривать, как работает рисование.

/* Graphics.mqh */


//+------------------------------------------------------------------+
//|  Рисует прямую по произвольно заданным экстремумам. В текущей    |
//|    реализации первый экстремум задаётся горячей клавишей         |
//|    (по умолчанию - Q), второй - кликом вблизи следующеq нужной   |
//|    вершины                                                       |
//+------------------------------------------------------------------+
//|  Параметры:                                                      |
//|    int _bar - стартовый бар для поиска                           |
//|    bool _isUp - впадина или вершина?                             |
//|    int _fractalSizeRight - количество баров справа от экстремума |
//|    int _fractalSizeLeft -  количество баров слева от экстремума  |
//+------------------------------------------------------------------+
void CGraphics::DrawFreeLine(
  int _bar,
  bool _isUp,
  int _fractalSizeRight=1,
  int _fractalSizeLeft=1
)
 {
//--- Переменные
  double    selectedPrice,countedPrice,trendPrice1,trendPrice2;
  datetime  selectedTime,countedTime,trendTime1,trendTime2;
  int       selectedBar,countedBar;
  int       bar1,bar2;

  string trendName="",trendDescription="p2;";
  int fractalForFirstSearch = MathMax(_fractalSizeRight,_fractalSizeLeft)* 2;

//--- Поиск бара, удовлетворяющего критериям экстремума
  selectedBar = CUtilites::GetNearesExtremumSearchAround(
    _bar,
    _isUp,
    _fractalSizeLeft,
    _fractalSizeRight
  );

//--- Построение начального маркера
  if(0==m_Clicks_Count)
   {
    m_Clicks_Count=1;
    if(_isUp)
     {
      m_First_Point_Price=iHigh(NULL,PERIOD_CURRENT,selectedBar);
     }
    else
     {
      m_First_Point_Price=iLow(NULL,PERIOD_CURRENT,selectedBar);
     }
    m_First_Point_Time=iTime(NULL,PERIOD_CURRENT,selectedBar);
    //---
    m_First_Point_Time=CUtilites::DeepPointSearch(
                         m_First_Point_Time,
                         _isUp,
                         ENUM_TIMEFRAMES(Period())
                       );
    //---
    DrawFirstPointMarker(_isUp);
   
   }
//--- Обработка клика на графике
  else
   {
    ObjectDelete(0,m_First_Point_Marker_Name);
    if(_isUp)
     {
      countedPrice=iHigh(NULL,PERIOD_CURRENT,selectedBar);
     }
    else
     {
      countedPrice=iLow(NULL,PERIOD_CURRENT,selectedBar);
     }
    countedTime=iTime(NULL,PERIOD_CURRENT,selectedBar);
    //--- Сдвинуть точку по времени на младших таймфреймах
    countedTime=CUtilites::DeepPointSearch(countedTime,_isUp,ENUM_TIMEFRAMES(Period()));

    //--- Линия всегда рисуется слева направо. 
    //--- Если это неудобно, можно закомментировать 
    //---   до следующего комментария
    if(countedTime<m_First_Point_Time)
     {
      trendTime1=countedTime;
      trendPrice1=countedPrice;
      trendTime2=m_First_Point_Time;
      trendPrice2=m_First_Point_Price;
     }
    else
     {
      trendTime2=countedTime;
      trendPrice2=countedPrice;
      trendTime1=m_First_Point_Time;
      trendPrice1=m_First_Point_Price;
     }
    //--- Задать описание для будущей коррекции
    trendDescription+=TimeToString(trendTime2)+";"+DoubleToString(trendPrice2,Digits());

    //selectedPrice=CUtilites::EquationDirect(
    //                trendTime1,trendPrice1,trendTime2,trendPrice2,selectedTime
    //              );
    trendName=CUtilites::GetCurrentObjectName(allPrefixes[0],OBJ_TREND);
    
    TrendCreate(
      0,                    // ID графика
      trendName,            // имя линии
      0,                    // номер подокна
      trendTime1,           // время первой точки
      trendPrice1,          // цена первой точки
      trendTime2,           // время второй точки
      trendPrice2,          // цена второй точки
      CUtilites::GetTimeFrameColor(
        CUtilites::GetAllLowerTimeframes()
      ),                    // цвет линии
      Trend_Line_Style,     // стиль линии
      Trend_Line_Width,     // толщина линии
      false,                // на заднем плане
      true,                 // выбрана ли линия
      true                  // луч вправо
    );
    
    bar1=iBarShift(NULL,0,trendTime1);
    bar2=iBarShift(NULL,0,trendTime2);
    selectedTime = CUtilites::GetTimeInFuture(
                     //iTime(NULL,PERIOD_CURRENT,0),
                     trendTime1,
                     (int)((bar1-bar2)*m_Free_Trend_Length_Coefficient),
                     COUNT_IN_BARS
                   );
    selectedPrice= ObjectGetValueByTime(0,trendName,selectedTime);
    ObjectSetInteger(0,trendName,OBJPROP_RAY,IsRay());
    ObjectSetInteger(0,trendName,OBJPROP_RAY_RIGHT,IsRay());
    ObjectMove(0,trendName,1,selectedTime,selectedPrice);
    //---
    m_Clicks_Count=0;
    ToggleFreeLineMode();
   }

  ObjectSetString(0,trendName,OBJPROP_TEXT,trendDescription);
  ChartRedraw();
 }

Функция получилась объёмной, и, вероятно, при будущих рефакторингах будет разбита на несколько более мелких. Но сейчас я надеюсь, что цветовая маркировка и комментарии в коде помогут понять, как всё работает.

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

Если вызов функции — первый (то есть была нажата клавиша), нужно найти первую точку и нарисовать маркер.

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

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

Итак, в коде, отмеченном розовым цветом, базовые экстремумы уже найдены, и остаётся прочертить линию. Для этого сначала рисуем линию между двумя "базовыми" точками-экстремумами, и при этом включаем свойство "луч", чтобы линия прорисовалась в будущее (функция TrendCreate в самом начале этого блока).

Нужное время в будущем рассчитываем, исходя из настроек:

selectedTime = CUtilites::GetTimeInFuture(
                     //iTime(NULL,PERIOD_CURRENT,0),
                     trendTime1,
                     (int)((bar1-bar2)*m_Free_Trend_Length_Coefficient),
                     COUNT_IN_BARS
                   );

И дальше с помощью стандартной функции получаем нужную цену.

selectedPrice= ObjectGetValueByTime(0,trendName,selectedTime);

После этого остаётся только переместить вторую точку линии в нужные координаты и задать реальное свойство луча (напомню, это свойство по умолчанию переключается клавишей R — от слова Ray).

После того, как линия нарисована, необходимо отключить состояние ожидания клика, что и делает строки 

    m_Clicks_Count=0;
    ToggleFreeLineMode();

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

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

Концы линий D1 Концы линий Н4

Видно, что левый край линии, точно совпадающий с экстремумом на периоде D1, на периоде 4 часа смещен влево и с экстремумом не совпадает. Это — вполне очевидный эффект, связанный с тем, что экстремум дня не обязан приходиться на его начало. И если хочется большей точности, то при рисовании вручную можно нарисовать линию приблизительно, затем "спуститься" на младшие таймы и подправить концы.

Вот только это хорошо для одного, ну, двух графиков... А если их 20? Или 100? Это раздражает...

И, поскольку автоматическая функция рисования уже есть, почему бы не поручить ей эту рутинную работу при создании каждого объекта?

Вот примерно такие мысли привели к созданию функции DeepPointSearch.


Функция "Углубленной привязки" DeepPointSearch

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

//+------------------------------------------------------------------+
//| Поиск заданной точки на младших таймфреймах                      |
//+------------------------------------------------------------------+
//| Параметры:                                                       |
//|   datetime _neededTime - начальное времяна старшем таймфрейме    |
//|   bool _isUp - поиск по максимумам или по минимумам              |
//|   ENUM_TIMEFRAMES _higher_TF - самый верхний период              |
//+------------------------------------------------------------------+
//| Возвращаемое значение:                                           |
//|   Усточнённая дата (на самом нижнем возможном таймфрейме)        |
//+------------------------------------------------------------------+
datetime CUtilites::DeepPointSearch(
  datetime _neededTime,
  bool _isUp,
  ENUM_TIMEFRAMES _higher_TF=PERIOD_CURRENT
)
 {
//---
  //--- Результат - самое точное время из доступных
  datetime deepTime=0;
  //--- текущий таймфрейм
  ENUM_TIMEFRAMES currentTF;
  //--- Номер самого верхнего таймфрейма в списке всех доступных периодов
  int highTFIndex = GetTimeFrameIndexByPeriod(_higher_TF); 
  //--- Верхний интервал в секундах
  int highTFSeconds = PeriodSeconds(_higher_TF);
  //--- Текущий интервал в секундах
  int currentTFSeconds;
  //--- Счетчик
  int i;
  //--- Номер бара на старшем таймфрейме
  int highBar=iBarShift(NULL,_higher_TF,_neededTime);
  //--- Номер бара на текущем таймфрейме
  int currentBar;
  //--- Общее количество баров в текущем тааймфрейме
  int tfBarsCount;
  //--- Сколько баров младшего периода вмещается в один бар старшего
  int lowerBarsInHigherPeriod;
  //--- Максимально допустимое количество баров в терминале
  int terminalMaxBars = TerminalInfoInteger(TERMINAL_MAXBARS);

//--- Перебираем последовательно все таймфреймы
  for(i=0; i<highTFIndex; i++)
   {
    //--- Получаем таймфрейм по номеру в списке
    currentTF=GetTimeFrameByIndex(i);
//--- Проверяем, есть ли на данном фрейме нужное время...
    tfBarsCount=iBars(NULL,currentTF);
    if(tfBarsCount>terminalMaxBars-1)
     {
      tfBarsCount=terminalMaxBars-1;
     }
    deepTime=iTime(NULL,currentTF,tfBarsCount-1);
//--- ...И если да - находим его.
    if(deepTime>0 && deepTime<_neededTime)
     {
      currentTFSeconds=PeriodSeconds(currentTF);
      
      //--- Нужный бар ищем только в пределах старшей свечи
      lowerBarsInHigherPeriod=highTFSeconds/currentTFSeconds;
      currentBar = iBarShift(NULL,currentTF,_neededTime);
      
      if(_isUp)
       {
        currentBar = iHighest(
                       NULL,currentTF,MODE_HIGH,
                       lowerBarsInHigherPeriod+1,
                       currentBar-lowerBarsInHigherPeriod+1
                     );

       }
      else
       {
        currentBar = iLowest(
                       NULL,currentTF,MODE_LOW,
                       lowerBarsInHigherPeriod+1,
                       currentBar-lowerBarsInHigherPeriod+1
                     );
       }
      deepTime=iTime(NULL,currentTF,currentBar);
      //--- И когда нужное время найдено, прекращаем перебор
      break;
     }
   }
//--- Если дошли до конца цикла...
  if(i==highTFIndex)
   {
    //--- ...значит, нужное время есть только на старшем.
    deepTime=_neededTime;
   }
//---
  return (deepTime);
 }

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

//--- Максимально допустимое количество баров в терминале
  int terminalMaxBars = TerminalInfoInteger(TERMINAL_MAXBARS);

Если в истории слишком много баров, ограничиваемся только отображаемыми.

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

Если все проверки пройдены, начинается рутина. Нужная нам точка по определению будет самой экстремальной внутри диапазона свечи старшего временного интервала. Остаётся только понять, сколько нужно рассмотреть свечей. После этого стандартные функции помогают определить самый экстремальный из экстремумов, после чего мы можем посчитать время и завершить работу.

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


Коррекция времени

Второй "фишкой" данной реализации является коррекция прямых по времени. Анимация ниже поясняет проблему.

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

И ладно FOREX, где такие прыжки могут происходить раз в неделю. Однако есть же еще и рынок акций, и там разрывы по времени могут быть каждый день — в зависимости от биржи, и часто такое случается еще и внутри дня.

Здорово, что есть автоматика!

Для того, чтобы всё сработало, нужно как-то сохранить "правильные" координаты, а потом корректировать их по мере необходимости.

Я выбрал для сохранения координат описание прямой, поскольку большинство трейдеров при создании автоматических объектов всё равно описания не использует. Альтернативы — файлы со списком прямых или глобальные переменные терминала, если прямых не слишком много.

/* Graphics.mqh */

void CGraphics::DrawFreeLine(//...)
 {

//...
  string trendDescription="p2;";

//...
  trendDescription+=TimeToString(trendTime2)+";"+DoubleToString(trendPrice2,Digits());
  
//...
  ObjectSetString(0,trendName,OBJPROP_TEXT,trendDescription);

А дальше — тот же трюк с координатами, взятыми на "физической" прямой, что я пописывал ранее. По-моему, всё довольно и тривиально, и комментарии в коде примера должны помочь понять всё, что в функции происходит.

/* Utilites.mqh */

//+------------------------------------------------------------------+
//| Корректирует положение конца прямой в будущем при ценовых        |
//|   разрывах                                                       |
//+------------------------------------------------------------------+
//| Параметры:                                                       |
//|   string _line_name - имя линии, предназзначенной для коррекции  |
//+------------------------------------------------------------------+
void CUtilites::CorrectTrendFutureEnd(string _line_name)
 {
//---
  if(ObjectFind(0,_line_name)<0)
   {
    PrintDebugMessage(__FUNCTION__+" _line_name="+_line_name+": Объект не существует");
    //--- Если объекта для поиска нет, делать больше нечего.
    return;
   }
  //--- Получаем описание
  string line_text=ObjectGetString(0,_line_name,OBJPROP_TEXT);
  
  string point_components[]; // массив для фрагментов описания точки
  string name_components[];  // массив, содержащий фрагменты имени прямой
  string helpful_name="Helpful line"; // имя вспомогательной прямой
  string vertical_name=""; // имя сопутствующей вертикали из перекрестья
  
  //--- Получаем время и цену точки в строковом виде
  int point_components_count=StringSplit(line_text,StringGetCharacter(";",0),point_components);
  
  datetime time_of_base_point; // время базовой точки
  datetime time_first_point,time_second_point; // время первой и второй точек 
  datetime time_far_ideal; // рассчетное время в будущем
  double price_of_base_point; // цена базовой точки
  double price_first_point,price_second_point; // цены первой и второй точек
  int i; // счетчик

//--- Проверка, нужная ли прямая
  if(line_text=="" || point_components_count<3 || point_components[0]!="p2")
   {
    PrintDebugMessage(__FUNCTION__+" Ошибка: линию нельзя использовать");
    return;
   }
//--- Получение координат "базовой" точки из описания прямой
  time_of_base_point=StringToTime(point_components[1]);
  price_of_base_point=StringToDouble(point_components[2]);
  if(time_of_base_point==0 || price_of_base_point==0)
   {
    PrintDebugMessage(__FUNCTION__+" Error: Unusable description");
    return;
   }
//--- Получение реальных координат прямой
  time_first_point = (datetime)ObjectGetInteger(0,_line_name,OBJPROP_TIME,0);
  time_second_point = (datetime)ObjectGetInteger(0,_line_name,OBJPROP_TIME,1);
  price_first_point = ObjectGetDouble(0,_line_name,OBJPROP_PRICE,0);
  price_second_point = ObjectGetDouble(0,_line_name,OBJPROP_PRICE,1);

//--- Создание вспомогательной линии (от начальной до базовой точки)
  MakeHelpfulLine(
    time_first_point,
    price_first_point,
    time_of_base_point,
    price_of_base_point
  );

//--- Вычисление правильного времени для текущей ситуации
  time_far_ideal=ObjectGetTimeByValue(0,helpful_name,price_second_point);
//---
  if(time_second_point != time_far_ideal)
   {
    //--- перемещение совбодного конца трендовой
    ObjectMove(0,_line_name,1,time_far_ideal,price_second_point);
    //--- и сопутствующей ей вертикали
    StringSplit(_line_name,StringGetCharacter("_",0),name_components);
    for(i=0; i<ObjectsTotal(0,-1,OBJ_VLINE); i++)
     {
      vertical_name = ObjectName(0,i,-1,OBJ_VLINE);
      if(name_components[0]==StringSubstr(vertical_name,0,StringFind(vertical_name,"_",0)))
       {
        if((datetime)ObjectGetInteger(0,vertical_name,OBJPROP_TIME,0)==time_second_point)
         {
          ObjectMove(0,vertical_name,0,time_far_ideal,price_second_point);
          break;
         }
       }
     }
   }
  // Удалене вспомогательной линии
  RemoveHelpfulLine();
 }

Чтобы всё это заработало, необходимо вызывать этот код с некоторой периодичностью. Я выбрал начало каждого нового часа.

/* Shortcuts.mq5 */

int OnCalculate(/*...*/)
 {
   //...
   if(CUtilites::IsNewBar(First_Start_True,PERIOD_H1))
   {
    for(i=0; i<all_lines_count; i++)
     {
      line_name=ObjectName(0,i,-1,OBJ_TREND);
      CUtilites::CorrectTrendFutureEnd(line_name);
      ChartRedraw();
     }
   }
   //...
 }


Клавиши, используемые в текущей реализации библиотеки

Действие
 Клавиша От английского слова
 Перейти на таймфрейм вверх по основным периодам (из панели периодов)  U  Up
 Перейти на таймфрейм вниз  D  Down
 Смена Z-уровня графика (график сверху или снизу объектов)  Z  Z-order
 Рисование наклонной трендовой линии по двум ближайшим к мыши однонаправленным экстремумам  T  Trend line
 Переключение режима луча для новых прямых
 R  Ray
 Рисование простой вертикальной черты
 I(i) [Only visual  vertical]
 Рисование простой горизонтальной черты
 H  Horizontal
 Рисование комплекта вил Эндрюса
 P  Pitchfork
 Рисование веера Фибоначчи (VFun)
 F  Fun
 Рисование короткого горизонтального уровня
 S  Short
 Рисование удлинённого горизонтального уровня
 L  Long
 Рисование вертикальной черты с отметками уровней
 V  Vertical
 Рисование перекрестья
 X  [Only visual  cross]
 Рисование лини по произвольным вершинам
 Q  [No conformity... "L" and "T" is not free]
 Рисование группы прямоугольников
 B  Box


Заключение

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

В ближайших планах — сделать возможность рисовать линии не только по строгим вершинам, но и по касательным.

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

В отдалённой перспективе — сохранение настроек в файле (вместо — или вместе — с настройками индикатора с помощью input-переменных) и добавление графического интерфейса для изменения настроек "на лету".


Прикрепленные файлы |
Графика в библиотеке DoEasy (Часть 87): Коллекция графических объектов - контроль модификации свойств объектов на всех открытых графиках Графика в библиотеке DoEasy (Часть 87): Коллекция графических объектов - контроль модификации свойств объектов на всех открытых графиках
В статье продолжим работу над отслеживанием событий стандартных графических объектов и создадим функционал, позволяющий контролировать изменение свойств графических объектов, расположенных на любых открытых в терминале графиках.
Графика в библиотеке DoEasy (Часть 86): Коллекция графических объектов - контролируем модификацию свойств Графика в библиотеке DoEasy (Часть 86): Коллекция графических объектов - контролируем модификацию свойств
В статье рассмотрим отслеживание модификации значений свойств, удаление и переименование графических объектов в библиотеке.
Разработка торговых роботов при помощи визуального программирования Разработка торговых роботов при помощи визуального программирования
В статье демонстрируется возможности редактора botbrains.app — no-code платформы для разработки торговых роботов. Чтобы создать торгового робота не нужно программировать — просто перетащите нужные блоки на схему, задайте их параметры и установите связи между ними.
Рецепты MQL5 – Экономический календарь Рецепты MQL5 – Экономический календарь
В статье освещаются программные возможности по работе с Экономическим календарём. Для этих целей создаётся класс для упрощенного доступа к свойствам календаря и получения значений событий. В качестве практического примера предлагается запрограммировать индикатор, использующий данные по чистому объёму спекулятивных позиций от CFTC.