Conjunto de ferramentas para marcação manual de gráficos e negociação (Parte I). Preparação - Descrição da Estrutura e Classe Auxiliar

Oleh Fedorov | 29 setembro, 2020

Introdução


Eu sou como um "freio de mão". Com isso quero dizer que eu não analiso os gráficos com ajuda de fórmulas ou indicadores, senão "manualmente", baseando-me nos gráficos. Isso me permite que ao negociar seja mais flexível, note coisas difíceis de formular, alterne facilmente timeframes quando vejo que o movimento se está tornando mais forte ou mais fraco, saiba o comportamento previsto do preço muito antes de entrar no mercado.

Basicamente, para análise, uso diferentes combinações de linhas de tendência (garfos, leques, níveis, etc.). Nesse sentindo, eu criei uma ferramenta conveniente para mim que me permite desenhar diferentes linhas de tendência muito rápido, basta eu clicar teclas. E agora quero compartilhar essa ferramenta com a comunidade.

Apresentação em vídeo. Como isso funciona



Concepção geral. Formulação do problema


Portanto, preciso de uma ferramenta que me ajude com atalhos de teclado a realizar as operações mais frequentes que realizo.

Quais são essas tarefas? Eu gostaria, por exemplo:

Claro que para a maioria dessas tarefas podemos criar scripts. Eu tenho vários e vou compartilhá-los num anexo. Mas prefiro uma outra abordagem.

Eu quero que, em qualquer EA ou indicador, seja possível criar um método OnCHartEvent cuja missão será reagir a qualquer evento: clique de botão, movimento do mouse, criação/remoção de um objeto gráfico.

Por esse motivo, decidi que o programa será executado na forma de arquivos de inclusão. E todas as funções e variáveis serão distribuídas em várias classes para facilitar o acesso a elas. Agora como preciso de classes só para agrupar facilmente funções, nas primeiras implementações definitivamente não haverá nada complexo como herança ou "fábricas de classes". Será apenas uma coleção.

Certamente, quero que a classe seja multiplataforma e funcione de maneira semelhante em MQL4 e MQL5.

Estrutura do programa

Lista de arquivos

A biblioteca contém cinco arquivos interligados. Todos os arquivos estão localizados na pasta "Shortcuts" no diretório Include. Seus nomes podem ser vistos na imagem: GlobalVariables.mqh, Graphics.mqh, Mouse.mqh, Shortcuts.mqh, Utilites.mqh.

Arquivo principal da biblioteca (Shortcuts.mqh)

O arquivo principal do programa, "Shortcuts.mqh", conterá a lógica para reagir às teclas. É este arquivo que será anexado ao EA. Nele também serão incluídos todos os arquivos auxiliares.

//+------------------------------------------------------------------+
//|                                                    Shortcuts.mqh |
//|                        Copyright 2020, MetaQuotes Software Corp. |
//|                            https://www.mql5.com/ru/articles/7468 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2020, MetaQuotes Software Corp."
#property link      "https://www.mql5.com/ru/articles/7468"

#include "GlobalVariables.mqh"
#include "Mouse.mqh"
#include "Utilites.mqh"
#include "Graphics.mqh"

//+------------------------------------------------------------------+
//| Основной управляющий класс программы. Именно его нужно подключать|
//| в советник.                                                      |
//+------------------------------------------------------------------+
class CShortcuts
  {
private:
   CGraphics         m_graphics;   // Object for drawing m_graphics
public:
                     CShortcuts();
   void              OnChartEvent(const int id,
                                  const long &lparam,
                                  const double &dparam,
                                  const string &sparam);
  };
//+------------------------------------------------------------------+
//| Конструктор по умолчанию                                         |
//+------------------------------------------------------------------+
CShortcuts::CShortcuts(void)
  {
   ChartSetInteger(0,CHART_EVENT_MOUSE_MOVE,true);
  }

//+------------------------------------------------------------------+
//| Функция обработки событий                                        |
//+------------------------------------------------------------------+
void CShortcuts::OnChartEvent(
   const int id,
   const long &lparam,
   const double &dparam,
   const string &sparam
)
  {
//---

   // Здесь будет описание событий нажатия кнопок клавиатуры и движений
   // мыши

   //  ...

  }
}

CShortcuts shortcuts;

O arquivo contém a descrição da classe CShortcuts.

Ao início do arquivo são anexadas todas as classes auxiliares.

A classe tem apenas dois métodos: o primeiro é um manipulador de eventos OnChartEvent, no qual, de fato, ocorrerá o processamento de eventos de teclas e de movimentos do mouse, o segundo, um construtor padrão em que será permitido controlar os movimentos do mouse.

Após a descrição da classe, será criada a variável shortcuts que deverá ser usada no método OnChartEvent do EA principal ao anexar a biblioteca.

Anexar em si requer duas strings:

//+------------------------------------------------------------------+
//| Основной эксперт (файл "Shortcuts-Main-Expert.mq5")              |
//+------------------------------------------------------------------+
#include <Shortcuts\Shortcuts.mqh>


// ...

//+------------------------------------------------------------------+
//| Функция ChartEvent                                               |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//---
   shortcuts.OnChartEvent(id,lparam,dparam,sparam);
  }

A primeira string anexa o arquivo da classe, já a segunda transfere para a classe o controle do tratamento de eventos.

Depois disso, o Expert Advisor ficará pronto para trabalhar, assim, ele poderá ser compilado para imediatamente começar a desenhar linhas.

Classe de processamento de movimentos do mouse

Esta classe armazenará todos os parâmetros básicos da posição atual do cursor: coordenadas X, Y (em pixels e preços/tempo), número da barra em que o ponteiro está localizado, e assim por diante. Será armazenada no arquivo "Mouse.mqh"

//+------------------------------------------------------------------+
//|                                                        Mouse.mqh |
//|                        Copyright 2020, MetaQuotes Software Corp. |
//|                            https://www.mql5.com/ru/articles/7468 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2020, MetaQuotes Software Corp."
#property link      "https://www.mql5.com/ru/articles/7468"
//+------------------------------------------------------------------+
//| Класс обработки движений мыши                                    |
//+------------------------------------------------------------------+
class CMouse
  {
   //--- Members
private:
   static int        m_x;           // X
   static int        m_y;           // Y
   static int        m_barNumber;   // Номер бара
   static bool       m_below;       // Признак курсора над ценой
   static bool       m_above;       // Признак курсора под ценой

   static datetime   m_currentTime; // Текущее время
   static double     m_currentPrice;// Текущая цена
   //--- Methods
public:

   //--- Запоминает основные параметры курсора мыши
   static void       SetCurrentParameters(
      const int id,
      const long &lparam,
      const double &dparam,
      const string &sparam
   );
   //--- Возвращает координату X (в пикселах) текущего положения курсора
   static int        X(void) {return m_x;}
   //--- Возвращает координату Y (в пикселах) текущего положения курсора
   static int        Y(void) {return m_y;}
   //--- Возвращает цену текущего положения курсора
   static double     Price(void) {return m_currentPrice;}
   //--- Возвращает время текущего положения курсора
   static datetime   Time(void) {return m_currentTime;}
   //--- Возвращает номер бара текущего положения курсора
   static int        Bar(void) {return m_barNumber;}
   //--- Возвращает флаг, показывающий, что цена находится ниже Low текущего бара
   static bool       Below(void) {return m_below;}
   //--- Возвращает признак того, что цена находится выше High текущего бара
   static bool       Above(void) {return m_above;}
  };
//---

int CMouse::m_x=0;
int CMouse::m_y=0;
int CMouse::m_barNumber=0;
bool CMouse::m_below=false;
bool CMouse::m_above=false;
datetime CMouse::m_currentTime=0;
double CMouse::m_currentPrice=0;


//+------------------------------------------------------------------+
//| Запоминает основные параметры при движении мыши: координаты      |
//| курсора в пикселях и в ценах/времени, находится ли курсор над    |
//| или под ценами.                                                  |
//|+-----------------------------------------------------------------+
static void CMouse::SetCurrentParameters(
   const int id,
   const long &lparam,
   const double &dparam,
   const string &sparam
)
  {
//---
   int window = 0;
//---
   ChartXYToTimePrice(
      0,
      (int)lparam,
      (int)dparam,
      window,
      m_currentTime,
      m_currentPrice
   );
   m_x=(int)lparam;
   m_y=(int)dparam;
   m_barNumber=iBarShift(
                  Symbol(),
                  PERIOD_CURRENT,
                  m_currentTime
               );
   m_below=m_currentPrice<iLow(Symbol(),PERIOD_CURRENT,m_barNumber);
   m_above=m_currentPrice>iHigh(Symbol(),PERIOD_CURRENT,m_barNumber);
  }

//+------------------------------------------------------------------+

Dada classe pode ser usada de qualquer lugar no programa, pois seus métodos são declarados como estáticos. Por isso, não será preciso criar uma instância para usá-la.

Descrição do bloco de configurações do EA. Arquivo "GlobalVariables.mqh"


Todas as configurações poderão ser acessadas pelo usuário e serão armazenadas no arquivo "GlobalVariables.mqh".

No próximo artigo serão descritas muitas mais configurações, à medida que aparecerem mais objetos para desenhar.

E agora este é o código do que é usado na versão atual:

//+------------------------------------------------------------------+
//|                                              GlobalVariables.mqh |
//|                        Copyright 2020, MetaQuotes Software Corp. |
//|                            https://www.mql5.com/ru/articles/7468 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2020, MetaQuotes Software Corp."
#property link      "https://www.mql5.com/ru/articles/7468"
//+------------------------------------------------------------------+
//| Файл описания параметров, доступных пользователю                 |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Настройки клавиш                                                 |
//+------------------------------------------------------------------+
input string   Keys="=== Настройки клавиш ===";
input string   Up_Key="U";            // Переключить таймфрейм вверх
input string   Down_Key="D";          // Переключить таймфрейм вниз
input string   Trend_Line_Key="T";              // Трендовая линия
input string   Switch_Trend_Ray_Key="R";        // Признак луча трендовой
input string   Z_Index_Key="Z";                 // Признак графика сверху

//+------------------------------------------------------------------+
//| Настройки размеров                                               |
//+------------------------------------------------------------------+
input string   Dimensions="=== Настройки размеров ===";
input int      Trend_Line_Width=2;        // Ширина трендовой линии

//+------------------------------------------------------------------+
//| Стили отображения                                                |
//+------------------------------------------------------------------+
input string   Styles="=== Стили отображения ===";
input ENUM_LINE_STYLE      Trend_Line_Style=STYLE_SOLID;       // Стиль Трендовая

//+------------------------------------------------------------------+
//| Остальные параметры                                              |
//+------------------------------------------------------------------+
input string               Others="=== Остальные параметры ===";

input bool                 Is_Trend_Ray=false;                      // Трендовая - луч
input bool                 Is_Change_Timeframe_On_Create = true;    // Скрывать объекты на старших таймфреймах?
                                                                    //   (true - скрывать, false - отображать)
input bool                 Is_Select_On_Create=true;                // Выделение при создании
input bool                 Is_Different_Colors=true;                // Менять ли цвета для таймов

// Количество баров слева и справа
// для экстремумов веера и трендовой
input int                  Fractal_Size_Left=1;                     // Размер фрактала слева
input int                  Fractal_Size_Right=1;                    // Размер фрактала справа

//+------------------------------------------------------------------+
//| Префиксы имён рисуемых фигур (меняются только в коде,            |
//| в параметрах советника не видны)                                 |
//+------------------------------------------------------------------+
// string   Prefixes="=== Prefixes ===";

string   Trend_Line_Prefix="Trend_";                     // Префикс трендовой

//+------------------------------------------------------------------+
//| Цвета объектов одного таймфрейма (меняются только в коде,        |
//| в параметрах советника не видны)                                 |
//+------------------------------------------------------------------+
// string TimeframeColors="=== Time frame colors ===";
color mn1_color=clrCrimson;
color w1_color=clrDarkOrange;
color d1_color=clrGoldenrod;
color h4_color=clrLimeGreen;
color h1_color=clrLime;
color m30_color=clrDeepSkyBlue;
color m15_color=clrBlue;
color m5_color=clrViolet;
color m1_color=clrDarkViolet;
color common_color=clrGray;

//--- Вспомогательная константа для вывода сообщаений об ошибках
#define DEBUG_MESSAGE_PREFIX "=== ",__FUNCTION__," === "

//--- Константы для описания основных таймфреймов при рисовании
#define PERIOD_LOWER_M5 OBJ_PERIOD_M1|OBJ_PERIOD_M5
#define PERIOD_LOWER_M15 PERIOD_LOWER_M5|OBJ_PERIOD_M15
#define PERIOD_LOWER_M30 PERIOD_LOWER_M15|OBJ_PERIOD_M30
#define PERIOD_LOWER_H1 PERIOD_LOWER_M30|OBJ_PERIOD_H1
#define PERIOD_LOWER_H4 PERIOD_LOWER_H1|OBJ_PERIOD_H4
#define PERIOD_LOWER_D1 PERIOD_LOWER_H4|OBJ_PERIOD_D1
#define PERIOD_LOWER_W1 PERIOD_LOWER_D1|OBJ_PERIOD_W1
//+------------------------------------------------------------------+

Funções auxiliares

O programa tem muitas funções que não estão diretamente relacionadas ao desenho, mas ajudam a encontrar pontos extremos, alternar timeframes e assim por diante. Todas elas foram colocadas no arquivo "Utilites.mqh".

Cabeçalho do arquivo Utilites.mqh

//+------------------------------------------------------------------+
//|                                                     Utilites.mqh |
//|                        Copyright 2020, MetaQuotes Software Corp. |
//|                            https://www.mql5.com/ru/articles/7468 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2020, MetaQuotes Software Corp."
#property link      "https://www.mql5.com/ru/articles/7468"

//+------------------------------------------------------------------+
//| Класс для описания вспомогательных функций                       |
//+------------------------------------------------------------------+
class CUtilites
  {
public:
   //--- Изменяет таймфрейм на следующий в панели инструментов
   static void       ChangeTimeframes(bool isUp);
   //--- Преобразует строковые константы команд в коды клавиш
   static int        GetCurrentOperationChar(string keyString);
   //--- Переключает слои в графиках (график поверх всех объектов)
   static void       ChangeChartZIndex(void);
   //--- Возвращает номер ближайшего экстремального бара
   static int        GetNearestExtremumBarNumber(
      int starting_number=0,
      bool is_search_right=false,
      bool is_up=false,
      int left_side_bars=1,
      int right_side_bars=1,
      string symbol=NULL,
      ENUM_TIMEFRAMES timeframe=PERIOD_CURRENT
   );

   //--- Возвращает цвет для текущего таймфрейма
   static color      GetTimeFrameColor(long allDownPeriodsValue);
   //--- Возвращает список всех таймфреймов, лежащих ниже текущего (включая текущий)
   static long       GetAllLowerTimeframes(int NeededTimeframe=PERIOD_CURRENT);
   //--- Координаты прямой. Заносит в точки p1 и p2 номера экстремалных баров
   static void       SetExtremumsBarsNumbers(bool _is_up,int &p1, int &p2);
   //--- Преобразование строки в массив чисел с плавающей точкой
   static void       StringToDoubleArray(
      string _haystack,
      double &_result[],
      const string _delimiter=","
   );
   //--- Определяет имя текущего объекта
   static string            GetCurrentObjectName(
      const string _prefix,
      const ENUM_OBJECT _type=OBJ_TREND,
      int _number = -1
   );
   //--- Получает номер следующего объекта
   static int               GetNextObjectNumber(
      const string _prefix,
      const ENUM_OBJECT _object_type,
      bool true
   );
   //--- Возвращает расстояние в экранных пикселях между соседними барами
   static int               GetBarsPixelDistance(void);
   //--- Конвертация числового значения таймфрейма в его строковое имя
   static string            GetTimeframeSymbolName(
      ENUM_TIMEFRAMES _timeframe=PERIOD_CURRENT  // Нужный таймфрейм
   );
  };

Função que altera sequencialmente o período gráfico

As primeiras funções neste arquivo são triviais, por exemplo, a função que altera os timeframes do gráfico atual, um após o outro, se vê assim:

//+------------------------------------------------------------------+
//| Последовательно изменяет период текущего графика                 |
//|                                                                  |
//| В данной реализации используются только таймфреймы, выведенные   |
//| в стандартную панель.                                            |
//|                                                                  |
//| Параметры:                                                       |
//|    _isUp - направление смены таймфрейма: вверх (true)            |
//|            или вниз (false)                                      |
//+------------------------------------------------------------------+
static void CUtilites::ChangeTimeframes(bool _isUp)
  {
   ENUM_TIMEFRAMES timeframes[] =
     {
      PERIOD_CURRENT,
      PERIOD_M1,
      PERIOD_M5,
      PERIOD_M15,
      PERIOD_M30,
      PERIOD_H1,
      PERIOD_H4,
      PERIOD_D1,
      PERIOD_W1,
      PERIOD_MN1
     };
   int period = Period();
   int shift = ArrayBsearch(timeframes,period);
   if(_isUp && shift < ArraySize(timeframes)-1)
     {
      ChartSetSymbolPeriod(0,NULL,timeframes[++shift]);
     }
   else
      if(!_isUp && shift > 1)
        {
         ChartSetSymbolPeriod(0,NULL,timeframes[--shift]);
        }
  } 

Primeiro, esta função descreve uma matriz com todos os timeframes especificados no painel de controle por padrão. Se precisar alternar entre todos os timeframes disponíveis no MetaTrader 5, você só deverá adicionar as constantes correspondentes aos locais necessários na matriz. Porém, neste caso, pode ser perdida a compatibilidade com versões anteriores, em outras palavras, a biblioteca pode parar de funcionar com MQL4.

Em seguida, usamos funções padrão para obter o timeframe atual e o encontrar na lista.

Depois, para alternar períodos gráficos, é usada a função padrão ChartSetSymbolPeriod, à qual é transferido o período que segue ao atual.

Não vejo nenhum sentido em comentar o resto das funções fora do código. É simplesmente código.

Algumas funções simples

//+------------------------------------------------------------------+
//| Преобразует строковые константы команд в коды клавиш             |
//+------------------------------------------------------------------+
static int CUtilites::GetCurrentOperationChar(string keyString)
  {
   string keyValue = keyString;
   StringToUpper(keyValue);
   return(StringGetCharacter(keyValue,0));
  }
//+------------------------------------------------------------------+
//| Переключает расположение графика поверх других объектов          |
//+------------------------------------------------------------------+
static void CUtilites::ChangeChartZIndex(void)
  {
   ChartSetInteger(
      0,
      CHART_FOREGROUND,
      !(bool)ChartGetInteger(0,CHART_FOREGROUND)
   );
   ChartRedraw(0);
  } 
//+------------------------------------------------------------------+
//| Возвращает строковое имя для любого стандартного таймфрейма      |
//| Параметры:                                                       |
//|    _timeframe - числовое значение ENUM_TIMEFRAMES, для которого  |
//|                 нужно подобрать строковое название               |
//| Возвращаемое значение:                                           |
//|   строковое название нужного таймфрейма                          |
//+------------------------------------------------------------------+
static string CUtilites::GetTimeframeSymbolName(
   ENUM_TIMEFRAMES _timeframe=PERIOD_CURRENT  // Нужный таймфрейм
)
  {
   ENUM_TIMEFRAMES current_timeframe; // текущий таймфрейм
   string result = "";
//---
   if(_timeframe == PERIOD_CURRENT)
     {
      current_timeframe = Period();
     }
   else
     {
      current_timeframe = _timeframe;
     }
//---
   switch(current_timeframe)
     {
      case PERIOD_M1:
         return "M1";
      case PERIOD_M2:
         return "M2";
      case PERIOD_M3:
         return "M3";
      case PERIOD_M4:
         return "M4";
      case PERIOD_M5:
         return "M5";
      case PERIOD_M6:
         return "M6";
      case PERIOD_M10:
         return "M10";
      case PERIOD_M12:
         return "M12";
      case PERIOD_M15:
         return "M15";
      case PERIOD_M20:
         return "M20";
      case PERIOD_M30:
         return "M30";
      case PERIOD_H1:
         return "H1";
      case PERIOD_H2:
         return "M1";
      case PERIOD_H3:
         return "H3";
      case PERIOD_H4:
         return "H4";
      case PERIOD_H6:
         return "H6";
      case PERIOD_H8:
         return "H8";
      case PERIOD_D1:
         return "D1";
      case PERIOD_W1:
         return "W1";
      case PERIOD_MN1:
         return "MN1";
      default:
         return "Unknown";
     }
  }
//+------------------------------------------------------------------+
//| Возвращает стандартный цвет для текущего таймфрейма              |
//+------------------------------------------------------------------+
static color CUtilites::GetTimeFrameColor(long _all_down_periods_value)
  {
   if(Is_Different_Colors)
     {
      switch((int)_all_down_periods_value)
        {
         case OBJ_PERIOD_M1:
            return (m1_color);
         case PERIOD_LOWER_M5:
            return (m5_color);
         case PERIOD_LOWER_M15:
            return (m15_color);
         case PERIOD_LOWER_M30:
            return (m30_color);
         case PERIOD_LOWER_H1:
            return (h1_color);
         case PERIOD_LOWER_H4:
            return (h4_color);
         case PERIOD_LOWER_D1:
            return (d1_color);
         case PERIOD_LOWER_W1:
            return (w1_color);
         case OBJ_ALL_PERIODS:
            return (mn1_color);
         default:
            return (common_color);
        }
     }
   else
     {
      return (common_color);
     }
  }

Função de pesquisa de extremos e sua aplicação

A próxima função ajuda a encontrar extremos. Dependendo dos parâmetros, os extremos podem ser encontrados tanto à direita quanto à esquerda do ponteiro do mouse, e é possível definir quantas barras devem estar à esquerda do extremo e quantas, à direita.

//+------------------------------------------------------------------+
//| Возвращает номер бара ближайшего "фрактала" в выбранном          |
//|   направлении                                                    |
//| Параметры:                                                       |
//|   starting_number - номер бара, с  которого стартует поиск       |
//|   is_search_right - Ищем вправо (true) или влево(false)?         |
//|   is_up - если "true" - поиск по уровням High, иначе - Low       |
//|   left_side_bars - количество баров слева                        |
//|   right_side_bars - количество баров справа                      |
//|   symbol - имя символа для поиска                                |
//|   timeframe - временной интервал                                 |
//| Возвращаемое значение:                                           |
//|   номер ближайшего к starting_number экстремума,                 |
//|   соответствующего заданным параметрам                           |                 
//+------------------------------------------------------------------+
static int CUtilites::GetNearestExtremumBarNumber(
   int starting_number=0,              // Номер начального бара
   const bool is_search_right=false,   // Направление вправо
   const bool is_up=false,             // Искать по "верхам" (High)
   const int left_side_bars=1,         // Количество баров слева
   const int right_side_bars=1,        // Количество баров справа
   const string symbol=NULL,           // Нужный символ
   const ENUM_TIMEFRAMES timeframe=PERIOD_CURRENT // Нужный таймфрейм
)
  {
//---
   int   i,
         nextExtremum,
         sign = is_search_right ? -1 : 1;

//--- Если стартовый бар указан неверно
//--- (лежит за границами текущего графика)
//--- и поиск - в сторону границы.
   if((starting_number-right_side_bars<0
       && is_search_right)
      || (starting_number+left_side_bars>iBars(symbol,timeframe)
          && !is_search_right)
     )
     { //--- Необходимо вывести сообщение об ошибке
      if(Print_Warning_Messages)
        {
         Print(DEBUG_MESSAGE_PREFIX,
               "Не могу найти экстремум: ",
               "неверное направление");
         Print("left_side_bars = ",left_side_bars,"; ",
               "right_side_bars = ",right_side_bars);
        }
      return (-2);
     }
   else
     {
      //--- иначе - направление позволяет выбрать правильный бар.
      //---   бежим по всем барам в нужном направлении,
      //---   пока находимся за границами известного графика
      while((starting_number-right_side_bars<0
             && !is_search_right)
            || (starting_number+left_side_bars>iBars(symbol,timeframe)
                && is_search_right)
           )
        {
         starting_number +=sign;
        }
     }
//---
   i=starting_number;
 
   //--- Подготовка закончена. Приступаем к поиску
   while(i-right_side_bars>=0
         && i+left_side_bars<iBars(symbol,timeframe)
        )
     {
      //--- В зависимости от уровня проверяем нужный экстремум
      if(is_up)
        { //--- ...либо верхний...
         nextExtremum = iHighest(
                           Symbol(),
                           Period(),
                           MODE_HIGH,
                           left_side_bars+right_side_bars+1,
                           i-right_side_bars
                        );
        }
      else
        {  //--- ...либо нижний...
         nextExtremum = iLowest(
                           Symbol(),
                           Period(),
                           MODE_LOW,
                           left_side_bars+right_side_bars+1,
                           i-right_side_bars
                        );
        }
      if(nextExtremum == i) // Если текущий бар - экстремум,
        {
         return nextExtremum;  // то задача решена
        }
      else // Иначе - продолжаем смещать счётчик проверяемой свечи в нужную сторону
         if(is_search_right)
           {
            if(nextExtremum<i)
              {
               i=nextExtremum;
              }
            else
              {
               i--;
              }
           }
         else
           {
            if(nextExtremum>i)
              {
               i=nextExtremum;
              }
            else
              {
               i++;
              }
           }
     }
//--- Если добежали до края и никаких экстремумов не нашли,
   if(Print_Warning_Messages)
     {
      //--- нужно сообщить об ошибке.
      Print(DEBUG_MESSAGE_PREFIX,
            "Не могу найти экстремум: ",
            "ошибочно выбрана начальная точка либо граничные условия.");
      Print("left_side_bars = ",left_side_bars,"; ",
            "right_side_bars = ",right_side_bars);
     }
   return (-1);
  } 

Para desenhar linhas de tendência, é preciso de uma função que encontre os dois pontos extremos mais próximos à direita do mouse. Esta função pode usar a anterior:

//+------------------------------------------------------------------+
//| Находит 2 ближайших экстремума справа от текущей позиции         |
//| указателя мыши                                                   |
//| Параметры:                                                       |
//|    _is_up - поиск по High (true) или по Low (false)              |
//|    int &_p1 - номер бара первой точки                            |
//|    int &_p2 - номер бара второй точки                            |
//+------------------------------------------------------------------+
static void CUtilites::SetExtremumsBarsNumbers(
   bool _is_up, // поиск по High (true) или по Low (false)
   int &_p1,    // номер бара первой точки
   int &_p2     // номер бара второй точки
)
  {
   int dropped_bar_number=CMouse::Bar();
//---
   _p1=CUtilites::GetNearestExtremumBarNumber(
          dropped_bar_number,
          true,
          _is_up,
          Fractal_Size_Left,
          Fractal_Size_Right
       );
   _p2=CUtilites::GetNearestExtremumBarNumber(
          _p1-1, // Бар левее предыдущего найденного экстремума
          true,
          _is_up,
          Fractal_Size_Left,
          Fractal_Size_Right
       );
   if(_p2<0)
     {
      _p2=0;
     }
  } 

Gerando nomes de objetos

Para desenhar os mesmos objetos em série, eles devem receber nomes exclusivos. Para fazer isso, a maneira mais fácil é pegar algum prefixo que bata com dado tipo de objeto e adicionar um número único a ele. Prefixos para diferentes tipos de objetos estão listados no arquivo "GlobalVariables.mqh".

Os números são gerados pela função correspondente.

//+------------------------------------------------------------------+
//| Возвращает номер следующего объекта в серии                      |
//| Параметры:                                                       |
//|   prefix - префикс имени для данной группы объектов.             |
//|   object_type - тип тех объектов, которые нужно искать.          |
//|   only_prefixed - если "false", то поиск - по всем объектам      |
//|                      данного типа, "true" - только по объектам   |
//|                      с указанным префиксом                       |
//| Возвращаемое значение:                                           |              |
//|   номер следующего объекта серии. Если ищем по префиксам,        |
//|      и существующая нумерация имеет "пробел", следующий номер    |
//|      будет в начале этого "пробела".                             |
//+------------------------------------------------------------------+
int               CUtilites::GetNextObjectNumber(
   const string prefix,
   const ENUM_OBJECT object_type,
   bool true
)
  {
   int count = ObjectsTotal(0,0,object_type),
       i,
       current_element_number,
       total_elements = 0;
   string current_element_name = "",
          comment_text = "";
//---
   if(only_prefixed)
     {
      for(i=0; i<count; i++)
        {
         current_element_name=ObjectName(0,i,0,object_type);
         if(StringSubstr(current_element_name,0,StringLen(prefix))==prefix)
           {
            current_element_number=
               (int)StringToInteger(
                  StringSubstr(current_element_name,
                               StringLen(prefix),
                               -1)
               );
            if(current_element_number!=total_elements)
              {
               break;
              }
            total_elements++;
           }
        }
     }
   else
     {
      total_elements = ObjectsTotal(0,-1,object_type);
      do
        {
         current_element_name = GetCurrentObjectName(
                                   prefix,
                                   object_type,
                                   total_elements
                                );
         if(ObjectFind(0,current_element_name)>=0)
           {
            total_elements++;
           }
        }
      while(ObjectFind(0,current_element_name)>=0);
     }
//---
   return(total_elements);
  } 

O código implementa dois algoritmos de pesquisa. O primeiro (principal para esta biblioteca) itera sobre todos os objetos que correspondem ao tipo e que têm o prefixo especificado. Assim que encontrar um número livre, ele o devolve ao usuário. Isso permite que fechar "buracos" na numeração.

No entanto, este algoritmo não é muito adequado quando há vários objetos com o mesmo número, mas com sufixos diferentes. Nas primeiras versões, quando eu ainda usava scripts para desenhar, chamava o jogo de garfos dessa forma. 

Por isso, a biblioteca implementa um segundo método de pesquisa. Ele pega o número total de objetos de um determinado tipo e verifica se há um nome que comece com o mesmo prefixo e tenha o mesmo número. Nesse caso, o número é incrementado em 1 até que seja encontrado um valor livre.

Bem, e quando o número já existe (ou pode ser facilmente obtido usando uma função), torna-se bastante simples compor um nome.

//+------------------------------------------------------------------+
//| Генерирует имя текущего элемента                                 |
//| Параметры:                                                       |
//|    _prefix - префикс имени для данной группы объектов.           |
//|    _type - тип тех объектов, которые нужно искать.               |
//|    _number - номер текущего объекта, если он уже готов.          |
//| Возвращаемое значение:                                           |
//|    строка имени текущего объекта.                                |
//+------------------------------------------------------------------+
string            CUtilites::GetCurrentObjectName(
   string _prefix,
   ENUM_OBJECT _type=OBJ_TREND,
   int _number = -1
)
  {
   int Current_Line_Number;

//--- Дополнение к префиксу - текущий таймфрейм
   string Current_Line_Name=IntegerToString(PeriodSeconds()/60)+"_"+_prefix;

//--- Получаем номер элемента
   if(_number<0)
     {
      Current_Line_Number = GetNextObjectNumber(Current_Line_Name,_type);
     }
   else
     {
      Current_Line_Number = _number;
     }

//--- Генерируем имя
   Current_Line_Name +=IntegerToString(Current_Line_Number,4,StringGetCharacter("0",0));

//---
   return (Current_Line_Name);
  } 

Distância entre barras adjacentes (em pixels)

Às vezes é necessário calcular a distância até um determinado ponto no futuro. Uma das maneiras mais confiáveis de obter esse d é calcular a distância em pixels entre duas barras adjacentes e, em seguida, multiplicá-la pelo coeficiente desejado (quantas barras queremos diminuir).

A distância entre as barras adjacentes pode ser calculada usando a seguinte função:

//+------------------------------------------------------------------+
//| Вычисляет расстояние в пикселах между двумя соседними барами     |
//+------------------------------------------------------------------+
int        CUtilites::GetBarsPixelDistance(void)
  {
   double price; // произвольная цена на графике (нужна для
                 // стандартных функций вычисления координат
   datetime time1,time2;  // время текущей и соседней свечей
   int x1,x2,y1,y2;       // экранные координаты двух точек
                          //   на соседних свечах
   int deltha;            // результат - искомое расстояние

//--- Начальные установки
   price = iHigh(Symbol(),PERIOD_CURRENT,CMouse::Bar());
   time1 = CMouse::Time();

//--- Получение времени соседней свечи
   if(CMouse::Bar()<Bars(Symbol(),PERIOD_CURRENT)){ // если в середине графика,
      time2 = time1+PeriodSeconds();                // берём свечу слева,
   }
   else {                                           // иначе
      time2 = time1;
      time1 = time1-PeriodSeconds();                // берём свечу справа
   }

//--- Преобразование координат из значений цена/время в экранные пикселы
   ChartTimePriceToXY(0,0,time1,price,x1,y1);
   ChartTimePriceToXY(0,0,time2,price,x2,y2);

//--- Вычисление расстояния
   deltha = MathAbs(x2-x1);
//---
   return (deltha);
  } 

Função que converte uma string numa matriz double

A maneira mais fácil de ajustar os níveis de Fibonacci usando os parâmetros do EA é usar strings que consistem em valores separados por vírgulas. No entanto, a MQL exige que o para definir o nível sejam usados números double.

A função a seguir ajudará a extrair números de uma string.

//+------------------------------------------------------------------+
//| Преобразует строку с разделителями в массив двоичных (double)    |
//|    чисел                                                         |
//| Параметры:                                                       |
//|    _haystack - исходная строка для конвертации                   |
//|    _result[] - результирующий массив                             |
//|    _delimiter - символ-разделитель                               |
//+------------------------------------------------------------------+
static void CUtilites::StringToDoubleArray(
   string _haystack,             // исходная строка
   double &_result[],            // массив результатов
   const string _delimiter=","   // символ-разделитель
)
  {
//---
   string haystack_pieces[]; // Массив фрагментов строки
   int pieces_count,         // Количество фрагментов
       i;                    // Счётчик
   string current_number=""; // Текущий фрагмент строки (предполагаемое число)

//--- Разбиение строки на фрагменты
   pieces_count=StringSplit(_haystack,StringGetCharacter(_delimiter,0),haystack_pieces);
//--- Конвертация
   if(pieces_count>0)
     {
      ArrayResize(_result,pieces_count);
      for(i=0; i<pieces_count; i++)
        {
         StringTrimLeft(haystack_pieces[i]);
         StringTrimRight(haystack_pieces[i]);
         _result[i]=StringToDouble(haystack_pieces[i]);
        }
     }
   else
     {
      ArrayResize(_result,1);
      _result[0]=0;
     }
  } 

Classe de desenho: exemplo de uso de funções utilitárias

Como artigo acabará por ser muito grande, farei uma descrição da maioria das funções de desenho na próxima parte. No entanto, a fim de testar as capacidades de algumas funções já criadas, darei aqui um código que ajuda a desenhar linhas retas simples (com base em dois extremos adjacentes).

Cabeçalho do arquivo de desenho de gráfico Graphics.mqh

//+------------------------------------------------------------------+
//|                                                     Graphics.mqh |
//|                        Copyright 2020, MetaQuotes Software Corp. |
//|                            https://www.mql5.com/ru/articles/7468 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2020, MetaQuotes Software Corp."
#property link      "https://www.mql5.com/ru/articles/7468"

//+------------------------------------------------------------------+
//| Класс для рисования графических объектов                         |
//+------------------------------------------------------------------+
class CGraphics
  {
   //--- Поля
private:
   bool              m_Is_Trend_Ray;
   bool              m_Is_Change_Timeframe_On_Create;
   //--- Методы
private:
   //--- Устанавливает общие параметры для любого вновь созданного объекта
   void              CurrentObjectDecorate(
      const string _name,
      const color _color=clrNONE,
      const int _width = 1,
      const ENUM_LINE_STYLE _style = STYLE_SOLID
   );
public:
   //--- Конструктор по умолчанию
                     CGraphics();
   //--- Универсальная функция для создания трендовых линий с заданными параметрами
   bool              TrendCreate(
      const long            chart_ID=0,        // ID графика
      const string          name="TrendLine",  // имя линии
      const int             sub_window=0,      // номер подокна
      datetime              time1=0,           // время первой точки
      double                price1=0,          // цена первой точки
      datetime              time2=0,           // время второй точки
      double                price2=0,          // цена второй точки
      const color           clr=clrRed,        // цвет линии
      const ENUM_LINE_STYLE style=STYLE_SOLID, // стиль линии
      const int             width=1,           // толщина линии
      const bool            back=false,        // на заднем плане
      const bool            selection=true,    // выбрана ли линия
      const bool            ray_right=false,   // луч вправо
      const bool            hidden=true,       // скрывать в списке объектов
      const long            z_order=0          // приоретет щелчка мыши (Z-Index)
   );
   //--- Отрисовывает трендовую линию по двум ближайшим (справа от мыши) экстремумам
   void              DrawTrendLine(void);
   //--- Проверяет, является ли прямая лучом
   bool              IsRay() {return m_Is_Trend_Ray;}
   //--- Устанавливает признак луча (продлевать ли прямую вправо)
   void              IsRay(bool _is_ray) {m_Is_Trend_Ray = _is_ray;}
   //--- Проверяет, менять ли отображение создаваемых программой объектов на старших таймфреймах
   bool              IsChangeTimeframe(void) {return m_Is_Change_Timeframe_On_Create;}
   //--- Устанавливает признак отображения создаваемых программой объектов на старших таймфреймах
   void              IsChangeTimeframe(bool _is_tf_change) {m_Is_Change_Timeframe_On_Create = _is_tf_change;}
  };
  

Função que define parâmetros gerais para qualquer objeto gráfico recém-criado

//+------------------------------------------------------------------+
//| Устанавливает общие параметры для всех новых объектов            |
//| Параметры:                                                       |
//|    _name - имя изменяемого объекта                               |
//|    _color - цвет изменяемого объекта. Если не задан,             |
//|             используется "стандартный" цвет текущего периода     |
//|    _width - ширина линии объекта                                 |
//|    _style - тип линии объекта                                    |
//+------------------------------------------------------------------+
void CGraphics::CurrentObjectDecorate(
   const string _name,            // имя изменяемого объекта
   const color _color=clrNONE,    // цвет изменяемого объекта
   const int _width = 1,          // ширина линии объекта
   const ENUM_LINE_STYLE _style = STYLE_SOLID  // тип линии объекта
)
  {
   long timeframes;         // таймфреймы, на которых будет отображён объект
   color currentColor;      // цвет объекта
//--- Уточнение таймфреймов, на которых будет отображен объект
   if(Is_Change_Timeframe_On_Create)
     {
      timeframes = CUtilites::GetAllLowerTimeframes();
     }
   else
     {
      timeframes = OBJ_ALL_PERIODS;
     }
//--- Уточнение цвета объекта
   if(_color != clrNONE)
     {
      currentColor = _color;
     }
   else
     {
      currentColor = CUtilites::GetTimeFrameColor(timeframes);
     }
//--- Задание атрибутов
   ObjectSetInteger(0,_name,OBJPROP_COLOR,currentColor);            // Цвет
   ObjectSetInteger(0,_name,OBJPROP_TIMEFRAMES,timeframes);         // Периоды
   ObjectSetInteger(0,_name,OBJPROP_HIDDEN,true);                   // Скрывать в списке объектов
   ObjectSetInteger(0,_name,OBJPROP_SELECTABLE,true);               // Возможность выделять
   ObjectSetInteger(0,_name,OBJPROP_SELECTED,Is_Select_On_Create);  // Оставлять ли выделенным после создания
   ObjectSetInteger(0,_name,OBJPROP_WIDTH,_width);                  // Толщина линии
   ObjectSetInteger(0,_name,OBJPROP_STYLE,_style);                  // Стиль линии
  } 

Funções de desenho de retas

//+------------------------------------------------------------------+
//| Универсальная функция для создания трендовых линий с заданными   |
//|    параметрами                                                   |
//| Параметры:                                                       |
//|    chart_ID - ID графика                                         |
//|    name - имя линии                                              |
//|    sub_window - номер подокна                                    |
//|    time1 - время первой точки                                    |
//|    price1 - цена первой точки                                    |
//|    time2 - время второй точки                                    |
//|    price2 - цена второй точки                                    |
//|    clr - цвет линии                                              |
//|    style - стиль линии                                           |
//|    width - толщина линии                                         |
//|    back - на заднем плане                                        |
//|    selection - выбрана ли линия                                  |
//|    ray_right - луч вправо                                        |
//|    hidden - скрывать в списке объектов                           |
//|    z_order - приоретет щелчка мыши (Z-Index)                     |
//| Возвращаемое значение:                                           |
//|    признак удачного завершения. Если нарисовать линию не вышло,  |
//|    вернёт false, иначе - true                                    |
//+------------------------------------------------------------------+
bool              CGraphics::TrendCreate(
      const long            chart_ID=0,        // ID графика
      const string          name="TrendLine",  // имя линии
      const int             sub_window=0,      // номер подокна
      datetime              time1=0,           // время первой точки
      double                price1=0,          // цена первой точки
      datetime              time2=0,           // время второй точки
      double                price2=0,          // цена второй точки
      const color           clr=clrRed,        // цвет линии
      const ENUM_LINE_STYLE style=STYLE_SOLID, // стиль линии
      const int             width=1,           // толщина линии
      const bool            back=false,        // на заднем плане
      const bool            selection=true,    // выбрана ли линия
      const bool            ray_right=false,   // луч вправо
      const bool            hidden=true,       // скрывать в списке объектов
      const long            z_order=0          // приоретет щелчка мыши (Z-Index)
)
  {

//--- Сбросить последнее сообщение об ошибке
   ResetLastError();
//--- Создаём линию
   if(!ObjectCreate(chart_ID,name,OBJ_TREND,sub_window,time1,price1,time2,price2))
     {
      if(Print_Warning_Messages) // Если не получилось, сообщаем об ошибке
        {
         Print(__FUNCTION__,
               ": Can't create trend line! Error code = ",GetLastError());
        }
      return(false);
     }

//--- Задаём дополнительные атрибуты
   CurrentObjectDecorate(name,clr,width,style);

//--- линия на переднем (false) или на заднем (true) плане?
   ObjectSetInteger(chart_ID,name,OBJPROP_BACK,back);
//--- луч вправо (true) или чёткие границы (false)?
   ObjectSetInteger(chart_ID,name,OBJPROP_RAY_RIGHT,ray_right);
//--- приоретет кликов мыши (Z-index)
   ObjectSetInteger(chart_ID,name,OBJPROP_ZORDER,z_order);
//--- Обновим картинку графика
   ChartRedraw(0);
//--- Всё в порядке. Линия успешно нарисована.
   return(true);
  }

Será com base nesta função comum que criaremos outra função que desenha uma linha reta com base em dois extremos adjacentes.

//+------------------------------------------------------------------+
//| Рисует трендовую линию по ближайшим справа экстремумам           |
//+------------------------------------------------------------------+
void              CGraphics::DrawTrendLine(void)
  {
   int dropped_bar_number=CMouse::Bar(); // номер свечи под мышью
   int p1=0,p2=0;                        // номера первой и второй точек
   string trend_name =                   // имя линии тренда
      CUtilites::GetCurrentObjectName(Trend_Line_Prefix,OBJ_TREND);
   double
      price1=0,   // цена первой точки
      price2=0,   // цена второй точки
      tmp_price;  // переменная для временного хранения цены
   datetime
      time1=0,    // Время первой точки
      time2=0,    // Время второй точки
      tmp_time;
   int x1,x2,y1,y2;   // Координаты точек
   int window=0;      // Номер подокна

//--- Установка начальных параметров
   if(CMouse::Below()) // Если мышь под Low свечи
     {
      //--- Находим два экстремума снизу
      CUtilites::SetExtremumsBarsNumbers(false,p1,p2);

      //--- Определяем координаты точек
      time1=iTime(Symbol(),PERIOD_CURRENT,p1);
      price1=iLow(Symbol(),PERIOD_CURRENT,p1);
      time2=iTime(Symbol(),PERIOD_CURRENT,p2);
      price2=iLow(Symbol(),PERIOD_CURRENT,p2);
     }
   else // иначе
      if(CMouse::Above()) // если мышь над High свечи
        {
         //--- Находим два экстремума сверху
         CUtilites::SetExtremumsBarsNumbers(true,p1,p2);
        
         //--- Определяем координаты точек
         time1=iTime(Symbol(),PERIOD_CURRENT,p1);
         price1=iHigh(Symbol(),PERIOD_CURRENT,p1);
         time2=iTime(Symbol(),PERIOD_CURRENT,p2);
         price2=iHigh(Symbol(),PERIOD_CURRENT,p2);

        }
//--- Отрисовка линии
   TrendCreate(0,trend_name,0,
               time1,price1,time2,price2,
               CUtilites::GetTimeFrameColor(CUtilites::GetAllLowerTimeframes()),
               0,Trend_Line_Width,false,true,m_Is_Trend_Ray
              );

//--- Перерисовка графика
   ChartRedraw(0);
  } 

Quero mais uma vez chamar sua atenção para a chamada de função CUtilites::SetExtremumsBarsNumbers, que obtém os números das barras para os pontos 1 e 2. Seu código foi descrito acima. O resto me parece óbvio e não requer uma longa descrição.

A função final desenha uma linha simples com base em dois pontos. Dependendo do parâmetro global Is_Trend_Ray, (descrito no arquivo "GlobalVariables.mqh") a linha será um raio estendido para a direita ou apenas um segmento curto entre dois extremos.

 Is_Trend_Ray = true  Is_Trend_Ray = false

Também geraremos a capacidade de alternar a extensão da linha.


Criação de um bloco de controle: configurando o método OnChartEvent

Agora que foram escritas as funções básicas, é possível personalizar seus atalhos de teclado.

Em "Shortcuts.mqh" escrevemos o método CShortcuts::OnChartEvent.

//+------------------------------------------------------------------+
//| Функция обработки событий                                        |
//+------------------------------------------------------------------+
void CShortcuts::OnChartEvent(
   const int id,
   const long &lparam,
   const double &dparam,
   const string &sparam
)
  {
//---
   switch(id)
     {
      //--- Сохранить координаты курсора мыши
      case CHARTEVENT_MOUSE_MOVE:
         CMouse::SetCurrentParameters(id,lparam,dparam,sparam);
         break;

      //--- Обработка нажатий клавиш
      case CHARTEVENT_KEYDOWN:
         //--- Изменить таймфрейм на 1 уровень вверх
         if(CUtilites::GetCurrentOperationChar(Up_Key) == lparam)
           {
            CUtilites::ChangeTimeframes(true);
           };
         //--- Изменить таймфрейм на 1 уровень вниз
         if(CUtilites::GetCurrentOperationChar(Down_Key) == lparam)
           {
            CUtilites::ChangeTimeframes(false);
           };
         //--- Изменить Z-Index графика (график поверх всех объектов)
         if(CUtilites::GetCurrentOperationChar(Z_Index_Key) == lparam)
           {
            CUtilites::ChangeChartZIndex();
           }
         //--- Нарисовать трендовую линию
         if(CUtilites::GetCurrentOperationChar(Trend_Line_Key) == lparam)
           {
            m_graphics.DrawTrendLine();
           }
         //--- Переключить параметр Is_Trend_Ray
         if(CUtilites::GetCurrentOperationChar(Switch_Trend_Ray_Key) == lparam)
           {
            m_graphics.IsRay(!m_graphics.IsRay());
           }

         break;
         //---

     }
  } 

Teclas usadas na implementação atual da biblioteca

Ação
 Teclas Da palavra em inglês
 Ir um timeframe acima segundo os períodos principais (do painel de períodos) U  Up
 Ir um timeframe abaixo  D  Down
 Alterar o nível Z do gráfico (gráfico acima ou abaixo dos objetos)  Z  Z-order
 Desenhar linha de tendência inclinada segundo dois extremos unidirecionais mais próximos do mouse  T  Trend line
 Alternar modo raio para novas retas
 R  Ray

Fim do artigo

O arquivo anexado contém um arquivo da versão atual da biblioteca. Também há três scripts no arquivo.

É melhor anexar essa biblioteca a um Expert Advisor, e não a um indicador, porque se anexarmos a um indicador e tentar usar este indicador junto com algum outro Expert Advisor, haverá congelamentos terríveis. Em qualquer caso, isso aconteceu comigo. Talvez eu não saiba prepará-lo?

E, finalmente, falarei sobre planos.

A segunda versão da biblioteca conterá, de fato, o desenho daqueles objetos úteis que são mostrados no vídeo. Quanto a alguns objetos que são primitivos (linha vertical ou horizontal), como linhas com o comprimento que preciso, tive de pensar muito, porque eles nem sempre funcionam perfeitamente por causa do "erro de fim de semana" ou por algum outro motivo. Conterei sobre minhas decisões e, é claro, terei todo o prazer em receber críticas construtivas.

A terceira versão conterá uma interface gráfica para configurar os parâmetros das ferramentas gráficas.

A quarta - se eu conseguir - já será sobre um EA de acompanhamento completo que facilitará a negociação manual. Aqui está uma pergunta para a comunidade. Não tenho certeza se aplicarei novas soluções em comparação com as contrapartes existentes. Certamente, eu mesmo desenharei a interface, e posso ouvir desejos, mas negociar com as mãos é negociar com as mãos ☺. Por isso, alguém precisa disso?