Canal universal com GUI

Dmitry Fedoseev | 6 março, 2017


Conteúdo

Introdução

Anteriormente já foi escrito um artigo sobre a criação de um oscilador universal com uma interface gráfica do usuário. O resultado foi um indicador muito interessante, conveniente e útil que simplificava e acelerava consideravelmente a análise de gráficos. Além do oscilador, existem outros tipos de indicadores de análise técnica que representem não menos interessante do que os osciladores. Eles são indicadores de tendência, indicadores de volatilidade e de volumes que, por sua vez, também podem ser divididos em diferentes categorias. Neste artigo, aprenderemos como criar um indicador universal de canal.

A tarefa revelou-se bastante desafiadora e, talvez, mais apropriada para programadores avançados, e não para iniciantes. Como o tema deste artigo está muito relacionado com a criação de um oscilador universal, o canal universal será criado com base no oscilador universal, a fim de não considerar duas vezes questões gerais. Assim, até mesmo programadores iniciantes podem criar seus indicadores universais através de algumas melhorias, sem entrar em todas as nuances quanto à criação de indicadores universais com uma interface gráfica do usuário.

Apesar da semelhança com o oscilador universal, encontraremos algumas diferenças fundamentais. Todos os indicadores de canais apresentam três linhas, isto é: central, superior e inferior. A linha central, quanto à sua plotagem, é idêntica à média móvel. Na maioria dos casos, para a plotagem do canal, é utilizada a média móvel. As linhas superior e inferior são equidistantes da linha central. Esta distância pode ser determinada simplesmente em pontos, em porcentagem do preço (indicador Envelopes), pode ser usado o valor do desvio padrão (bandas de Bollinger), podem ser empregado o valor do indicador de ATR (canal Keltner). Isso significa que o indicador de canal será plotado usando dois blocos independentes:

  1. Bloco de cálculo para a linha central
  2. Bloco de definição de largura de canal (ou plotagem das bordas)

Existem outros tipos de canais um pouco diferentes, em especial, o canal Donchian (canal de preço). Geralmente, durante sua criação, primeiro são plotadas as linhas extremas (faixa de preço) e, em seguida, é calculado o valor da linha central (no meio da faixa). Mas, e este canal pode ser plotado de acordo com o sistema acima, isto é: primeiro é construída a linha central - definida como o ponto médio da faixa de preço - e, depois, são terminadas as bordas a uma distância de metade da faixa de preço. Claro, são requeridos mais cálculos do que durante plotagem habitual. No entanto, como o objetivo principal deste artigo é a criação de um indicador universal, você pode permitir algumas exceções, especialmente quando esta abordagem aumenta o número de combinações possíveis da linha central e bordas. Por exemplo, é possível obter um indicador com uma linha central como a do canal de preço, mas com bordas dentro do desvio padrão, como as bandas de Bollinger, etc. 

Tipos de linha central

Para a linha central, serão utilizadas diferentes médias móveis. Nós definimos seus tipos e número de parâmetros. Todas as variantes da linha central que serão usadas ​​no indicador são mostradas na tabela 1.

Tabela 1. Tipos de linha central

Função
padrão
Nome Parâmetros
iAMA Adaptive Moving Average 1. int ama_period — período AMA 
2. int fast_ma_period — período da média rápida
3. int slow_ma_period — período da média lenta
4 int ama_shift — deslocamento horizontal do indicador
5. ENUM_APPLIED_PRICE  applied_price — tipos de preço ou handle 
iDEMA Double Exponential Moving Average 1. int ma_period — período de média
2. int ma_shift — deslocamento horizontal do indicador
3. ENUM_APPLIED_PRICE  applied_price — tipo de preço
iFrAMA Fractal Adaptive Moving Average 1. int ma_period — período de média
2. int ma_shift — deslocamento horizontal do indicador
3. ENUM_APPLIED_PRICE  applied_price — tipo de preço
iMA Moving Average 1. int ma_period — período de média
2. int ma_shift — deslocamento horizontal do indicador
3. ENUM_MA_METHOD ma_method — tipo de suavização
4 ENUM_APPLIED_PRICE applied_price — tipo de preço
iTEMA Triple Exponential Moving Average 1. int ma_period — período de média
2. int ma_shift — deslocamento horizontal do indicador
3. ENUM_APPLIED_PRICE  applied_price — tipo de preço
iVIDyA Variable Index Dynamic Average 1. int cmo_period — período do Chande Momentum 
2. int ema_period — período do fator de suavização
3. int ma_shift — deslocamento horizontal do indicador
4 ENUM_APPLIED_PRICE  applied_price — tipo de preço
- linha central do canal de preço 1. int period

Com base na análise da coluna "Parâmetros" a partir da tabela 1, obtemos o conjunto mínimo requerido de parâmetros (tabela 2).

Tabela 2. Conjunto universal de parâmetros para o cálculo da linha central do canal 

Tipo Nome
int period1
int period2
int period3
int shift
ENUM_MA_METHOD ma_method 
ENUM_APPLIED_PRICE  price
Na janela de propriedades do indicador, os parâmetros de linha central será prefixado com "c_". 

Tipos de bordas

Assim como com a linha central do canal, definimos as variantes para o cálculo das bordas do canal (tabela 3).

Tabela 3. Variantes para o cálculo da largura do canal 

Padrão
padrão
Nome  Parâmetros
iATR Average True Range 1. int ma_period — período de média
iStdDev  Desvio Padrão 1. int ma_period — período de média
2. int ma_shift — deslocamento horizontal do indicador
3. ENUM_MA_METHOD — tipo de suavização
4 ENUM_APPLIED_PRICE applied_price — tipo de preço 
em pontos  int width — largura em pontos 
em porcentagem (como o Envelopes)  double width — largura em porcentagem a partir do preço   
como o canal de preço  double width — fator de dimensionamento em relação à largura real do canal de preço

Com base na coluna "Parâmetros" - na tabela 3 - obtemos o conjunto necessário de parâmetros (tabela 4).

Tabela 4. Conjunto universal de parâmetros para o cálculo da largura do canal

Tipo Nome
int period
int  shift 
ENUM_MA_METHOD  ma_method  
ENUM_APPLIED_PRICE  price
double  width 

Durante o cálculo em pontos, é necessária uma variável do tipo int, mas ela não existe na tabela 4, porque, em vez dela, pode ser utilizada a variável do tipo double. Isso faz com que o número total de variáveis ​​diminua, na janela de propriedades.

Na janela de propriedades do indicador, os parâmetros de cálculo de bordas terá o prefixo "W_". 

Classes de linha central

Quanto ao seu princípio e conjunto de métodos, a classe base da linha central é idêntica à classe CUniOsc do artigo sobre o oscilador universal com interface gráfica do usuário, por isso ela é tomada como uma base e, a seguir, é modificada um pouco.

Na pasta MQL5/Include, criamos a pasta UniChannel, copiamos e colamos nela o arquivo CUniOsc.mqh (a partir da pasta Include/UniOsc) e mudamos seu nome para CUniChannel.mqh. No arquivo, deixamos a classe base (COscUni), classe filha Calculate1 (nome completo COscUni_Calculate1) e sua classe filha COscUni_ATR, removemos as outras classes.

Renomeamos as classes: substituímos o fragmento "COscUni" com "CChannelUni". Para este fim. é conveniente usar a função do editor (Menu Principal — Editar — Localizar e substituir — Substituir), tendo em conta não pressionar "Substituir tudo", mas sim realizar a substituição individualmente para monitorar o processo e ter certeza de que todas as substituições são feitas apenas quando necessário.

A classe da linha central sempre desenha uma linha contínua, por isso muitos dos métodos da classe base não são necessários. Após a remoção dos métodos desnecessários, fica a seguinte classe base:

class CChannelUniWidth{
   protected:
      int m_handle;      // identificador do indicador
      string m_name;     // nome do indicador
      string m_label1;   // nome do buffer 1      
      string m_help;     // pequeno guia sobre os parâmetros do indicador
      double m_width;    // largura do canal
   public:
  
      // construtor
      void CChannelUniWidth(){
         m_handle=INVALID_HANDLE;
      }
      
      // destrutor
      void ~CChannelUniWidth(){
         if(m_handle!=INVALID_HANDLE){
            IndicatorRelease(m_handle);
         }
      }
  
      // método principal a ser chamado a partir da função OnCalculate() do indicador
      virtual int Calculate( const int rates_total,
                     const int prev_calculated,
                     double & bufferCentral[],
                     double & bufferUpper[],
                     double & bufferLower[],
      ){
         return(rates_total);
      }
      
      // obtenção do identificador do indicador carregado
      int Handle(){
         return(m_handle);
      }
      
      // método de verificação do identificador, para saber se foi carregado o indicador  
      bool CheckHandle(){
         return(m_handle!=INVALID_HANDLE);
      }
      
      // obtenção do nome do indicador
      string Name(){
         return(m_name);
      }    

      // obtenção do texto das etiquetas para os buffers
      string Label1(){
         return(m_label1);
      }
      
      // obtenção das dicas sobre os parâmetros
      string Help(){
         return(m_help);
      }
};

É possível apagar - a partir da classe Calculate - tudo relacionado com o segundo buffer, após isso, na classe, ficará o método Calculate:

class CChannelUni_Calculate1:public CChannelUni{
   public:
      // método principal a ser chamado a partir da função OnCalculate() do indicador
      // os primeiros dois parâmetros são semelhantes aos primeiros dois parâmetros
      // função OnCalculate() do indicador
      // o terceiro parâmetro é o buffer de indicador para a linha central
      int Calculate( const int rates_total,    
                     const int prev_calculated,
                     double & buffer0[]
      ){
        
         // definição do número de elementos a serem copiados
        
         int cnt;
        
         if(prev_calculated==0){
            cnt=rates_total;
         }
         else{
            cnt=rates_total-prev_calculated+1;
         }  
        
         // cópia de dados para o buffer de indicador
         if(CopyBuffer(m_handle,0,0,cnt,buffer0)<=0){
            return(0);
         }
        
         return(rates_total);
      }
};

Escrevemos a classe filha que usa o indicador iMA. Mudamos o nome da classe CChannelUni_ATR - localizado no arquivo - para CChannelUni_MA, substituímos nele o indicador a ser chamado e apagamos o restante. O resultado é a seguinte classe:

class CChannelUni_MA:public CChannelUni_Calculate1{
   public:
   // construtor
   // os primeiros dois parâmetros são iguais para todas as classes filhas
   // a seguir, os parâmetros do indicador carregado
   void CChannelUni_MA( bool use_default,
                        bool keep_previous,
                        int & ma_period,
                        int & ma_shift,
                        long & ma_method,
                        long & ma_price){
      if(use_default){ // é selecionado o uso de valores por padrão
         if(keep_previous){
            // não se deve alterar os parâmetros usados anteriormente
            if(ma_period==-1)ma_period=14;
            if(ma_shift==-1)ma_shift=0;
            if(ma_method==-1)ma_method=MODE_SMA;
            if(ma_price==-1)ma_price=PRICE_CLOSE;
         }
         else{
            ma_period=14;
            ma_shift=0;
            ma_method=MODE_SMA;
            ma_price=PRICE_CLOSE;            
         }      
      }    
      
      // carregamento do indicador
      m_handle=iMA(Symbol(),Period(),ma_period,ma_shift,(ENUM_MA_METHOD)ma_method,(ENUM_APPLIED_PRICE)ma_price);
      
      // geração da cadeia de caracteres como o nome do indicador
      m_name=StringFormat( "iMA(%i,%i,%s,%s)",
                           ma_period,
                           ma_shift,        
                           EnumToString((ENUM_MA_METHOD)ma_method),              
                           EnumToString((ENUM_APPLIED_PRICE)ma_price)
                        );

      
      // cadeia de caracteres para o nome do buffer
      m_label1=m_name;

      // dica sobre os parâmetros
      m_help=StringFormat( "ma_period - c_Period1(%i), "+
                           "ma_shift - c_Shift(%i), "+
                           "ma_method - c_Method(%s)"+
                           "ma_price - c_Price(%s)",
                           ma_period,
                           ma_shift,
                           EnumToString((ENUM_MA_METHOD)ma_method),
                           EnumToString((ENUM_APPLIED_PRICE)ma_price)
                           );
   }
};

Consideremos a formação de cadeias de caracteres nas variáveis m_name e mlabel1. O nome do indicador (variável m_name), nos indicadores que estão localizados na sub-janela, é visível no canto superior esquerdo da sub-janela. Como o canal será exibido no gráfico do preço, seu nome será visível, de modo que a variável m_label adota exatamente os mesmo nome detalhado como a variável m_name, para que quando você passa o mouse sobre o canal central na dica sejam vistos todos seus parâmetros. 

Assim como para a classe do indicador IMA, são criadas classes para todos os outros indicadores padrão. O canal de preço é uma exceção. Como o canal de preço não está entre os indicadores padrão, é necessário levar a cabo seu cálculo. Podem haver duas variantes:

  1. Criar um tipo de classe filha Calculate e realizar cálculos nela
  2. Escrever um indicador adicional e chamá-lo através da função iCustom
Ambas as variantes têm seu direito de existir. No primeiro caso, é reduzido o número de arquivos dos quais depende o indicador a ser criado no artigo, mas será exigida uma nova execução do mesmo cálculo (primeiro, a definição de bordas para o cálculo da linha média do canal, e, em seguida, outra vez a definição das bordas do canal para determinar a largura de canal). No segundo caso, além de não haver duplicação de cálculos, podemos obter um indicador de canal adicional e completo que pode ser usado de forma independente.      

Anexado ao artigo, encontram-se: o arquivo CUniChannel.mqh, com as classes filhas para todos os outros indicadores, e o indicador iPriceChannel. No indicador iPriceChannel,, os dados das linhas centrais estão localizadas no buffer 0. Se alguém quiser modificar a classe para outro indicador, cujos os dados necessários não estão localizados no buffer de zero, então será necessário criar outra classe filha Calculate ou, na classe base, criar uma variável para o índice do buffer e, no construtor da classe filha, definir para ela o valor desejado.   

Classes de cálculo para a largura e a plotagem do canal

Como fundamento da classe base pegamos novamente na classe CUniChannel. No método Calculate, será transferido o buffer de indicador com os valores da linha central, dois buffers para as bordas cujos canais serão preenchidos com os valores calculados no método. Aqui, ao contrário da CUniChannel, para cada variante de cálculo de bordas, serão suas classes filhas Calcule que carregarão os indicadores e, nas quais, serão formados os nomes do indicador e dos buffers. Ainda é necessária uma pequena edição complementar da classe base: adicionar uma variável para a largura do canal, o valor será definido pelo construtor da classe filha.

Salvamos o arquivo CUniChannel.mqh com o nome CUniChannelWidth.mqh e fazemos nele as alterações. Primeiro, removemos todas as classes filhas, deixando apenas a classe base e a classe Calculate. Mudamos o nome da classe CChannelUni para CChannelUniWidth (não se esqueça de que também precisa mudar o construtor e o destruidor do nome da classe mãe, na classe filha). O resultado é a seguinte classe:

class CChannelUniWidth{
   protected:
      int m_handle;           // indicador do indicador
      string m_name;          // nome do indicador
      string m_label1;        // nome do buffer 1      
      string m_help;          // pequeno quia sobre os parâmetros do indicador
      double m_width;         // largura do canal
   public:
  
      // construtor
      void CChannelUniWidth(){
         m_handle=INVALID_HANDLE;
      }
      
      // destrutor
      void ~CChannelUniWidth(){
         if(m_handle!=INVALID_HANDLE){
            IndicatorRelease(m_handle);
         }
      }
  
      // método principal a ser chamado a partir da função OnCalculate() do indicador
      virtual int Calculate( const int rates_total,
                     const int prev_calculated,
                     double & bufferCentral[],
                     double & bufferUpper[],
                     double & bufferLower[],
      ){
         return(rates_total);
      }
      
      // obtenção do identificador do indicador carregado
      int Handle(){
         return(m_handle);
      }
      
      // método de verificação do identificador, para saber se foi carregado o indicador  
      bool CheckHandle(){
         return(m_handle!=INVALID_HANDLE);
      }
      
      // obtenção do nome do indicador
      string Name(){
         return(m_name);
      }    

      // obtenção do texto das etiquetas para os buffers
      string Label1(){
         return(m_label1);
      }
      
      // obtenção das dicas sobre os parâmetros
      string Help(){
         return(m_help);
      }
};

Mudamos o nome da classe CChannelUni_Calculate para CChannelUni_Calculate_ATR e adicionamos nela o construtor. O construtor pode ser tomado a partir da classe COscUni_ATR do oscilador universal, mas ele deve ser renomeado e acrescentado com o parâmetro de largura. E serão necessárias outras alterações: você precisa adicionar a formação de nomes do indicador e dos buffers. Finalmente, a classe para o cálculo das bordas com base no indicador ATR é como se segue:

class CChannelUni_Calculate_ATR:public CChannelUniWidth{
   public:
      // construtor
      // os primeiros dois parâmetros são iguais para todas as classes filhas
      // a seguir, os parâmetros do indicador carregado
      // último parâmetro: largura do canal
      void CChannelUni_Calculate_ATR(bool use_default,
                                     bool keep_previous,
                                     int & ma_period,
                                     double & ch_width){
         if(use_default){ // é selecionado o uso de valores por padrão
            if(keep_previous){ // não se devem alterar os parâmetros usados anteriormente
               if(ma_period==-1)ma_period=14;
               if(ch_width==-1)ch_width=2;
            }
            else{
               ma_period=14;
               ch_width=2;
            }      
         } 
         
         // armazenamento de parâmetro de largura para utilizar no método de cálculo  
         m_width=ch_width; 
         // carregamento do indicador
         m_handle=iATR(Symbol(),Period(),ma_period);
         // geração da cadeia de caracteres com o nome do indicador
         m_name=StringFormat("ATR(%i)",ma_period);
         // cadeias de caracteres com nome dos buffers
         m_label1=m_name;
         // dicas sobre os parâmetros 
         m_help=StringFormat("ma_period - Period1(%i)",ma_period); // dica   
      }   
      
      // método principal a ser chamado a partir da função OnCalculate() do indicador
      // os primeiros dois parâmetros correspondem aos primeiros dois parâmetros
      // Função OnCalculate()
      // em seguida, são transferidos os buffers de indicador 
      int Calculate( const int rates_total,
                     const int prev_calculated,
                     double & bufferCentral[],
                     double & bufferUpper[],
                     double & bufferLower[],
      ){
      
         // definição do inicio do cálculo
         
         int start;
         
         if(prev_calculated==0){
            start=0
         }
         else
            start=prev_calculated-1;
         }  

         // ciclo básico de cálculo e preenchimento de buffers 

         for(int i=start;i<rates_total;i++){
            
            // obtenção do dados do indicador para a barra calculada
            double tmp[1];
            if(CopyBuffer(m_handle,0,rates_total-i-1,1,tmp)<=0){
               return(0);
            }
            
            // multiplicação pelo parâmetro de largura
            tmp[0]*=m_width;   

            // cálculo dos valores das bordas superior e inferior
            bufferUpper[i]=bufferCentral[i]+tmp[0];
            bufferLower[i]=bufferCentral[i]-tmp[0];

         }   
         
         return(rates_total);
      }
};

Por favor, note que o valor do indicador ATR é copiado dentro do ciclo principal para uma barra. Tal forma de realização é, naturalmente, muito mais lenta do que copiar e colar a série de valores no buffer. No entanto, esta abordagem poupa um buffer de indicador, e a perda de velocidade ocorreria apenas ao anexar manualmente o indicador no gráfico. Mas um atraso de alguns décimos de segundo não será insignificante para o utilizador. No testador, no início do teste, no gráfico, há um pequeno número de barras, por isso a perda de tempo com a cópia de dados, para cada barra separadamente, não será perceptível.

Algumas formas de calcular a largura de canal não requerem a utilização de indicadores adicionais. Neste caso, a variável m_handle da classe base adota o valor 0 (diferente do valore INVALID_HANDLE).

Anexado ao artigo, encontra-se o arquivo CUniChannelWidth.mqh com classe filha para todas as variantes de cálculo de canal.   

Criação do indicador de canal universal

Agora, tendo estabelecido as classes acima, você pode criar o indicador de canal universal, mas sem a interface gráfica.

No editor, criamos um novo indicador personalizado chamado iUniChannel. Ao criar o indicador no assistente MQL, selecionamos as funções: OnCalculate(...,open,high,low,close), OnTimer, OnChartEvent, criamos três buffers de tipo Line.

Para selecionar o tipo de linha central e tipo de canal, você deve criar duas enumerações. As transferências será localizadas no arquivo UniChannelDefines.mqh. Criar enumerações de acordo com as tabelas 1 e 3:

// enumeração de tipos de linha central
enum ECType{
   UniCh_C_AMA,
   UniCh_C_DEMA,
   UniCh_C_FrAMA,
   UniCh_C_MA,
   UniCh_C_TEMA,
   UniCh_C_VIDyA,
   UniCh_C_PrCh
};

// enumeração de tipos de bordas
enum EWType{
   UniCh_W_ATR,
   UniCh_W_StdDev,
   UniCh_W_Points,
   UniCh_W_Percents,
   UniCh_W_PrCh
};

A enumeração de tipos de linha central tem o nome ECType, enquanto a enumeração de tipos de largura do canal tem nome EWType. Ligamos ao indicador, o arquivo com enumerações e dois arquivos previamente criados com as classes:

#include <UniChannel/UniChannelDefines.mqh>
#include <UniChannel/CUniChannel.mqh>
#include <UniChannel/CUniChannelWidth.mqh>

Declaramos duas variáveis ​​externas para selecionar os tipos de linha central e a largura do canal e variáveis ​​para os parâmetros de acordo com as tabelas 2 e 4:

// parâmetros da linha central
input ECType               CentralType   =  UniCh_C_MA;
input int                  c_Period1     =  5;
input int                  c_Period2     =  10;
input int                  c_Period3     =  15;
input int                  c_Shift       =  0;
input ENUM_MA_METHOD       c_Method      =  MODE_SMA;
input ENUM_APPLIED_PRICE   c_Price       =  PRICE_CLOSE;
// parâmetros das bordas
input EWType               WidthType     =  UniCh_W_StdDev;
input int                  w_Period      =  20;
input int                  w_Shift       =  0;
input ENUM_MA_METHOD       w_Method      =  MODE_SMA;
input ENUM_APPLIED_PRICE   w_Price       =  PRICE_CLOSE;
input double               w_Width       =  2.0;

Declaramos duas variáveis ​​que, por agora, serão internas, mas na versão com interface gráfica serão exibidas na janela de propriedades:

bool                 UseDefault  =  false;
bool                 KeepPrev    =  false;

No artigo sobre o indicador universal, são descritos em detalhes os nomes de estas variáveis: com a variável UseDefault é ativado um modo em que cada novo indicador selecionado é carregado com parâmetros por padrão, com a variável KeepPrev é ativado um modo em que são armazenados os valores dos parâmetros ao mudar de indicadores. Na versão do indicador sem a GUI, o indicador é carregado com os parâmetros a partir da janela de propriedades, de modo que o valor usedefault é igual a false. Com a variável KeepPrev também é definido o valor false, porque a GUI ainda não está disponível e não há nenhuma indicação da alternância dos indicadores. 

Durante a inicialização do indicador, é necessários preparar os parâmetros. Assim como no oscilador universal, preparamos os parâmetros numa função separada com nome PrepareParameters(), mas primeiro fazemos uma cópia de todas as variáveis ​​externas:

ECType               _CentralType;
int                  _ma_Period1;
int                  _ma_Period2;
int                  _ma_Period3;
int                  _ma_Shift;
long                 _ma_Method;
long                 _ma_Price;
EWType               _WidthType;
int                  _w_Period;
int                  _w_Shift;
long                 _w_Method;
long                 _w_Price;
double               _w_Width;

Em seguida, escrevemos a função de preparação de parâmetros:

void PrepareParameters(){

   _CentralType=CentralType;
   _WidthType=WidthType;
  
   if(UseDefault && KeepPrev){
      _c_Period1=-1;
      _c_Period2=-1;
      _c_Period3=-1;
      _c_Shift=0;
      _c_Method=-1;
      _c_Price=-1;
      _w_Period=-1;
      _w_Shift=0;
      _w_Method=-1;
      _w_Price=-1;
      _w_Width=-1;
   }
   else{  
      _c_Period1=c_Period1;
      _c_Period2=c_Period2;
      _c_Period3=c_Period3;
      _c_Shift=c_Shift;
      _c_Method=c_Method;
      _c_Price=c_Price;
      _w_Period=w_Period;
      _w_Shift=w_Shift;
      _w_Method=w_Method;
      _w_Price=w_Price;
      _w_Width=w_Width;
   }
}

Observe que, ao satisfazer a condição UseDefault && KeepPrev, a todas as variáveis é atribuído o valor -1, enquanto à variável Shift é atribuído o valor 0, uma vez que os valores destas variáveis não são definidos a partir dos objetos do indicador, mas sim através da interface do usuário (da janela de propriedades ou interface gráfica).   

Após a preparação dos parâmetros, você pode criar os objetos para calcular a linha central e o canal. No oscilador universal, para isto estava a função LoadOscillator(). Neste caso, estará a função: LoadCentral() e LoadWidth(), mas, primeiro, declaramos os ponteiros-variáveis:

CChannelUni * central;
CChannelUniWidth * width;

Alguns indicadores têm um parâmetro para o deslocamento horizontal (shift), mas alguns outros não têm, porém, você pode deslocar todos os indicadores. Por isso declaramos uma variável adicional shift0 com valor 0, e enviamo-la para os construtores de classes. O deslocamento também será realizado movendo os buffers de indicador.

Função LoadCentral():

void LoadCentral(){
   switch(_CentralType){ // dependendo do tipo selecionado criamos a classe correspondente
      case UniCh_C_AMA:
         central=new CChannelUni_AMA(  UseDefault,
                                       KeepPrev,
                                       _c_Period1,
                                       _c_Period2,
                                       _c_Period3,
                                       shift0,
                                       _c_Price);
      break;
      case UniCh_C_DEMA:
         central=new CChannelUni_DEMA( UseDefault,
                                       KeepPrev,
                                       _c_Period1,
                                       shift0,
                                       _c_Price);
      break;
      case UniCh_C_FrAMA:
         central=new CChannelUni_FrAMA(UseDefault,
                                       KeepPrev,
                                       _c_Period1,
                                       shift0,
                                       _c_Price);
      break;
      case UniCh_C_MA:
         central=new CChannelUni_MA(   UseDefault,
                                       KeepPrev,
                                       _c_Period1,
                                       shift0,
                                       _c_Method,
                                       _c_Price);
      break;
      case UniCh_C_TEMA:
         central=new CChannelUni_TEMA( UseDefault,
                                       KeepPrev,
                                       _c_Period1,
                                       shift0,
                                       _c_Price);
      break;
      case UniCh_C_VIDyA:
         central=new CChannelUni_VIDyA(UseDefault,
                                       KeepPrev,
                                       _c_Period1,
                                       _c_Period2,
                                       shift0,
                                       _c_Price);
      break;
      case UniCh_C_PrCh:
         central=new CChannelUni_PriceChannel(  UseDefault,
                                                KeepPrev,
                                                _c_Period1);
      break;
   }
}

Uma das variantes para calcular a largura do canal (classe CChannelUni_Calculate_InPoints) tem um parâmetro que é alterado em pontos, enquanto, na classe, é suportada a adaptação do valor deste parâmetro de acordo com o número de casas decimais, nas cotações. Para que a adaptação funcione, ao criar o objeto, para o construtor da classe é necessário envia o multiplicador de parâmetro. Nas cotações com 2 y 4 dígitos, o valor do multiplicador será igual a 1, enquanto, para 3 e 5 dígitos será 10. Nos parâmetros externos declaramos a variável Auto5Digits do tipo bool:

input bool                 Auto5Digits   =  true;

Se Auto5Digits for igual a true, será executada a correção do parâmetro, se for false, será utilizado o valor existente. Logo, abaixo do Auto5Digits, declaramos mais uma variável para o multiplicador:

int mult;

No início da função OnInit(), calculamos o valor mult:

   if(Auto5Digits && (Digits()==3 || Digits()==5)){
      mult=10; // vamos multiplicar os parâmetros - que são alterados em pontos - por 10
   }
   else{
      mult=1; // os parâmetros - que são alterados em pontos - permanecem inalterados
   }

Agora escrevemos a função LoadWidth():

void LoadWidth(){
   switch(_WidthType){ // dependendo do tipo selecionado criamos a classe correspondente
      case UniCh_W_ATR:
         width=new CChannelUni_Calculate_ATR(UseDefault,KeepPrev,_w_Period,_w_Width);
      break;
      case UniCh_W_StdDev:
         width=new CChannelUni_Calculate_StdDev(UseDefault,KeepPrev,_w_Period,shift0,_w_Method,_w_Price,_w_Width);
      break;
      case UniCh_W_Points:
         width=new CChannelUni_Calculate_InPoints(UseDefault,KeepPrev,_w_Width,mult);
      break;
      case UniCh_W_Percents:
         width=new CChannelUni_Calculate_Envelopes(UseDefault,KeepPrev,_w_Width);
      break;
      case UniCh_W_PrCh:
         width=new CChannelUni_Calculate_PriceChannel(UseDefault,KeepPrev,_w_Period,_w_Width);
      break;
   }
}

Após criar cada um dos objetos (a linha central e largura), verificaremos o sucesso de sua criação. Se os objetos são criados, definimos o nome curto do indicador e, através da função Print(), obtemos a dica sobre os parâmetros: 

Print("Central line parameters matching:",central.Help());
Print("Width parameters matching:",width.Help());  

A definição de rótulos e deslocamentos para buffers é realizada na função SetStyles():

void SetStyles(){

   // nome dos buffers
   PlotIndexSetString(0,PLOT_LABEL,"Central: "+central.Label1());
   PlotIndexSetString(1,PLOT_LABEL,"Upper: "+width.Label1());  
   PlotIndexSetString(2,PLOT_LABEL,"Lower: "+width.Label1());  
  
   // deslocamento dos buffers
   PlotIndexSetInteger(0,PLOT_SHIFT,_c_Shift);
   PlotIndexSetInteger(1,PLOT_SHIFT,_w_Shift);
   PlotIndexSetInteger(2,PLOT_SHIFT,_w_Shift);

}

Como resultado, obtemos a seguinte função OnInit():

int OnInit(){
  
   // preparação do multiplicador para corregir o parâmetro que é alterado em pontos
   if(Auto5Digits && (Digits()==3 || Digits()==5)){
      mult=10;
   }
   else{
      mult=1;
   }
  
   // preparação de parâmetros
   PrepareParameters();
  
   // carregamento do indicador de linha central
   LoadCentral();
  
   // verificação do sucesso de carregamento de linha central
   if(!central.CheckHandle()){
      Alert("Central line error "+central.Name());
      return(INIT_FAILED);
   }    
  
   // carregamento do indicador de cálculo de largura
   LoadWidth();
  
   // verificação do sucesso de carregamento de indicador de largura
   if(!width.CheckHandle()){
      Alert("Width error "+width.Name());
      return(INIT_FAILED);
   }      

   // exibição de dicas segundo os parâmetros
   Print("Central line parameters matching: "+central.Help());
   Print("Width parameters matching: "+width.Help());  
  
   // definição de nomes
   ShortName="iUniChannel";  
   IndicatorSetString(INDICATOR_SHORTNAME,ShortName);  
  
   // parte padrão da função OnInit
   SetIndexBuffer(0,Label1Buffer,INDICATOR_DATA);
   SetIndexBuffer(1,Label2Buffer,INDICATOR_DATA);
   SetIndexBuffer(2,Label3Buffer,INDICATOR_DATA);
  
   // Definição de rótulos e deslocamentos de buffers
   SetStyles();

   return(INIT_SUCCEEDED);
}

Neste ponto, podemos considerar que o indicador sem interface gráfica já está totalmente pronto. Com ele, você pode testar o funcionamento de todas as classes e parâmetros e, em seguida, começar a criar uma GUI.

Durante o teste, o indicador revelou algumas características difíceis de trabalhar. Uma das desvantagens é a de separar o controle do período da linha central e o período de cálculo da largura do canal. O controle por si só aumenta as possibilidades do indicador, mas, em alguns casos, pode ser necessário controlar simultaneamente os dois períodos, utilizando um único parâmetro. Fazemos pequenas alterações para que o período da largura do canal seja igual a um dos três períodos de linha central. A seleção de uma das quatro variantes será realizada através de enumeração (localizada no arquivo UniChannelDefines.mqh):

enum ELockTo{
   LockTo_Off,
   LockTo_Period1,
   LockTo_Period2,
   LockTo_Period3
};

Ao selecionar a variante LockTo_Off, os períodos são regulados separadamente, enquanto, em outros casos, o valor do parâmetro w_Period é igual ao período correspondente da linha central. Declaramos a variável do tipo ELockTo imediatamente após da variável w_Period:

input ELockTo              w_LockPeriod  =  LockTo_Off;

Modificamos a função PrepareParameters(), adicionamos à parte inferior da função o código a seguir:

switch(w_LockPeriod){ // dependendo do tipo de bloqueio
   case LockTo_Period1:
      _w_Period=_c_Period1;
   break;
   case LockTo_Period2:
      _w_Period=_c_Period2;      
   break;
   case LockTo_Period3:
      _w_Period=_c_Period3;      
   break;
}

Outra desvantagem é que as mensagens sobre a correspondência dos parâmetros são exibidas na guia "Expert Advisor" numa cadeia de caracteres, e, nas telas estreitas, parte da cadeia de caracteres é exibida fora da borda. Modificamos para a exibição da cadeia de caracteres numa coluna. Em vez da função Print, utilizamos nossa função PrintColl(). Para esta função são enviados os dois parâmetros: o cabeçalho e a cadeia de caracteres com dica. Na função, a cadeia de caracteres com dica é dividida e exibida em partes:

void PrintColl(string caption,string message){
   Print(caption); // exibição de cabeçalho
   string res[];
   // dividimos a mensagem
   int cnt=StringSplit(message,',',res);
   // exibição da mensagem em partes
   for(int i=0;i<cnt;i++){
      StringTrimLeft(res[i]);
      Print(res[i]);
   }
}

Por conseguinte, na função OnInit(), são alteradas duas cadeias de caracteres de exibição de dicas:

PrintColl("Central line parameters matching:",central.Help());
PrintColl("Width parameters matching:",width.Help());  

Agora o indicador está completamente pronto, o nome do arquivo está no apêndice — "iUniChanhel". Procedemos a criar a interface gráfica.   

Criação das classes da GUI

A interface gráfica será criada com base na interface gráfica do oscilador universal. Copiamos e colamos o arquivo UniOsc/UniOscGUI.mqh na pasta UniChannel e alteramos seu nome para UniChannelGUI.mqh. A interface gráfica do usuário do canal será significativamente diferente da interface do oscilador universal, por isso temos que trabalhar duro.

A principal diferença reside no fato de que, no canal universal, é realizada a escolha independente dos dois indicadores (da linha central e bordas), por isso deve haver duas listas principais de seleção do tipo de indicador. Após a primeira lista, devem ser colocados os controles dos parâmetros da linha central, em seguida, a segunda lista e os controles dos parâmetros das bordas. Assim, a segunda lista não tem coordenadas fixos, elas devem ser calculadas. Além das duas listas para selecionar os tipos, no formulário sempre devem haver dois campos para inserir os valores de deslocamento, suas coordenadas também não são fixas. Há outro ponto que precisa de atenção, trata-se da lista para selecionar a variante que correspondente ao parâmetro w_LockPeriod. Em todos os casos, quando queremos exibir a caixa de edição do parâmetros w_Period, no grupo de controles de largura, é necessário exibir uma lista suspensa adicional.

Primeiro, no arquivo UniChannelGUI.mqh executamos as alterações gerais:

1. O caminho para a enumeração de arquivos:

#include <UniOsc/UniOscDefines.mqh>

deve ser substituído com a seguinte cadeia de caracteres:

#include <UniChannel/UniChannelDefines.mqh>

2. Adicionamos a matriz com valores de enumeração ELockTo:

ELockTo e_lockto[]={LockTo_Off,LockTo_Period1,LockTo_Period2,LockTo_Period3};

3. Apagamos as matizes com as enumerações ENUM_APPLIED_VOLUME e ENUM_STO_PRICE.

Agora procedemos a alterar a classe CUniOscControls.  

Classe de controle de linha central

1. Alteramos o nome da classe CUniOscControls para CUniChannelCentralControls.

2. Na classe, excluímos a declaração das variáveis m_volume e m_sto_price. Assim, excluímos todo associado com estes controles a partir dos métodos SetPointers(), Hide(), Events().

3. Adicionamos a variável m_last_y, nela será fixada a coordenada Y do último controle do grupo. Adicionamos o método para obter o valor desta variável — GetLastY(). O método FormHeight() não é mais necessário, por isso é excluído, mas, em vez dele, adicionamos o método ControlsCount(), ele retornará o número de controles na classe filha. Esta quantidade será necessária para calcular a altura do formulário.

Como resultado, obtém-se a seguinte classe mãe:

class CUniChannelCentralControls{
   protected:
      CSpinInputBox * m_value1; // para o período 1
      CSpinInputBox * m_value2; // para o período 2
      CSpinInputBox * m_value3; // para o período 3
      CComBox * m_price;        // para o preço
      CComBox * m_method;       // para o método
      int m_last_y;             // posição Y do último controle

   public:
  
   // obtenção da posição Y do último controle
   int GetLastY(){
      return(m_last_y);
   }
  
   // método para enviar ponteiros de objetos
   void SetPointers(CSpinInputBox & value1,
                        CSpinInputBox & value2,      
                        CSpinInputBox & value3,
                        CComBox & price,
                        CComBox & method){
      m_value1=GetPointer(value1);
      m_value2=GetPointer(value2);      
      m_value3=GetPointer(value3);            
      m_price=GetPointer(price);
      m_method=GetPointer(method);
   }
  
   // ocultamento do grupo de controles
   void Hide(){
      m_value1.Hide();
      m_value2.Hide();
      m_value3.Hide();
      m_price.Hide();
      m_method.Hide();
   }
  
   // manipulação de eventos
   int Event(int id,long lparam,double dparam,string sparam){
      int e1=m_value1.Event(id,lparam,dparam,sparam);
      int e2=m_value2.Event(id,lparam,dparam,sparam);
      int e3=m_value3.Event(id,lparam,dparam,sparam);
      int e4=m_price.Event(id,lparam,dparam,sparam);
      int e5=m_method.Event(id,lparam,dparam,sparam);
      if(e1!=0 || e2!=0 || e3!=0 || e4!=0 || e5!=0){
         return(1);
      }
      return(0);
   }
  
   // método de inicialização de controles (para alterar rótulos)
   virtual void InitControls(){
   }  
  
   // exibição de grupo de controles
   virtual void Show(int x,int y){
   }  
  
   // obtenção do número de controles no grupo
   virtual int ControlsCount(){
      return(0);
   }      
};

Alteramos a classe filha CUniOscControls_ATR:

1. Mudamos seu nome para CUniChannelCentralControls_AMA, esta será a classe para o indicador AMA.

2. De acordo com a coluna "Parâmetros", na Tabela 1, inicializamos os controles no método InitControls(), enquanto no método Show() chamamos os métodos Show() para todos os controles. Atribuímos à variável m_last_y o valor do último controle.

3. Removemos o método FormHeight(), em vez dele, adicionamos o método ControlsCount().

Obtemos a seguinte classe:

class CUniChannelCentralControls_AMA:public CUniChannelCentralControls{
   void InitControls(){
      // inicializamos os controles
      m_value1.Init("c_value1",SPIN_BOX_WIDTH,1," ama_period");
      m_value2.Init("c_value2",SPIN_BOX_WIDTH,1," fast_ma_period");      
      m_value3.Init("c_value3",SPIN_BOX_WIDTH,1," slow_ma_period");      
   }
  
   // exibição
   void Show(int x,int y){
      m_value1.Show(x,y);
      y+=20;
      m_value2.Show(x,y);
      y+=20;
      m_value3.Show(x,y);
      y+=20;
      m_price.Show(x,y);
      m_last_y=y;
   }
  
   // obtemos o número de elementos no grupo
   int ControlsCount(){
      return(4);
   }
};

Do mesmo modo, criamos as classes para o resto dos indicadores de linha central, e removemos todas as classes filhas mais velhas dos osciladores.

Classes de elementos de controle do cálculo para a largura

Como fundamento da classe CUniChannelCentralControls, criamos a classe para controlar os parâmetros de largura do canal. Fazemos uma cópia da classe CUniChannelCentralControls, mudamos seu nome para CUniChannelWidthControls. Nesta classe, são necessários dois campos de entrada (o período e a largura), duas enumerações padrão para o tipo de média de preços, bem como a enumeração do parâmetro w_LockPeriod. O resultado das alterações é a seguinte classe:

class CUniChannelWidthControls{
   protected:
      CSpinInputBox * m_value1; // para o período
      CSpinInputBox * m_value2; // para a largura    
      CComBox * m_price;        // para o preço
      CComBox * m_method;       // para o método
      CComBox * m_lockto;       // para o tipo de bloqueio
      int m_last_y;             // posição Y do último controle

   public:
  
   // obtenção da posição Y do último controle
   int GetLastY(){
      return(m_last_y);
   }
  
   // método para enviar ponteiros de objetos
   void SetPointers(CSpinInputBox & value1,
                        CSpinInputBox & value2,      
                        CComBox & price,
                        CComBox & method,
                        CComBox & lockto){
      m_value1=GetPointer(value1);
      m_value2=GetPointer(value2);      
      m_price=GetPointer(price);
      m_method=GetPointer(method);
      m_lockto=GetPointer(lockto);      
   }
  
   // ocultamento do grupo de controles
   void Hide(){
      m_value1.Hide();
      m_value2.Hide();
      m_price.Hide();
      m_method.Hide();
   }
  
   // manipulação de eventos
   int Event(int id,long lparam,double dparam,string sparam){
      int e1=m_value1.Event(id,lparam,dparam,sparam);
      int e2=m_value2.Event(id,lparam,dparam,sparam);
      int e4=m_price.Event(id,lparam,dparam,sparam);
      int e5=m_method.Event(id,lparam,dparam,sparam);
      int e6=m_lockto.Event(id,lparam,dparam,sparam);      
      if(e1!=0 || e2!=0 || e4!=0 || e5!=0 || e6){
         return(1);
      }
      return(0);
   }
  
   // método de inicialização de controles (para alterar rótulos)
   virtual void InitControls(){
   }  
  
   // exibição de grupo de controles
   virtual void Show(int x,int y){
   }  
  
   // obtenção do número de controles no grupo
   virtual int ControlsCount(){
      return(0);
   }    
};

Criamos para ela as classes filhas. A principal diferença em relação à classe da linha central é que, após o campo de entrada do período, é necessário a lista suspensa para o parâmetro w_LockPeriod. Obtemos a classe para calcular a largura segundo o ATR:

class CUniChannelWidthControls_ATR:public CUniChannelWidthControls{
   void InitControls(){
      // inicialização do controle
      m_value1.Init("w_value1",SPIN_BOX_WIDTH,1," period");
   }
  
   // exibição do grupo de controle
   void Show(int x,int y){
      m_value1.Show(x,y);
      y+=20;
      m_lockto.Show(x,y);
      m_last_y=y;
   }  
  
   // obtemos o número de controle no grupo
   int ControlsCount(){
      return(2);
   }    
};

Do mesmo modo, são criadas as classes para o resto das variantes de cálculo da largura do canal. 

Agora, no arquivo UniChannelGUI.mqh, encontram-se duas classes bases de controles, muitas de suas classes filhas e a classe de formulário. Com esta última, temos de trabalhar. Devido ao considerável tamanho, mais tarde pode resultar inconveniente trabalhar nela, por isso levamos as classes de controles para outros arquivos. Criamos o arquivo UniChannel/CUniChannelCentralControls.mqh e transferimos para ele a classe CUniChannelCentralControls e todas suas classe filha e ligamos os arquivos adicionais: 

#include <IncGUI_v4.mqh>
#include <UniChannel/UniChannelDefines.mqh>

Transferimos para o arquivo UniChannelDefines.mqh a definição da constante FORM_WIDTH, SPIN_BOX_WIDTH, COMBO_BOX_WIDTH. Depois disso, podemos compilar o arquivo CUniChannelCentralControls para verificar se há erros. Também transferimos para um arquivo separado a classe CUniChannelWidthControls. Após isso, será conveniente trabalhar com a classe de formulário. 

Classe de formulário

Ligamos ao arquivo UniChannelGUI.mqh os dois arquivos recém-criados:

#include <UniChannel/CUniChannelCentralControls.mqh>
#include <UniChannel/CUniChannelWidthControls.mqh>

Mudamos o nome da classe CUniOscForm para CUniChannelForm. Na seção public, removemos a variável-ponteiro de tipo CUniOscControls, em vez dela, declaramos duas variável-ponteiro diferentes: CUniChannelCentralControls e CUniChannelWidthControls, definimos outros controles da classe de formulários. Como resultado, na secção public, temos as seguintes variáveis:

CComBox           m_c_cmb_main;  // lista para selecionar a linha central
CSpinInputBox     m_c_value1;    // caixa de edição do período 1
CSpinInputBox     m_c_value2;    // caixa de edição do período 2
CSpinInputBox     m_c_value3;    // caixa de edição do período 3
CComBox           m_c_price;     // lista para selecionar o preço
CComBox           m_c_method;    // lista para selecionar o método
CSpinInputBox     m_c_shift;     // caixa de edição do deslocamento

CComBox           m_w_cmb_main;  // lista para selecionar as bordas
CSpinInputBox     m_w_value1;    // caixa de edição do período
CSpinInputBox     m_w_value2;    // caixa de edição da largura  
CComBox           m_w_price;     // lista para selecionar o preço
CComBox           m_w_method;    // lista para selecionar o método
CComBox           m_w_lockto;    // lista para selecionar a variante de bloqueio    
CSpinInputBox     m_w_shift;     // caixa de edição do deslocamento          

// grupo de controle da linha central
CUniChannelCentralControls * m_central_controls;
// grupo de controle das bordas
CUniChannelWidthControls * m_width_controls;  

No método Mainproperties( ), alteramos os valores das variáveis ​​m_Name e m_Caption, as outras variáveis ​​permanecem inalteradas:

void MainProperties(){
      m_Name         =  "UniChannelForm";
      m_Width        =  FORM_WIDTH;
      m_Height       =  150;
      m_Type         =  0;
      m_Caption      =  "UniChannel";
      m_Movable      =  true;
      m_Resizable    =  true;
      m_CloseButton  =  true;
}

No método OnInitEvent(), chamamos o método Init() de todos os controles cujos rótulos não foram alterados (conjunto de controle correspondente ao indicador selecionado) e preenchemos as listas flutuantes:

void OnInitEvent(){

   // inicialização dos controles que não são parte do grupo
  
   m_c_cmb_main.Init("cb_c_main",COMBO_BOX_WIDTH," select central");
   m_w_cmb_main.Init("cb_w_main",COMBO_BOX_WIDTH," select bands");

   m_c_price.Init("c_price",COMBO_BOX_WIDTH," price");
   m_c_method.Init("c_method",COMBO_BOX_WIDTH," method");
   m_c_shift.Init("c_shift",COMBO_BOX_WIDTH,1," shift");    
  
   m_w_price.Init("w_price",COMBO_BOX_WIDTH," price");
   m_w_method.Init("w_method",COMBO_BOX_WIDTH," method");
   m_w_shift.Init("w_shift",COMBO_BOX_WIDTH,1," shift");    
  
   m_w_lockto.Init("cb_w_lockto",COMBO_BOX_WIDTH," lock period");
   m_w_value2.Init("w_value2",SPIN_BOX_WIDTH,0.001," width");
  
   // preenchimento das listas flutuantes
  
   for(int i=0;i<ArraySize(e_price);i++){
      m_c_price.AddItem(EnumToString(e_price[i]));
      m_w_price.AddItem(EnumToString(e_price[i]));
   }
   for(int i=0;i<ArraySize(e_method);i++){
      m_c_method.AddItem(EnumToString(e_method[i]));
      m_w_method.AddItem(EnumToString(e_method[i]));
   }            
   for(int i=0;i<ArraySize(e_lockto);i++){
      m_w_lockto.AddItem(EnumToString(e_lockto[i]));            
   }
  
   // autorização para inserir o deslocamento a partir do teclado            
   m_c_shift.SetReadOnly(false);
   m_w_shift.SetReadOnly(false);                        
}

Nos métodos OnShowEvent(), exibimos os controles, ao fazer isto, após a exibição de grupos de elementos, obtemos a coordenada Y e de acordo com ela exibimos os seguintes controles:

void OnShowEvent(int aLeft, int aTop){
   m_c_cmb_main.Show(aLeft+10,aTop+10);        // lista para selecionar o tipo de linha central
   m_central_controls.Show(aLeft+10,aTop+30);  // grupo de controles dos parâmetros da linha central
   int m_y=m_central_controls.GetLastY();      // obtenção da coordenada do último controle
   m_c_shift.Show(aLeft+10,m_y+20);            // caixa de edição do parâmetro de deslocamento
   m_w_cmb_main.Show(aLeft+10,m_y+40);         // lista para selecionar o canal
   m_width_controls.Show(aLeft+10,m_y+60);     // grupo de controles dos parâmetros do canal
   m_y=m_width_controls.GetLastY();            // obtenção da coordenada do último controle
   m_w_value2.Show(aLeft+10,m_y+20);           // caixa de edição da largura
   m_w_shift.Show(aLeft+10,m_y+40);            // caixa de edição do parâmetro de deslocamento
}

No método OnHideEvent(), ocultamos os controles:

void OnHideEvent(){
   m_c_cmb_main.Hide();       // lista para selecionar o tipo de linha central    
   m_central_controls.Hide(); // grupo de controles dos parâmetros da linha central
   m_c_shift.Hide();          // caixa de edição dos parâmetros de deslocamento
   m_w_cmb_main.Hide();       // lista para selecionar o canal
   m_width_controls.Hide();   // grupo de controle dos parâmetros do canal
   m_w_shift.Hide();          // caixa de edição dos parâmetros de deslocamento
   m_w_lockto.Hide();         // lista para selecionar o tipo de bloqueio do período
   m_width_controls.Hide();   // caixa de edição da largura
}

Alteramos o método SetValues(). Alteramos o conjunto de parâmetros do método, no método, para todos os controles, definimos os valores correspondentes a estes parâmetros:

void SetValues(int c_value1,
               int c_value2,
               int c_value3,
               long c_method,
               long c_price,
               long c_shift,                    
               int w_value1,
               int w_value2,
               long w_method,
               long w_price,
               long w_lockto,
               long w_shift  
){

   // caixa de edição de parâmetros da linha central
   m_c_value1.SetValue(c_value1);
   m_c_value2.SetValue(c_value2);      
   m_c_value3.SetValue(c_value3);
   m_c_shift.SetValue(c_shift);        

   // caixa de edição de parâmetros de canal
   m_w_value1.SetValue(w_value1);
   m_w_value2.SetValue(w_value2);        
   m_w_shift.SetValue(w_shift);            
  
   // exibição dos tipos selecionados nas listas de seleção de métodos de suavização
   for(int i=0;i<ArraySize(e_method);i++){
      if(c_method==e_method[i]){
         m_c_method.SetSelectedIndex(i);
      }
      if(w_method==e_method[i]){
         m_w_method.SetSelectedIndex(i);
      }            
   }
  
   // exibição dos tipos selecionados nas listas de seleção de tipo de preço
   for(int i=0;i<ArraySize(e_price);i++){
      if(c_price==e_price[i]){
         m_c_price.SetSelectedIndex(i);
      }
      if(w_price==e_price[i]){
         m_w_price.SetSelectedIndex(i);
      }            
   }

   // exibição do tipo selecionado de bloqueio de período de canal
   for(int i=0;i<ArraySize(e_lockto);i++){
      if(w_lockto==e_lockto[i]){
         m_w_lockto.SetSelectedIndex(i);
         break;
      }
   }                    
}

Em vez do método SetType(), criamos dois métodos: SetCentralType() — para definir o tipo de linha central e SetWidthType() — para definir o tipo de borda. No final de cada método, após criar o objeto de controle, são definidos os valores que permitem inserir valores a partir do teclado. Além disso, definimos os valores mais baixos possíveis, e chamamos o método particular de cálculo da altura do formulário:  

Método SetCentralType():

void SetCentralType(long type){
   // se o objeto já foi criado, removemo-lo
   if(CheckPointer(m_central_controls)==POINTER_DYNAMIC){
      delete(m_central_controls);
      m_central_controls=NULL;
   }
   switch((ECType)type){ // dependendo do tipo selecionado criamos o objeto
      case UniCh_C_AMA:
         m_central_controls=new CUniChannelCentralControls_AMA();
      break;
      case UniCh_C_DEMA:
         m_central_controls=new CUniChannelCentralControls_DEMA();            
      break;
      case UniCh_C_FrAMA:
         m_central_controls=new CUniChannelCentralControls_FrAMA();            
      break;
      case UniCh_C_MA:
         m_central_controls=new CUniChannelCentralControls_MA();            
      break;
      case UniCh_C_TEMA:
         m_central_controls=new CUniChannelCentralControls_TEMA();            
      break;
      case UniCh_C_VIDyA:
         m_central_controls=new CUniChannelCentralControls_VIDyA();            
      break;
      case UniCh_C_PrCh:
         m_central_controls=new CUniChannelCentralControls_PrCh();            
      break;
   }    
  
   // envio de ponteiros para os objetos dos controles
   m_central_controls.SetPointers(m_c_value1,m_c_value2,m_c_value3,m_c_price,m_c_method);
   // inicialização dos controles dos grupos
   m_central_controls.InitControls();
  
   // autorização de entrada a partir do teclado
   m_c_value1.SetReadOnly(false);
   m_c_value2.SetReadOnly(false);
   m_c_value3.SetReadOnly(false);
  
   // definição dos valores mínimos admissíveis
   m_c_value1.SetMinValue(1);        
   m_c_value2.SetMinValue(1);
   m_c_value3.SetMinValue(1);            
  
   // cálculo da altura do formulário
   this.SolveHeight();

}

 Método SetWidthType():

void SetWidthType(long type){
   // se o objeto já foi criado, removemo-lo
   if(CheckPointer(m_width_controls)==POINTER_DYNAMIC){
      delete(m_width_controls);
      m_width_controls=NULL;
   }
   switch((EWType)type){ // dependendo do tipo selecionado criamos o objeto
      case UniCh_W_ATR:
         m_width_controls=new CUniChannelWidthControls_ATR();
      break;
      case UniCh_W_StdDev:
         m_width_controls=new CUniChannelWidthControls_StdDev();            
      break;
      case UniCh_W_Points:
         m_width_controls=new CUniChannelWidthControls_InPoints();            
      break;
      case UniCh_W_Percents:
         m_width_controls=new CUniChannelWidthControls_Envelopes();            
      break;
      case UniCh_W_PrCh:
         m_width_controls=new CUniChannelWidthControls_PrCh();                        
      break;
   }    

   //    envio de ponteiros para os objetos dos controles
   m_width_controls.SetPointers(m_w_value1,m_w_value2,m_w_price,m_w_method);
   // inicialização dos controles dos grupos
   m_width_controls.InitControls();
  
   // definição dos valores mínimos admissíveis
   m_w_value1.SetReadOnly(false);
   m_w_value2.SetReadOnly(false);
  
   // definição dos valores mínimos admissíveis
   m_w_value1.SetMinValue(1);        
   m_w_value2.SetMinValue(0);
  
   // cálculo da altura do formulário
   this.SolveHeight();
              
}

No final dos métodos SetCentralType() e SetWidthType(), é chamado o método para calcular a altura do formulário SolveHeight():

void SolveHeight(){
   // se existem ambos os objetos (linha central e largura)
   if(CheckPointer(m_central_controls)==POINTER_DYNAMIC && CheckPointer(m_width_controls)==POINTER_DYNAMIC){
      m_Height=(m_width_controls.ControlsCount()+m_central_controls.ControlsCount()+6)*20+10;
   }      
}  

Procedemos à conexão do indicador e a interface gráfica do usuário.  

Conexão do indicador e a GUI

Salvamos o indicador iUniChannel como o nome iUniChannelGUI. Por analogia com o indicador iUniOscGUI, adicionamos ao topo de sua janela de propriedades o parâmetro externo UseGUI. A seguir, colocamos as variáveis UseDefault, KeepPrev, definimos para elas seus valores por padrão como true e exibimo-las na janela de propriedades:

input bool                 UseGUI        =  true;
input bool                 UseDefault    =  true;
input bool                 KeepPrev      =  true;

Ligamos o arquivo com a interface gráfica (no mesmo lugar onde são relacionados os arquivos com as classes dos indicadores):

#include <UniChannel/UniChannelGUI.mqh>

Na parte inferior da função OnInit(), adicionamos o código de carregamento da GUI, mas antes, são exigidas as matrizes com os tipos de linha central e bordas. Adicionamo-las abaixo dos parâmetros externos do indicador:

// matriz com os tipos de linha central
ECType ctype[]={
   UniCh_C_AMA,
   UniCh_C_DEMA,
   UniCh_C_FrAMA,
   UniCh_C_MA,
   UniCh_C_TEMA,
   UniCh_C_VIDyA,
   UniCh_C_PrCh
};

// matriz com os tipos de bordas
EWType wtype[]={
   UniCh_W_ATR,
   UniCh_W_StdDev,
   UniCh_W_Points,
   UniCh_W_Percents,
   UniCh_W_PrCh
};

Neste ponto, adicionamos a variável-ponteiro na classe do formulário:

CUniChannelForm * frm;

Na função OnInit(), no final, criamos o objeto do interface gráfica do usuário:

if(UseGUI){
  
   // criação e inicialização do objeto do formulário
   frm=new CUniChannelForm();
   frm.Init();
  
   // variáveis auxiliares
   int ind1=0;
   int ind2=0;
  
   // pesquisa do tipo selecionado de linha centra na matriz de tipos de linha central
   for(int i=0;i<ArraySize(ctype);i++){        
      frm.m_c_cmb_main.AddItem(EnumToString(ctype[i]));
      if(ctype[i]==_CentralType){
         ind1=i;
      }
   }
  
   // pesquisa do tipo selecionado de bordas de canal na matriz de tipos de bordas
   for(int i=0;i<ArraySize(wtype);i++){        
      frm.m_w_cmb_main.AddItem(EnumToString(wtype[i]));
      if(wtype[i]==_WidthType){
         ind2=i;
      }
   }      
  
   // exibição na lista de tipo selecionado de linha central
   frm.m_c_cmb_main.SetSelectedIndex(ind1);      
   // preparação dos controles correspondentes aos tipos
   frm.SetCentralType(_CentralType);
  
   // exibição na lista do tipo selecionado de bordas
   frm.m_w_cmb_main.SetSelectedIndex(ind2);      
   frm.SetWidthType(_WidthType);      
  
   // definição de valores
   frm.SetValues(
                  _c_Period1,
                  _c_Period2,
                  _c_Period3,
                  _c_Method,
                  _c_Price,
                  _c_Shift,
                  _w_Period,
                  _w_Width,
                  _w_Method,
                  _w_Price,
                  _w_LockPeriod,
                  _w_Shift
   );
  
   // definição de propriedades do formulário
   frm.SetSubWindow(0);
   frm.SetPos(10,30);
   // exibição do formulário
   frm.Show();
}  

Além de criar o objeto do formulário, é realizado o preenchimento das listas para a seleção de indicadores e definição de suas variantes selecionadas. Além disso, definimos todos os outros valores nos controles. Depois disso, ao anexar o indicador ao gráfico, é exibido um formulário com controles (Fig. 1).


Fig. 1. Formulário com controles de canal universal

Os controles já são exibidos, agora é necessário fornecer as ações dos botões do formulário. Na função OnChartEvent(), são manipulados seis eventos diferentes. A manipulação de alguns deles é bastante complexa e volumosa, por isso tem sido colocados em funções individuais:

void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
{
   // eventos do formulário
   if(frm.Event(id,lparam,dparam,sparam)==1){
      EventForm();
   }
  
   // seleção do tipo de linha central
   if(frm.m_c_cmb_main.Event(id,lparam,dparam,sparam)==1){
      EventCentralTypeChange();
   }  
  
   // seleção do tipo de bordas
   if(frm.m_w_cmb_main.Event(id,lparam,dparam,sparam)==1){
      EventWidthTypeChange();
   }  
  
   // alteração dos parâmetros da linha central
   if(frm.m_central_controls.Event(id,lparam,dparam,sparam)==1){
      EventCentralParametersChange();
   }  
  
   // alteração dos parâmetros das bordas
   if(frm.m_width_controls.Event(id,lparam,dparam,sparam)==1){
      EventWidthParametersChange();

   }  

   // alteração dos parâmetros do deslocamento
   if(frm.m_c_shift.Event(id,lparam,dparam,sparam)!=0 ||
      frm.m_w_shift.Event(id,lparam,dparam,sparam)
   ){
      EventShift();
   }    
}

Consideramos todas estas funções. Função EventForm():

void EventForm(){      
   int win=ChartWindowFind(0,ShortName);  // definição da subjanela do indicador
   ChartIndicatorDelete(0,win,ShortName); // remoção do indicador
   ChartRedraw();
}  

Esta função é executada quando: o formulário é fechado usando o botão com uma cruz, é procurada a janela do indicador pelo seu nome curto e é removido o indicador. 

Função EventCentralTypeChange():

void EventCentralTypeChange(){    
   // obtenção do novo tipo na variável
   _CentralType=ctype[frm.m_c_cmb_main.SelectedIndex()];
  
   // exclusão do antigo objeto e criação de um novo
   delete(central);
   LoadCentral(true);
  
   // verificação de como foi carregado o indicador
   if(!central.CheckHandle()){
      Alert("Erro de carregamento do indicador "+central.Name());
   }

   // definição de deslocamento e nomes dos buffers
   SetStyles();

   // definição na lista do novo tipo
   frm.SetCentralType(ctype[frm.m_c_cmb_main.SelectedIndex()]);
   // atualização dos valores dos parâmetros no formulário
   frm.SetValues(
                  _c_Period1,
                  _c_Period2,
                  _c_Period3,
                  _c_Method,
                  _c_Price,
                  _c_Shift,
                  _w_Period,
                  _w_Width,
                  _w_Method,
                  _w_Price,
                  _w_LockPeriod,
                  _w_Shift
   );
   // atualização do formulário
   frm.Refresh();
  
   // execução do temporizador para recálculo dos indicadores
   EventSetMillisecondTimer(100);
}

Nesta função, é alterado o tipo de indicador de linha central. Primeiro, é obtido o tipo de indicador selecionado; é removido o objeto antigo; é criado um novo. Ao criar um novo objeto, seus parâmetros podem ser alterados (devido à função UseDefault), por isso é chamado o método SetValues() a fim de definir os novos valores dos controles, é atualizada a exibição do formulário (método Refresh()). No final, é executado o temporizador para recálculo do indicador.      

A função EventWidthTypeChange() é semelhante com a função EventCentralTypeChange(), ela não será considerada em detalhe. 

Nas funções EventCentralParametersChange() e EventWidthParametersChange(), é fornecida a reação dos indicadores às alterações dos valores dos parâmetros. Estas duas funções, na sua funcionalidade básica, são idênticas umas às outras. No entanto, durante a alteração dos parâmetros, é preciso de prestar atenção ao bloqueio dos períodos, executar a correção de parâmetros de acordo com ele, por isso, as funções têm suas próprias características e serão consideradas ambas.

void EventCentralParametersChange(){          
  
   // variável que indica se é necessário reiniciar o indicador de bordas
   bool dolock=false;
  
   // alteração do valor do período 1
   if((int)frm.m_c_value1.Value()>0){
      // atribuição de variável do valor obtido a partir do controle
      _c_Period1=(int)frm.m_c_value1.Value();
      // se o período o período 1 estiver ligado com o período do indicador de largura
      if(_w_LockPeriod==LockTo_Period1){
         // à variável com período do indicador de largura é atribuído o valor de período 1
         _w_Period=_c_Period1;
         // exibimo-lo no formulário
         frm.m_w_value1.SetValue(_w_Period);
         // indicamos que o segundo indicador deve ser reiniciado
         dolock=true;
      }
   }
  
   // a alteração do valor do período 2 é semelhante à alteração do período 1
   if((int)frm.m_c_value2.Value()>0){
      _c_Period2=(int)frm.m_c_value2.Value();
      if(_w_LockPeriod==LockTo_Period2){
         _w_Period=_c_Period2;
         frm.m_w_value1.SetValue(_w_Period);
         dolock=true;
      }        
   }
  
   // a alteração do valor do período 3 é semelhante à alteração do período 1
   if((int)frm.m_c_value3.Value()>0){
      _c_Period3=(int)frm.m_c_value3.Value();
      if(_w_LockPeriod==LockTo_Period3){
         _w_Period=_c_Period3;
         frm.m_w_value1.SetValue(_w_Period);
         dolock=true;
      }        
   }
  
   // alteração do método
   if(frm.m_c_method.SelectedIndex()!=-1){
      _c_Method=e_method[frm.m_c_method.SelectedIndex()];
   }
  
   // alteração do preço
   if(frm.m_c_price.SelectedIndex()!=-1){
      _c_Price=e_price[frm.m_c_price.SelectedIndex()];
   }
  
   // exclusão do objeto antigo e criação de um novo
   delete(central);
   LoadCentral(false);
   if(!central.CheckHandle()){
      Alert("Erro de carregamento do indicador"+central.Name());
   }  

   // exclusão e criação do novo objeto do segundo indicador
   if(dolock){
      delete(width);
      LoadWidth(false);
      if(!width.CheckHandle()){
         Alert("Erro de carregamento do indicador "+width.Name());
      }  
   }  

   // definição dos deslocamentos e nomes dos buffers
   SetStyles();

   // execução do temporizador para recálculo dos indicadores
   EventSetMillisecondTimer(100);
}  

Nesta função, ao alterar qualquer um dos três períodos, é verificado o valor do parâmetro de bloqueio, e se o bloqueio é feito, é alterado o parâmetro para o indicador das bordas, a sua atualização - no formulário e variável dolock - é definido pelo valor true. No final, é removido o objeto velho do indicador e é criado um novo, e, se a variável dolock é igual a true, é executada a exclusão e criação de objeto das bordas. Depois de tudo isso, é iniciado o temporizador que espera o recálculo dos indicadores.

void EventWidthParametersChange(){  
      
   // variável que indica se é necessário reiniciar o indicador de linha central
   bool dolock=false;

   // alteração do período
   if((int)frm.m_w_value1.Value()>0){
      // atribuição de variável do valor obtido a partir do controle
      _w_Period=(int)frm.m_w_value1.Value();
      // execução do bloqueio
      // o parâmetro de largura é ligado como o primeiro período da linha central
      if(_w_LockPeriod==LockTo_Period1){
         // atribuição do novo valor da variável do indicador da linha central
         _c_Period1=_w_Period;
         // atualização do valor no formulário
         frm.m_c_value1.SetValue(_c_Period1);
         // especificamos sobre a necessidade de reiniciar o indicador de largura
         dolock=true;
      }
      else if(_w_LockPeriod==LockTo_Period2){ // se o bloqueio com período 2
         _c_Period2=_w_Period;
         frm.m_c_value2.SetValue(_c_Period2);
         dolock=true;
      }
      else if(_w_LockPeriod==LockTo_Period3){ // se o bloqueio com período 3
         _c_Period3=_w_Period;
         frm.m_c_value3.SetValue(_c_Period3);
         dolock=true;
      }
   }
  
   // alteração do parâmetro de largura do canal
   if((double)frm.m_w_value2.Value()>0){
      _w_Width=(double)frm.m_w_value2.Value();
   }      
  
   // alteração do método
   if(frm.m_w_method.SelectedIndex()!=-1){
      _w_Method=e_method[frm.m_w_method.SelectedIndex()];
   }
  
   // alteração do preço
   if(frm.m_w_price.SelectedIndex()!=-1){
      _w_Price=e_price[frm.m_w_price.SelectedIndex()];
   }
  
   // evento de alteração na lista de seleção do tipo de bloqueio de períodos
   if(frm.m_w_lockto.SelectedIndex()>=0){
      // atribuição de variável do valor obtido a partir do elemento de controle
      _w_LockPeriod=e_lockto[frm.m_w_lockto.SelectedIndex()];
      // se for selecionado o bloqueio com algum dos períodos,
      // será copiado seu valor e será atualizado no formulário
      if(_w_LockPeriod==LockTo_Period1){
         _w_Period=_c_Period1;
         frm.m_w_value1.SetValue(_w_Period);
      }
      else if(_w_LockPeriod==LockTo_Period2){
         _w_Period=_c_Period2;
         frm.m_w_value1.SetValue(_w_Period);
      }
      else if(_w_LockPeriod==LockTo_Period3){
         _w_Period=_c_Period3;
         frm.m_w_value1.SetValue(_w_Period);
      }
   }      

   // exclusão do objeto antigo e criação de um novo
   delete(width);
   LoadWidth(false);
   if(!width.CheckHandle()){
      Alert("Erro de carregamento do indicador "+width.Name());
   }
  
   // exclusão e criação do novo objeto do segundo indicador
   if(dolock){
      delete(central);
      LoadCentral(false);
      if(!central.CheckHandle()){
         Alert("Erro de carregamento do indicador "+central.Name());
      }
   }

   // definição dos deslocamentos e nomes dos buffers
   SetStyles();

   // execução do temporizador para recálculo dos indicadores
   EventSetMillisecondTimer(100);      
}  

Nesta função, ao alterar o período, é verificado o tipo de bloqueio, e, se necessário, é alterado o período correspondente do indicador da linha central. Se ocorrer o evento de lista de seleção de tipo de bloqueio, a variável de período de indicador das bordas executa a atribuição do valor a partir da variável correspondente do indicador da linha central.

A manipulação do evento de alteração de valores de deslocamento é bastante simples:

void EventShift(){     
   // obtenção de novos valores nas variáveis
   _c_Shift=(int)frm.m_c_shift.Value();
   _w_Shift=(int)frm.m_w_shift.Value();
   // definição de novos estilos
   SetStyles();
   // atualização do gráfico
   ChartRedraw();
}

Ás variáveis ​​são atribuídos os valores a partir dos controles, é chamada a função SetStyles() e é atualizado o gráfico.

Neste ponto, o indicador com interface gráfica do usuário está quase pronto.

Durante o processo de teste do indicador, uma falha foi detectada. Quando o parâmetro externo UseDefault estava habilitado era utilizado o bloqueio de período, e o bloqueio não funcionou. Isto é devido ao fato de que após arrancar o segundo indicador (indicador de largura), no seu construtor, são alterados os parâmetros. Para corrigir este erro, teve de modificar certas classes filhas do indicador de largura. Nos construtores de classes CChannelUni_Calculate_ATR, CChannelUni_Calculate_StdDev e CChannelUni_Calculate_PriceChannel foi adicionado o parâmetro locked com valor por padrão false (se o parâmetro não é enviado para a classe, tudo funciona sem alterações). Ao definir locked=true e use_default=true, os parâmetros do período no construtor não são alterados (ao ter a condição locked=true). Consideremos o trecho da classe CChannelUni_Calculate_ATR:

if(use_default){
   if(keep_previous){
      if(ma_period==-1 && !locked)ma_period=14// alteração
      if(ch_width==-1)ch_width=2;
   }
   else{
      if(!locked)ma_period=14// alteração
      ch_width=2;
   }      
}

À variável ma_period é atribuído o valor por padrão, se a variável locked é igual a false. Assim sendo realizada a conclução da função LoadWidth(). No início da função, é calculado o valor Locked:

bool Locked=(w_LockPeriod!=LockTo_Off);

A seguir, essa variável é enviada para os construtores de classes ao criar objetos.

Assim como fizemos no oscilador universal, adicionamos a possibilidade de alterar o esquema de cores e garantir o armazenamento de parâmetros do indicador ao alterar o timeframe. Não vamos considerar o uso de esquemas de cores, uma vez que foi considerado ao criar o oscilador universal. Passamos para o armazenamento de parâmetros.

Na função OnDeinit() do indicador, se o motivo da anulação da inicialização é a alteração do gráfico, criamos um objeto gráfico com os valores dos parâmetros. Criaremos estes objetos gráficos para além da visibilidade do gráfico:

void SaveOrDeleteParameters(const int reason){
   // se isto não é a alteração do gráfico, excluímos os objetos gráficos 
   if(reason!=REASON_CHARTCHANGE){
      ObjectDelete(0,"_CentralType");
      ObjectDelete(0,"_c_Period1");
      ObjectDelete(0,"_c_Period2");
      ObjectDelete(0,"_c_Period3");
      ObjectDelete(0,"_c_Shift");
      ObjectDelete(0,"_c_Method");
      ObjectDelete(0,"_c_Price");
      ObjectDelete(0,"_WidthType");
      ObjectDelete(0,"_w_Period");
      ObjectDelete(0,"_w_LockPeriod");
      ObjectDelete(0,"_w_Shift");
      ObjectDelete(0,"_w_Method");
      ObjectDelete(0,"_w_Price");
      ObjectDelete(0,"_w_Width");      
   }
   else// ao alterar o gráfico, criamos objetos gráficos com valores dos parâmetros
      SaveParameter("_CentralType",(string)_CentralType);
      SaveParameter("_c_Period1",(string)_c_Period1);
      SaveParameter("_c_Period2",(string)_c_Period2);
      SaveParameter("_c_Period3",(string)_c_Period3);
      SaveParameter("_c_Shift",(string)_c_Shift);
      SaveParameter("_c_Method",(string)_c_Method);
      SaveParameter("_c_Price",(string)_c_Price);
      SaveParameter("_WidthType",(string)_WidthType);
      SaveParameter("_w_Period",(string)_w_Period);
      SaveParameter("_w_LockPeriod",(string)_w_LockPeriod);
      SaveParameter("_w_Shift",(string)_w_Shift);
      SaveParameter("_w_Method",(string)_w_Method);
      SaveParameter("_w_Price",(string)_w_Price);
      SaveParameter("_w_Width",(string)_w_Width);        
   }
}

// função auxiliar para armazenamento de um parâmetro no objeto gráfico
void SaveParameter(string name,string value){
   if(ObjectFind(0,name)==-1){
      ObjectCreate(0,name,OBJ_LABEL,0,0,0);
      ObjectSetInteger(0,name,OBJPROP_XDISTANCE,0);
      ObjectSetInteger(0,name,OBJPROP_YDISTANCE,-30);
   }
   ObjectSetString(0,name,OBJPROP_TEXT,value);
}

Na função OnInit(), imediatamente após a chamada da função PrepareParameters(), chamamos a função LoadSavedParameters():

bool LoadSavedParameters(){
   // se existem todos os objetos com parâmetros 
   if(ObjectFind(0,"_CentralType")==0 &&
      ObjectFind(0,"_c_Period1")==0 &&
      ObjectFind(0,"_c_Period2")==0 &&
      ObjectFind(0,"_c_Period3")==0 &&
      ObjectFind(0,"_c_Shift")==0 &&
      ObjectFind(0,"_c_Method")==0 &&
      ObjectFind(0,"_c_Price")==0 &&
      ObjectFind(0,"_WidthType")==0 &&
      ObjectFind(0,"_w_Period")==0 &&
      ObjectFind(0,"_w_LockPeriod")==0 &&
      ObjectFind(0,"_w_Shift")==0 &&
      ObjectFind(0,"_w_Method")==0 &&
      ObjectFind(0,"_w_Price")==0 &&
      ObjectFind(0,"_w_Width")==0
   ){
      // obtenção de valores a partir dos objetos gráficos
      _CentralType=(ECType)ObjectGetString(0,"_CentralType",OBJPROP_TEXT);
      _c_Period1=(int)ObjectGetString(0,"_c_Period1",OBJPROP_TEXT);
      _c_Period2=(int)ObjectGetString(0,"_c_Period2",OBJPROP_TEXT);
      _c_Period3=(int)ObjectGetString(0,"_c_Period3",OBJPROP_TEXT);
      _c_Shift=(int)ObjectGetString(0,"_c_Shift",OBJPROP_TEXT);
      _c_Method=(long)ObjectGetString(0,"_c_Method",OBJPROP_TEXT);
      _c_Price=(long)ObjectGetString(0,"_c_Price",OBJPROP_TEXT);
      _WidthType=(EWType)ObjectGetString(0,"_WidthType",OBJPROP_TEXT);
      _w_Period=(int)ObjectGetString(0,"_w_Period",OBJPROP_TEXT);
      _w_LockPeriod=(long)ObjectGetString(0,"_w_LockPeriod",OBJPROP_TEXT);
      _w_Shift=(int)ObjectGetString(0,"_w_Shift",OBJPROP_TEXT);
      _w_Method=(long)ObjectGetString(0,"_w_Method",OBJPROP_TEXT);
      _w_Price=(long)ObjectGetString(0,"_w_Price",OBJPROP_TEXT);
      _w_Width=(double)ObjectGetString(0,"_w_Width",OBJPROP_TEXT);
      return(true);
   }
   else{
      return(false);
   }
}

Na função, é verificado se existem estes objetos, e se existirem, serão usados seus valores, ao acontecer isto, a função retornará true. Se a função retornar true, chamaremos as funções LoadCentral() e LoadWidth() usando o parâmetro false (para que não sejam definidos parâmetros por padrão). Trecho da função OnInit():

bool ChartCange=LoadSavedParameters();
  
LoadCentral(!ChartCange);

Da mesma forma, é chamada a função LoadWidth():

LoadWidth(!ChartCange);

Assim, a criação do canal universal foi completada. 

Conclusão

Apesar do fato de ter sido usado muito código pronto a partir do oscilador universal, a criação do canal universal exigiu uma quantidade significativa de trabalho adicional. Ao contrário do oscilador universal, a principal diferença é a existência de dois blocos independentes, isto é: a linha central e as bordas. A complexidade resultante disto quase dobrou a quantidade de trabalho. Também se tornou mais difícil alterar os parâmetros, devido à função de bloqueio de períodos. Até o próprio carregamento de novos indicadores tornou-se mais complexo, uma vez que são de dois. Além disso, foi adicionado um novo recurso, nomeadamente, salvar os parâmetros ao alterar o timeframe. O resultado é um indicador tão útil e conveniente como o oscilador universal. Este indicador estende significativamente as possibilidades da própria ideia do canal, porque agora você pode selecionar separadamente a linha central e o método para a plotagem das bordas. Como resultado, isto dá uma grande variedade de possíveis combinações. O aumento na velocidade ao aplicar o indicador - graças à GUI - permitirá explorar visualmente todas estas combinações.

Anexo

O apêndice inclui uma pasta com todos os arquivos necessários. Todos eles estão organizados por pastas de mesma forma como eles têm de ser colocados no terminal.