Universeller Kanal mit grafischem Interface

Dmitry Fedoseev | 30 März, 2017


Inhalt

Einleitung

Es wurde bereits ein Artikel über die Erstellung eines universellen Oszillators mit grafischem Interface verfasst. Als Ergebnis bekamen wir einen interessanten und nützlichen Indikator, der die Chartanalyse wesentlich erleichtert und beschleunigt. Neben Oszillatoren gibt es auch weitere Indikatortypen für die technische Analyse, die nicht weniger interessant als Oszillatoren sind. Dazu gehören Trendindikatoren, Volatilitätsindikatoren, Volumenindikatoren und andere, die in verschiedene Kategorien unterteilt werden können. In diesem Artikel wird die Erstellung eines universellen Kanal-Indikators betrachtet.

Der Artikel über den universellen Oszillator ist relativ kompliziert und eher für erfahrene Programmierer als für Anfänger geeignet. Da das Thema dieses Artikels dem Thema des universellen Oszillators eng verbunden ist, wird der universelle Kanal auf Basis des universellen Oszillators erstellt. Auf diese Weise können auch angehende Programmierer ihre universellen Indikatoren entwickeln, ohne bei der Erstellung universeller Indikatoren mit grafischem Interface ins Detail gehen zu müssen.

Trotz vieler Ähnlichkeiten mit dem universellen Oszillator, gibt es auch prinzipielle Unterschiede. Alle Channel Indikatoren stellen drei Linien dar: zentrale, obere und untere Linie. Die zentrale Linie ist nach dem Prinzip des Zeichnens dem gleitenden Durchschnitt identisch, und in den meisten Fällen wird für das Zeichnen eines Kanals gerade der gleitende Durchschnitt verwendet. Die obere und untere Linien sind von der zentralen Linie gleich weit entfernt. Dieser Abstand kann in Punkten oder in Prozent vom Preis (Envelopes Indikator) definiert werden, es können der Wert der Standardabweichung (Bollinger Bands) oder der Wert des ATR Indikators (Keltner Kanal) verwendet werden. Der Kanal-Indikator wird unter Verwendung von zwei separaten Blöcken erstellt:

  1. Block für die Berechnung der zentralen Linie
  2. Block für das Festlegen der Breite des Kanals (oder Zeichnen der Grenzen)

Es gibt Kanäle anderen Typs, zum Beispiel, Donchian Channel (Preiskanal). Das Zeichnen des Kanals beginnt mit dem Zeichnen der Randlinien (Preisbereich), dann wird der Wert der zentralen Linie berechnet (in der Mitte des Bereichs). Aber auch dieser Kanal kann nach dem oben beschriebenen System gezeichnet werden: zuerst wird die zentrale Linie als die Mitte des Preisbereichs gezeichnet, und danach werden die Grenzen gezeichnet. Natürlich werden hier mehr Berechnungen als gewöhnlich beim Zeichnen eines Preiskanals benötigt. Da der Hauptzweck des Artikels ist, einen universellen Indikator zu erstellen, kann man einige Ausnahmen zulassen, denn dieser Ansatz wird die Anzahl der möglichen Kombinationen der zentralen Linie und der Grenzen erhöhen. Zum Beispiel kann man einen Indikator mit der zentralen Linie wie beim Preiskanal erhalten, deren Grenzen aber mit dem Abstand der Standardabweichung liegen wie bei Bollinger Bands usw. 

Typen der zentralen Linie

Für die zentrale Linie werden verschiedene gleitende Durchschnitte verwendet. Definieren wir ihre Typen und die Anzahl ihrer Parameter. Alle Varianten der zentralen Linie, die im Indikator verwendet werden, sind in der Tabelle 1 angeführt.

Tabelle 1. Typen der zentralen Linie

Standardfunktion
Name Parameter
iAMA Adaptive Moving Average 1. int ama_period — AMA Periode
2. int fast_ma_period — Periode des schnellen MAs
3. int slow_ma_period — Periode des langsamen MAs
4. int ama_shift — horizontale Verschiebung des Indikators
5. ENUM_APPLIED_PRICE  applied_price — Preistyp oder Handle 
iDEMA Double Exponential Moving Average 1. int ma_period — Mittelungsperiode
2. int ma_shift — horizontale Verschiebung des Indikators
3. ENUM_APPLIED_PRICE  applied_price — Preistyp
iFrAMA Fractal Adaptive Moving Average 1. int ma_period — Mittelungsperiode
2. int ma_shift — horizontale Verschiebung des Indikators
3. ENUM_APPLIED_PRICE  applied_price — Preistyp
iMA Moving Average 1. int ma_period — Mittelungsperiode
2. int ma_shift — horizontale Verschiebung des Indikators  
3. ENUM_MA_METHOD ma_method — Typ der Glättung
4. ENUM_APPLIED_PRICE applied_price — Preistyp
iTEMA Triple Exponential Moving Average 1. int ma_period — Mittelungsperiode
2. int ma_shift — horizontale Verschiebung des Indikators  
3. ENUM_APPLIED_PRICE  applied_price — Preistyp
iVIDyA Variable Index Dynamic Average 1. int cmo_period — Chande Momentum Periode 
2. int ema_period — Periode des Glättungsfaktors 
3. int ma_shift — horizontale Verschiebung des Indikators 
4. ENUM_APPLIED_PRICE  applied_price — Preistyp
- zentrale Linie des Preiskanals 1. int period

Basierend auf der Analyse der Spalte "Parameter" aus der Tabelle 1, erhalten wir den minimalen Set von Parametern(Tabelle 2).

Tabelle 2. Universeller Set von Parametern für die Berechnung der zentralen Linie des Kanals 

Typ Name
int period1
int period2
int period3
int shift
ENUM_MA_METHOD ma_method 
ENUM_APPLIED_PRICE  price
Die Parameter der zentralen Linie haben im Fenster der Eigenschaften des Indikators die Präfixe "c_". 

Typen der Grenzen

Legen wir die Varianten der Berechnung der Kanalgrenzen fest (Tabelle 3).

Tabelle 3. Varianten der Berechnung der Breite des Kanals 

Standardfunktion
Name  Parameter
iATR Average True Range 1. int ma_period — Mittelungsperiode
iStdDev  Standard Deviation 1. int ma_period — Mittelungsperiode
2. int ma_shift — horizontale Verschiebung des Indikators
3. ENUM_MA_METHOD — Glättungstyp
4. ENUM_APPLIED_PRICE applied_price — Preistyp 
in Punkten  int width — Breite in Punkten 
in Prozent (wie in Envelopes)  double width — Breite in Prozent vom Preis   
wie beim Preiskanal  double width — Skalierungsfaktor hinsichtlich der aktuellen Breite des Preiskanals

Basierend auf der Spalte "Parameter" aus der Tabelle 3 erhalten wir den notwendigen Set von Parametern (Tabelle 4).

Tabelle 4. Universeller Set von Parametern für die Berechnung der Breite des Kanals

Typ Name
int period
int  shift 
ENUM_MA_METHOD  ma_method  
ENUM_APPLIED_PRICE  price
double  width 

Bei der Berechnung in Punkten wird eine Variable vom Typ int benötigt, sie ist aber nicht in der Tabelle 4 vorhanden, denn statt der Variablen kann man eine Variable vom Typ double verwenden. Auf diese Weise wird die Anzahl der Variablen im Fenster der Eigenschaften reduziert.

Die Parameter der Berechnung der Grenzen werden die Präfixe "w_" im Fenster der Eigenschaften des Indikators haben. 

Klassen der zentralen Linie

Die Basisklasse der zentralen Linie ist nach dem Prinzip und dem Set von Methoden der CUniOsc Klasse aus dem Artikel Universeller Oszillator mit grafischen Interface identisch, nehmen wir diese Klasse als Grundlage und modifizieren wir sie ein bisschen.

Im Ordner MQL5/Include erstellen wir den Ordner "UniChannel", kopieren wir die Datei CUniOsc.mqh (aus dem Ordner Include/UniOsc) in diesen Ordner und benennen wir sie in CUniChannel.mqh um. Lassen wir die Basisklasse (COscUni), die abgeleitete Klasse Calculate1 (absoluter Pfad COscUni_Calculate1) und die von ihr abgeleitete Klasse COscUni_ATR in der Datei, alle anderen Klassen werden gelöscht.

Benennen wir die Klassen um: ersetzen wir das Fragment "COscUni" durch "CChannelUni". Dafür passt eine Funktion des Editors sehr gut (Hauptmenü - Bearbeiten - Suchen und ersetzen - Ersetzen), aber bitte nicht den Button "Alles ersetzen" verwenden, sondern einzeln ersetzen, um den Prozess zu kontrollieren und um sicher zu sein, dass das Fragment nur an den richtigen Stellen ersetzt wurde.

Die Klasse der zentralen Linie zeichnet immer eine durchgehende Linie, deswegen braucht man viele Methoden der Basisklasse nicht. Nach dem Löschen unnötiger Methoden bleibt die Basisklasse wie folgt:

class CChannelUniWidth{
   protected:
      int m_handle;      // Handle des Indikators
      string m_name;     // Name des Indikators
      string m_label1;   // Name des Puffers 1      
      string m_help;     // kurze Hilfe zu den Parametern des Indikators
      double m_width;    // Breite des Kanals
   public:
  
      // Konstruktor
      void CChannelUniWidth(){
         m_handle=INVALID_HANDLE;
      }
      
      // Destruktor
      void ~CChannelUniWidth(){
         if(m_handle!=INVALID_HANDLE){
            IndicatorRelease(m_handle);
         }
      }
  
      // Hauptmethode, die aus der OnCalculate() Funktion des Indikators aufgerufen wird
      virtual int Calculate( const int rates_total,
                     const int prev_calculated,
                     double & bufferCentral[],
                     double & bufferUpper[],
                     double & bufferLower[],
      ){
         return(rates_total);
      }
      
      // Handle des geladenen Indikators erhalten
      int Handle(){
         return(m_handle);
      }
      
      // Methode der Überprüfung des Handles, um festzustellen, ob der Indikator geladen wurde  
      bool CheckHandle(){
         return(m_handle!=INVALID_HANDLE);
      }
      
      // den Namen des Indikators erhalten
      string Name(){
         return(m_name);
      }    

      // Text für Puffer erhalten
      string Label1(){
         return(m_label1);
      }
      
      // Tooltip mit den Parametern erhalten
      string Help(){
         return(m_help);
      }
};

Aus der Calculate Klasse kann man alles löschen, was den zweiten Puffer betrifft, so bleibt in der Klasse nur die Calculate Methode:

class CChannelUni_Calculate1:public CChannelUni{
   public:
      // Hauptmethode, die aus der OnCalculate() Funktion des Indikators aufgerufen wird
      // die ersten zwei Parameter sind gleich den ersten zwei Parametern
      // der OnCalculate() Funktion
      // dritter Parameter - Indikatorpuffer für die zentrale Linie
      int Calculate( const int rates_total,    
                     const int prev_calculated,
                     double & buffer0[]
      ){
        
         // Anzahl der zu kopierenden Elementen festlegen
        
         int cnt;
        
         if(prev_calculated==0){
            cnt=rates_total;
         }
         else{
            cnt=rates_total-prev_calculated+1;
         }  
        
         // Kopieren von Daten in den Puffer
         if(CopyBuffer(m_handle,0,0,cnt,buffer0)<=0){
            return(0);
         }
        
         return(rates_total);
      }
};

Schreiben wir eine abgeleitete Klasse, die den iMA Indikator verwendet. Die CChannelUni_ATR Klasse in der Datei benennen wir in CChannelUni_MA um, ersetzen wir den Indikator, der in der Klasse aufgerufen wird, und löschen wir alles Unnötige. Als Ergebnis bekommen wir die folgende Klasse:

class CChannelUni_MA:public CChannelUni_Calculate1{
   public:
   // Konstruktor
   // die ersten zwei Parameter sind gleich für alle abgeleiteten Klassen
   // darauf folgen die Parameter des zu ladenden Indikators
   void CChannelUni_MA( bool use_default,
                        bool keep_previous,
                        int & ma_period,
                        int & ma_shift,
                        long & ma_method,
                        long & ma_price){
      if(use_default){ // Verwendung von Standardwerten ausgewählt
         if(keep_previous){
            // die früher verwendeten Parameter nicht ändern
            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;            
         }      
      }    
      
      // Indikator laden
      m_handle=iMA(Symbol(),Period(),ma_period,ma_shift,(ENUM_MA_METHOD)ma_method,(ENUM_APPLIED_PRICE)ma_price);
      
      // einen String mit dem Namen des Indikators bilden
      m_name=StringFormat( "iMA(%i,%i,%s,%s)",
                           ma_period,
                           ma_shift,        
                           EnumToString((ENUM_MA_METHOD)ma_method),              
                           EnumToString((ENUM_APPLIED_PRICE)ma_price)
                        );

      
      // String für den Puffernamen
      m_label1=m_name;

      // Tooltip mit den Parametern
      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)
                           );
   }
};

Gehen wir näher auf die Bildung der Strings in den Variablen m_name und mlabel1 ein. Der Name der Indikators (m_name Variable) wird bei den Indikatoren im Unterfenster im linken oberen Ecke des Unterfenters angezeigt. Da der Kanal auf dem Preischart angezeigt wird, bleibt sein Name unsichtbar, deswegen weisen wir der m_label Variablen den gleichen ausführlichen Namen zu wie der m_name Variablen, damit wenn sich der Mauszeiger über dem zentralen Kanal befindet, wären alle seine Parameter im Tooltip zu sehen. 

Die Klassen für alle Standardindikatoren werden genauso wie die Klasse für den iMA Indikator erstellt. Eine Ausnahme bildet der Preiskanal. Da der Preiskanal nicht zu den Standardindikatoren des Terminals gehört, muss er berechnet werden. Hier kann es zwei Varianten geben:

  1. Eine abgeleitete Klasse vom Typ Calculate erstellen und alle Berechnungen in dieser Klasse durchführen
  2. Einen zusätzlichen Indikator schreiben und ihn mithilfe der iCustom Funktion aufrufen
Beide Varianten sind möglich. Im ersten Fall wird die Anzahl der Dateien reduziert, von welchen der im Artikel erstellte Indikator abhängt, es wird aber die Durchführung immer der gleichen Berechnungen benötigt (zuerst werden die Kanalgrenzen für die Berechnung der zentralen Linie festgelegt, danach die Grenzen für die Bestimmung der Breite des Kanals). Im zweiten Fall werden die Berechnungen nicht doubliert, darüber hinaus bekommen wir einen zusätzlichen unabhängigen und vollständigen Indikator des Preiskanals, der auch separat verwendet werden kann.      

Im Anhang finden Sie die Datei CUniChannel.mqh mit abgeleiteten Klassen für alle anderen Indikatoren und den iPriceChannel Indikator. Die Daten der zentralen Linie des iPriceChannel Indikators befinden sich im Puffer 0. Wenn jemand die Klasse für einen anderen Indikator modifizieren wird, in welchem die benötigten Daten nicht im Puffer 0 sind, muss man noch eine zusätzliche abgeleitete Klasse Calculate erstellen oder eine Variable für den Pufferindex in der Basisklasse erstellen und diese auf den benötigten Wert im Kostruktor der abgeleiteten Klasse setzen.   

Klassen für die Berechnung der Breite und das Zeichnen des Kanals

Nehmen wir die CUniChannel Klasse wieder als Grundlage für die Basisklasse. In die Methode der Calculate Klasse wird der Puffer des Indikators mit den Werten der zentralen Linie übergeben, zwei Puffer für die Kanalgrenzen, die mit den in der Methode berechneten Werten ausgefüllt werden. Im Gegensatz zu CUniChannel gibt es für jede Berechnungsvariante der Grenzen eigene abgeleitete Calculate Klassen, die die Indikatoren laden und in welchen die Namen der Indikatoren und Puffern gebildet werden. Die Basisklasse muss ein bisschen modifiziert werden: es muss die Variable der Breite des Kanals hinzugefügt werden; der Wert der Variablen wird durch den Konstruktor der angeleiteten Klasse gesetzt.

Speichern wir die Datei CUniChannel.mqh unter dem Namen CUniChannelWidth.mqh und nehmen wir einige Änderungen vor. Zuerst löschen wir alle abgeleiteten Klassen und lassen wir nur die Basisklasse und die Calculate Klasse. Benennen wir die Klasse CChannelUni in CChannelUniWidth um (nicht vergessen den Konstruktor, Destruktor und den Namen der Basisklasse bei der abgeleiteten Klasse, die auch geändert werden müssen). Wir erhalten die folgende Klasse:

class CChannelUniWidth{
   protected:
      int m_handle;           // Handle des Indikators
      string m_name;          // Name des Indikators
      string m_label1;        // Name des Puffers 1      
      string m_help;          // kurze Hilfe zu den Parametern des Indikators
      double m_width;         // Breite des Kanals
   public:
  
      // Konstruktor
      void CChannelUniWidth(){
         m_handle=INVALID_HANDLE;
      }
      
      // Destruktor
      void ~CChannelUniWidth(){
         if(m_handle!=INVALID_HANDLE){
            IndicatorRelease(m_handle);
         }
      }
  
      // Hauptmethode, die aus der OnCalculate() Funktion des Indikators aufgerufen wird
      virtual int Calculate( const int rates_total,
                     const int prev_calculated,
                     double & bufferCentral[],
                     double & bufferUpper[],
                     double & bufferLower[],
      ){
         return(rates_total);
      }
      
      // Handle des geladenen Indikators erhalten
      int Handle(){
         return(m_handle);
      }
      
      // Methode der Überprüfung des Handles, um festzustellen, ob der Indikator geladen wurde  
      bool CheckHandle(){
         return(m_handle!=INVALID_HANDLE);
      }
      
      // den Namen des Indikators erhalten
      string Name(){
         return(m_name);
      }    

      // Text für Puffer erhalten
      string Label1(){
         return(m_label1);
      }
      
      // Tooltip mit den Parametern erhalten
      string Help(){
         return(m_help);
      }
};

Die CChannelUni_Calculate Klasse benennen wir in CChannelUni_Calculate_ATR um und fügen ihr einen Konstruktor hinzu. Der Konstruktor kann der COscUni_ATR Klasse des universellen Oszillators entnommen werden, man muss ihn aber umbenennen und ihm den Parameter der Breite hinzufügen. Es werden auch einige weitere Änderungen benötigt: man muss die Bildung der Namen des Indikators und der Puffer hinzufügen. Im Endeffekt wird die Klasse für die Berechnung der Grenzen basierend auf dem ATR Indikator wie folgt aussehen:

class CChannelUni_Calculate_ATR:public CChannelUniWidth{
   public:
      // Konstruktor
      // die ersten zwei Parameter sind gleich für alle abgeleiteten Klassen 
      // darauf folgen die Parameter des Indikators
      // der letzte Parameter - Breite des Kanals
      void CChannelUni_Calculate_ATR(bool use_default,
                                     bool keep_previous,
                                     int & ma_period,
                                     double & ch_width){
         if(use_default){ // Verwendung von Standardwerten ausgewählt
            if(keep_previous){ // die früher verwendeten Parameter nicht ändern
               if(ma_period==-1)ma_period=14;
               if(ch_width==-1)ch_width=2;
            }
            else{
               ma_period=14;
               ch_width=2;
            }      
         } 
         
         // Parameter der Breite für die Verwendung in der Berechnungsmethode speichern  
         m_width=ch_width; 
         // Indikator laden
         m_handle=iATR(Symbol(),Period(),ma_period);
         // String mit dem Namen des Indikators erstellen
         m_name=StringFormat("ATR(%i)",ma_period);
         // String mit dem Namen der Puffer
         m_label1=m_name;
         // Tooltip mit den Parametern 
         m_help=StringFormat("ma_period - Period1(%i)",ma_period); // Tooltip   
      }   
      
      // Hauptmethode, die aus der OnCalculate() Funktion aufgerufen wird
      // die ersten zwei Parameter entsprechen den ersten zwei Parametern
      // der OnCalculate() Funktion
      // danach werden die Indikator-Puffer übergeben 
      int Calculate( const int rates_total,
                     const int prev_calculated,
                     double & bufferCentral[],
                     double & bufferUpper[],
                     double & bufferLower[],
      ){
      
         // den Beginn der Berechnung definieren
         
         int start;
         
         if(prev_calculated==0){
            start=0
         }
         else
            start=prev_calculated-1;
         }  

         // Hauptschleife der Berechnung und der Ausfüllung der Puffer 

         for(int i=start;i<rates_total;i++){
            
            // Daten des Indikators für den Balken erhalten
            double tmp[1];
            if(CopyBuffer(m_handle,0,rates_total-i-1,1,tmp)<=0){
               return(0);
            }
            
            // Multiplizieren mit dem Parameter der Breite
            tmp[0]*=m_width;   

            // Berechnung der Werte der oberen und der unteren Grenzen
            bufferUpper[i]=bufferCentral[i]+tmp[0];
            bufferLower[i]=bufferCentral[i]-tmp[0];

         }   
         
         return(rates_total);
      }
};

Bitte beachten Sie, dass der Wert des ATR Indikators innerhalb der Hauptschleife nur für einen Balken kopiert wird. Diese Variante ist viel langsamer, als eine Reihe der Werte in den Puffer zu kopieren. Bei diesem Ansatz sparen wir einen ganzen Puffer, aber der Geschwindigkeitsverlust würde nur beim manuellen Hinzufügen des Indikators auf den Chart auftreten. Aber eine Verzögerung von einigen Zehntelsekunden ist für den Nutzer nicht relevant. Am Anfang des Testens befinden sich nur wenige Balken im Strategietester, deswegen wird der Zeitverlust beim Kopieren der Daten für jeden einzelnen Balken auch nicht bemerkbar.

Einige Varianten der Berechnung der Breite benötigen keine zusätzlichen Indikatoren, insbesondere, wenn die Breite in Punkten oder wie im Envelope Indikator festgelegt wird. In diesem Fall weisen wir der m_handle Variante der Basisklasse den Wert 0 zu (unterscheidet sich vom Wert INVALID_HANDLE).

Im Anhang finden Sie die fertige Datei CUniChannelWidth.mqh mit abgeleiteten Klassen für alle Varianten der Berechnung.   

Erstellung des Indikators des universellen Kanals

Nun haben wir die oben erstellten Klassen und können den Indikator des universellen Kanals erstellen, aber noch ohne grafischen Interface.

Erstellen wir einen neuen benutzerdefinierten Indikator unter dem Namen iUniChannel im Editor. Bei der Erstellung des Indikators wählen wir die folgenden Funktionen in MQL Wizard: OnCalculate(...,open,high,low,close), OnTimer, OnChartEvent sowie erstellen drei Puffer vom Typ Line.

Für die Auswahl des Typs der zentralen Linie und des Typs des Kanals müssen wir zwei Aufzählungen erstellen. Die Aufzählungen werden in der Datei UniChannelDefines.mqh sein. Erstellen wir die Aufzählungen entsprechend den Tabellen 1 und 3:

// Aufzählung der Typen der zentralen Linie
enum ECType{
   UniCh_C_AMA,
   UniCh_C_DEMA,
   UniCh_C_FrAMA,
   UniCh_C_MA,
   UniCh_C_TEMA,
   UniCh_C_VIDyA,
   UniCh_C_PrCh
};

// Aufzählung der Typen der Grenzen
enum EWType{
   UniCh_W_ATR,
   UniCh_W_StdDev,
   UniCh_W_Points,
   UniCh_W_Percents,
   UniCh_W_PrCh
};

Die Aufzählung der Typen der zentralen Linie hat den Namen ECType, die Aufzählung der Typen der Breite des Kanals hat den Namen EWType. Fügen wir dem Indikator die Datei mit den Aufzählungen und zwei früher erstellten Dateien mit Klassen hinzu:

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

Deklarieren wir zwei externe Variablen für die Auswahl von Typen der zentralen Linie und der Breite des Kanals und Variablen für die Parameter entsprechend der Tabellen 2 und 4:

// Parameter der zentralen Linie
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;
// Parameter der Grenzen
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;

Deklarieren wir zwei Variablen, die erstmal intern sein werden, aber in der Version mit dem grafischen Interface werden sie im Fenster der Eigenschaften ausgegeben:

bool                 UseDefault  =  false;
bool                 KeepPrev    =  false;

Der Verwendungszweck dieser Variablen wurde im Artikel über den universellen Oszillator ausführlich beschrieben: für die UseDefault Variable wird der Modus aktiviert, in welchem jeder neu ausgewählte Indikator mit Standardparametern geladen wird; für die KeepPrev Variable wird der Modus aktiviert, bei welchem die Werte der Parameter beim Wechseln der Indikatoren gespeichert werden. In der Version des Indikators ohne grafisches Interface wird der Indikator mit den Parametern aus dem Fenster der Eigenschaften geladen, deswegen ist UseDefault gleich false. Die KeepPrev Variable wird auch auf false gesetzt, denn es gibt noch kein grafisches Interface und keinen Wechsel zwischen den Indikatoren. 

Bei der Initialisierung des Indikators muss man Parameter vorbereiten. Genauso wie im universellen Oszillator machen wir die Vorbereitung der Parameter in einer separaten Funktion PrepareParameters(), aber zuerst machen wir Kopien von allen externen Variablen:

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;

Danach schreiben wir die Funktion der Vorbereitung der Paramater:

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;
   }
}

Bitte beachten Sie, wenn die Bedingung UseDefault && KeepPrev erfüllt ist, wird allen Variablen der Wert -1 zugewiesen, und den Shift Variablen - der Wert 0, denn die Werte dieser Variablen werden nicht aus den Objekten der Indikatoren gesetzt, sondern aus der Benutzeroberfläche (Fenster der Eigenschaften des Indikators oder des grafischen Interfaces).   

Nach der Vorbereitung der Parameter kann man Objekte für die Berechnung der zentralen Linie und des Kanals erstellen. Im universellen Oszillator gab es dafür die LoadOscillator() Funktion. Hier haben wir zwei Funktionen: LoadCentral() und LoadWidth(), aber zuerst deklarieren wir die Pointer-Variablen:

CChannelUni * central;
CChannelUniWidth * width;

Einige Indikatoren verfügen den Parameter der horizontalen Verschiebung (shift), einige haben diesen Parameter nicht, dennoch kann jeder Indikator verschoben werden. Deswegen deklarieren wir eine zusätzliche Variable shift0 mit dem Wert 0, und diese wird den Konstruktoren der Klassen übergeben. Die Verschiebung erfolgt durch die Verschiebung der Indikator-Puffer.

Die LoadCentral() Funktion:

void LoadCentral(){
   switch(_CentralType){ // je nach dem ausgewählten Typ wird eine Klasse erstellt
      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;
   }
}

In einer der Berechnungsvarianten der Breite (Klasse CChannelUni_Calculate_InPoints) gibt es einen Parameter in Punkten, und in der Klasse ist die Anpassung diesen Parameters entsprechend der Anzahl der Stellen nach dem Komma in den Kursen vorgesehen. Für die Anpassungsfunktion muss bei der Erstellung des Objekts der Multiplikator des Parameters dem Konstruktor der Klasse übergeben werden. Für Kurse mit zwei und vier Stellen wird der Multiplikator gleich 1 sein 1, und für drei- und fünfstellige Kurse - 10. In den externen Parametern deklarieren wir die Auto5Digits Variable vom Typ bool:

input bool                 Auto5Digits   =  true;

Wenn Auto5Digits gleich true ist, wird der Parameter korrigiert, wenn false, wird der Wert verwendet so wie er ist. Unter Auto5Digits deklarieren wir eine weitere Variable für den Multiplikator:

int mult;

Ganz am Anfang der OnInit() Funktion berechnen wir den Wert mult:

   if(Auto5Digits && (Digits()==3 || Digits()==5)){
      mult=10; // multiplizieren Parameter in Punkten mit 10
   }
   else{
      mult=1; // Parameter in Punkten bleiben unverändert
   }

Nun schreiben wir die LoadWidth() Funktion:

void LoadWidth(){
   switch(_WidthType){ //je nach dem ausgewählten Typ wird eine Klasse erstellt
      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;
   }
}

Nach der Erstellung der Objekte (der zentralen Linie und der Breite) überprüfen wir, ob das erfolgreich war. Wenn die Objekte erfolgreich erstellt wurden, legen wir einen kurzen Namen des Indikators fest und geben wir den Tooltip mit den Parametern über die Print() Funktion aus: 

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

Die Beschriftungen und Verschiebungen werden den Puffern in der SetStyles() Funktion gesetzt:

void SetStyles(){

   // Namen der Puffer
   PlotIndexSetString(0,PLOT_LABEL,"Central: "+central.Label1());
   PlotIndexSetString(1,PLOT_LABEL,"Upper: "+width.Label1());  
   PlotIndexSetString(2,PLOT_LABEL,"Lower: "+width.Label1());  
  
   // Verschiebung der Puffer
   PlotIndexSetInteger(0,PLOT_SHIFT,_c_Shift);
   PlotIndexSetInteger(1,PLOT_SHIFT,_w_Shift);
   PlotIndexSetInteger(2,PLOT_SHIFT,_w_Shift);

}

Als Ergebnis bekommen wir die folgende OnInit() Funktion:

int OnInit(){
  
   // Vorbereitung des Multiplikators für die Korrektur des Parameters in Punkten
   if(Auto5Digits && (Digits()==3 || Digits()==5)){
      mult=10;
   }
   else{
      mult=1;
   }
  
   // Vorbereitung der Parameter
   PrepareParameters();
  
   // Laden des Indikators der zentralen Linie
   LoadCentral();
  
   // überprüfen, ob die zentrale Linie erfolgreich geladen wurde
   if(!central.CheckHandle()){
      Alert("Central line error "+central.Name());
      return(INIT_FAILED);
   }    
  
   // den Indikator für die Berechnung der Breite laden
   LoadWidth();
  
   // überprüfen, ob der Indikator der Breite erfolgreich geladen wurde
   if(!width.CheckHandle()){
      Alert("Width error "+width.Name());
      return(INIT_FAILED);
   }      

   // Ausgabe der Tooltips
   Print("Central line parameters matching: "+central.Help());
   Print("Width parameters matching: "+width.Help());  
  
   // Namen setzen
   ShortName="iUniChannel";  
   IndicatorSetString(INDICATOR_SHORTNAME,ShortName);  
  
   // Standardteil der OnInit Funktion
   SetIndexBuffer(0,Label1Buffer,INDICATOR_DATA);
   SetIndexBuffer(1,Label2Buffer,INDICATOR_DATA);
   SetIndexBuffer(2,Label3Buffer,INDICATOR_DATA);
  
   // Namen und Verschiebungen der Puffer setzen
   SetStyles();

   return(INIT_SUCCEEDED);
}

Damit ist die Erstellung des Indikators ohne grafisches Interface abgeschlossen. Mithilfe dieses Indikators kann man testen, wie alle Klassen und Parameter funktionieren, und danach mit der Erstellung des grafischen Interfaces anfangen.

Während des Testens des Indikators wurden einige negative Besonderheiten festgestellt Einer der Nachteile besteht darin, dass die Periode der zentralen Linie und die Periode der Berechnung der Breite des Kanals getrennt gesteuert werden. Eine solche Steuerung erweitert die Möglichkeiten des Indikators, aber in einigen Fälle muss man beide Perioden mithilfe eines Parameters steuern. Erweitern wir den Indikator so, dass die Periode der Breite gleich einer der drei Perioden der zentralen Linie ist. Die Auswahl einer der vier Varianten erfolgt durch eine Aufzählung (befindet sich in der Datei UniChannelDefines.mqh):

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

Bei der Auswahl der LockTo_Off Variante werden die Perioden separat reguliert, und in anderen Fällen entspricht der Wert des w_Period Parameters der Periode der zentralen Linie. Deklarieren wir die Variable vom Typ ELockTo direkt nach der w_Period Variablen:

input ELockTo              w_LockPeriod  =  LockTo_Off;

Modifizieren wir die PrepareParameters() Funktion und fügen wir den folgenden Code ganz unten hinzu:

switch(w_LockPeriod){ // je nach dem Typ des Blockierens
   case LockTo_Period1:
      _w_Period=_c_Period1;
   break;
   case LockTo_Period2:
      _w_Period=_c_Period2;      
   break;
   case LockTo_Period3:
      _w_Period=_c_Period3;      
   break;
}

Ein weiterer Nachteil besteht darin, dass Informationsmeldungen über die Übereinstimmung der Parameter in den Reiter "Experten" ausgegeben werden: auf kleinen Bildschirmen geht ein Teil des Strings über den Rand hinaus. Erweitern wir den Indikator so, dass der String in eine Spalte ausgegeben wird. Verwenden wir statt der Print Funktion die eigene PrintColl() Funktion. Dieser Funktion werden zwei Parameter übergeben: die Überschrift und der String mit dem Tooltip. In der Funktion wird der String mit dem Tooltip gesplittet und in Teilen ausgegeben:

void PrintColl(string caption,string message){
   Print(caption); // Ausgabe der Überschrift
   string res[];
   // Nachricht splitten
   int cnt=StringSplit(message,',',res);
   // Ausgabe der Nachricht in Teilen
   for(int i=0;i<cnt;i++){
      StringTrimLeft(res[i]);
      Print(res[i]);
   }
}

Dementsprechend werden in der OnInit() Funktion zwei Strings für die Ausgabe der Tooltips geändert:

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

Nun ist der Indikator fertig, Name der Datei im Anhang - "iUniChanhel". Fangen wir mit der Erstellung des grafischen Interfaces an.   

Erstellung der Klassen des grafischen Interfaces

Das grafische Interface wird auf Basis des grafischen Interfaces des universellen Oszillators erstellt. Kopieren wir die Datei UniOsc/UniOscGUI.mqh in den Ordner "UniChannel" und benennen wir ihn in UniChannelGUI.mqh um. Das grafische Interface des universellen Kanals wird sich vom Interface des universellen Oszillators wesentlich unterscheiden, deswegen steht uns viel Arbeit bevor.

Der grundlegende Unterschied besteht darin, dass beim universellen Kanal zwei Indikatoren (der zentralen Linie und der Grenzen) unabhängig voneinander auswählt werden müssen, deswegen muss es zwei Hauptlisten der Auswahl eines Indikatortyps geben. Der ersten Liste müssen die Controls für die Parameter der zentralen Linie folgen, danach folgt die zweite Liste und die Controls der Parameter der Grenzen. Das heißt dass die zweite Liste keine festgelegten Koordinaten hat, sie müssen berechnet werden. Außer zwei Listen für die Auswahl der Typen, müssen immer zwei Felder für die Eingabe des Wertes der Verschiebung vorhanden sein, ihre Koordinaten sind auch nicht festgelegt. Ein weiteres wichtiges Moment ist die Liste für die Auswahl einer Variante, die dem w_LockPeriod Parameter entspricht. Immer wenn das Eingabefeld für den w_Period Parameter in der Gruppe der Controls für die Parameter der Breite angezeigt werden muss, muss man eine zusätzliche Dropdown-Liste anzeigen.

Zunächst einmal nehmen wie allgemeine Änderungen in der Datei UniChannelGUI.mqh vor:

1. Der Pfad zur Datei mit den Aufzählungen:

#include <UniOsc/UniOscDefines.mqh>

muss durch den folgenden String ersetzt werden:

#include <UniChannel/UniChannelDefines.mqh>

2. Fügen wir das Array mit den Werten der Aufzählung ELockTo hinzu:

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

3. Löschen wir die Arrays mit den Aufzählungen ENUM_APPLIED_VOLUME und ENUM_STO_PRICE.

Nun fangen wir an, die CUniOscControls Klasse zu ändern.  

Klasse der Controls der zentralen Linie

1. Benennen wir die CUniOscControls Klasse in CUniChannelCentralControls um.

2. Löschen wir die Deklaration der Variablen m_volume und m_sto_price in der Klasse. Dementsprechend löschen wir alles, was mit diesen Elementen verbunden ist, aus den Methoden SetPointers(), Hide() und Events().

3. Fügen wir die Variable m_last_y hinzu, in dieser Variablen wird die Y Koordinate des letzten Steuerelements festgelegt. Fügen wir eine Methode für das Erhalten des Wertes dieser Variablen hinzu - GetLastY(). Die FormHeight() Methode wird überflüssig, deswegen löschen wir sie, stattdessen fügen wir die ControlsCount() Methode hinzu, die die Anzahl der Controls in der abgeleiteten Klasse zurückgeben wird. Diese Anzahl wird für die Berechnung der Höhe der Form benötigt.

Als Ergebnis bekommen wir die folgende Basisklasse:

class CUniChannelCentralControls{
   protected:
      CSpinInputBox * m_value1; // für die Periode 1
      CSpinInputBox * m_value2; // für die Periode 2
      CSpinInputBox * m_value3; // für die Periode 3
      CComBox * m_price;        // für den Preis
      CComBox * m_method;       // für die Methode
      int m_last_y;             // Y Position des letzten Steuerelements

   public:
  
   // Y-Position des letzten Steuerelements erhalten
   int GetLastY(){
      return(m_last_y);
   }
  
   // Methode für die Übergabe der Pointer auf Objekte ins Objekt
   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);
   }
  
   // Gruppe der Controls ausblenden
   void Hide(){
      m_value1.Hide();
      m_value2.Hide();
      m_value3.Hide();
      m_price.Hide();
      m_method.Hide();
   }
  
   // Verarbeitung der Ereignisse
   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);
   }
  
   // Methode zur Initialisierung der Controls (für die Änderung der Beschriftungen)
   virtual void InitControls(){
   }  
  
   // Anzeige der Gruppe der Controls
   virtual void Show(int x,int y){
   }  
  
   // Anzahl der Controls in der Gruppe erhalten
   virtual int ControlsCount(){
      return(0);
   }      
};

Ändern wir die abgeleitete Klasse CUniOscControls_ATR:

1. Benennen wir sie in CUniChannelCentralControls_AMA um, das wird eine Klasse für den AMA Indikator sein.

2. Entsprechend der Spalte "Parameter" aus der Tabelle 1 initialisieren wir die Controls in der InitControls() Methode, und in der Show() Methode rufen wir die Show() Methoden aller Controls auf. Der m_last_y Variablen weisen wir den Wert des letzten Steuerelements zu.

3. Löschen wir die FormHeight() Methode, stattdessen fügen wir die ControlsCount() Methode hinzu.

Wir erhalten die folgende Klasse:

class CUniChannelCentralControls_AMA:public CUniChannelCentralControls{
   void InitControls(){
      // Initialisierung der Controls
      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");      
   }
  
   // Anzeige
   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;
   }
  
   // Erhalten der Anzahl der Elemente in der Gruppe
   int ControlsCount(){
      return(4);
   }
};

Die Klassen für alle anderen Indikatoren der zentralen Linie werden auf die gleiche Weise erstellt, alle alten abgeleiteten Klassen der Oszillatoren werden gelöscht.

Klassen der Controls für die Berechnung der Breite

Basierend auf der erhaltenen Klasse CUniChannelCentralControls erstellen wir eine Klasse der Controls für die Parameter der Breite des Kanals. Erstellen wir eine Kopie der Klasse CUniChannelCentralControls und benennen wir diese in CUniChannelWidthControls um. In dieser Klasse werden zwei Eingabefelder (Periode und Breite), zwei Standardaufzählungen für den Mittelungstyp und Preis sowie die Aufzählung des w_LockPeriod Parameters benötigt. Nach den Änderungen bekommen wir die folgende Klasse:

class CUniChannelWidthControls{
   protected:
      CSpinInputBox * m_value1; // für die Periode
      CSpinInputBox * m_value2; // für die Breite    
      CComBox * m_price;        // für den Preis
      CComBox * m_method;       // für die Methode
      CComBox * m_lockto;       // für den Typ des Blocks
      int m_last_y;             // die Y-Position des letzten Steuerelements

   public:
  
   // Y-Position des letzten Steuerelements erhalten
   int GetLastY(){
      return(m_last_y);
   }
  
   // Methode für die Übergabe der Pointer auf Objekte ins Objekt
   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);      
   }
  
   // Gruppe der Controls ausblenden
   void Hide(){
      m_value1.Hide();
      m_value2.Hide();
      m_price.Hide();
      m_method.Hide();
   }
  
   // Verarbeitung der Ereignisse
   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);
   }
  
   // Methode zur Initialisierung der Controls (für die Änderung der Beschriftungen)
   virtual void InitControls(){
   }  
  
   // Anzeige der Gruppe der Controls
   virtual void Show(int x,int y){
   }  
  
   // Anzahl der Controls in der Gruppe erhalten
   virtual int ControlsCount(){
      return(0);
   }    
};

Erstellen wir abgeleitete Klassen. Der Hauptunterschied von der Klasse der zentralen Linie besteht darin, dass nach dem Eingabefeld eine Dropdownliste für den w_LockPeriod Parameter erstellt werden muss. Wir erhalten die folgende Klasse für die Berechnung der Breite nach ATR:

class CUniChannelWidthControls_ATR:public CUniChannelWidthControls{
   void InitControls(){
      // Initialisieung eines Steuerelements
      m_value1.Init("w_value1",SPIN_BOX_WIDTH,1," period");
   }
  
   // Anzeige der Gruppe der Controls
   void Show(int x,int y){
      m_value1.Show(x,y);
      y+=20;
      m_lockto.Show(x,y);
      m_last_y=y;
   }  
  
   // Anzahl der Controls in der Gruppe erhalten
   int ControlsCount(){
      return(2);
   }    
};

Die Klassen für andere Varianten der Berechnung der Breite wurden auf die gleiche Weise erstellt. 

Nun befinden sich in der Datei UniChannelGUI.mqh zwei Basisklassen der Controls, viele abgeleitete Klassen der Controls und die Klasse der Form. An der letzten muss noch gearbeitet werden. Wegen der Größe der Datei, werden weitere Operationen in der Datei unpraktisch sein, deswegen speichern wir die Klassen der Controls in anderen Dateien. Erstellen wir die Datei UniChannel/CUniChannelCentralControls.mqh und übertragen wir die CUniChannelCentralControls Klasse sowie alle von ihr abgeleiteten Klassen in diese Klasse und fügen wir ihr zusätzliche Dateien hinzu: 

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

Verschieben wir die Deklaration der Konstanten FORM_WIDTH, SPIN_BOX_WIDTH und COMBO_BOX_WIDTH in die Datei UniChannelDefines.mqh. Danach kann die Datei CUniChannelCentralControls kompiliert werden, um Fehler zu beheben. Die Klasse CUniChannelWidthControls muss auch in eine separate Datei verschoben werden. Danach können wir an der Klasse der Form arbeiten. 

Klasse der Form

Fügen wir der Datei UniChannelGUI.mqh die zwei gerade eben erstellten Dateien hinzu:

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

Benennen wir die CUniOscForm Klasse in CUniChannelForm um. In der public Sektion löschen wir die Pointer-Variable vom Typ CUniOscControls und deklarieren wir zwei weitere Pointer-Variablen: CUniChannelCentralControls und CUniChannelWidthControls. Im Endeffekt haben wir die folgenden Variablen in der public Sektion:

CComBox           m_c_cmb_main;  // Auswahlliste der zentralen Linie
CSpinInputBox     m_c_value1;    // Feld für die Eingabe der Periode 1
CSpinInputBox     m_c_value2;    // Feld für die Eingabe der Periode 2
CSpinInputBox     m_c_value3;    // Feld für die Eingabe der Periode 3
CComBox           m_c_price;     // Auswahlliste des Preises
CComBox           m_c_method;    // Auswahlliste der Methode
CSpinInputBox     m_c_shift;     // Feld für die Eingabe der Verschiebung

CComBox           m_w_cmb_main;  // Auswahlliste der Grenzen
CSpinInputBox     m_w_value1;    // Feld der Eingabe der Periode
CSpinInputBox     m_w_value2;    // Feld für die Eingabe der Breite  
CComBox           m_w_price;     // Auswahlliste des Preises
CComBox           m_w_method;    // Auswahlliste der Methode
CComBox           m_w_lockto;    // Auswahlliste des Blockierens    
CSpinInputBox     m_w_shift;     // Feld für die Eingabe der Verschiebung          

// Gruppe der Controls der zentralen Linie
CUniChannelCentralControls * m_central_controls;
// Gruppe der Controls der Grenzen
CUniChannelWidthControls * m_width_controls;  

In der MainProperties() Methode ändern wir die Werte der m_Name und m_Caption Variablen, andere Variablen bleiben unverändert:

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;
}

In der OnInitEvent() Methode rufen wir die Init() Methoden aller Controls auf, deren Beschriftungen sich nicht ändern (Set der Controls, der dem ausgewählten Indikator entspricht) und füllen wir die Dropdown-Listen aus:

void OnInitEvent(){

   // Initialisierung der Controls, die zu keiner Gruppe gehören
  
   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");
  
   // Ausfüllung der Dropdown-Listen
  
   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]));            
   }
  
   // Erlaubnis, Verschiebungen von der Tastatur einzugeben            
   m_c_shift.SetReadOnly(false);
   m_w_shift.SetReadOnly(false);                        
}

In den OnShowEvent() Methoden zeigen wir alle Controls an, dabei erhalten wir nach der Anzeige der Gruppen der Elemente die Y-Koordinate und zeigen die folgenden Controls entsprechend der Koordinate an:

void OnShowEvent(int aLeft, int aTop){
   m_c_cmb_main.Show(aLeft+10,aTop+10);        // Auswahlliste der zentralen Linie
   m_central_controls.Show(aLeft+10,aTop+30);  // Gruppe der Controls der Parameter der zentralen Linie
   int m_y=m_central_controls.GetLastY();      // die Koordinate des letzten Steuerelements erhalten
   m_c_shift.Show(aLeft+10,m_y+20);            // Feld für die Eingabe des Verschiebungsparameters
   m_w_cmb_main.Show(aLeft+10,m_y+40);         // Auswahlliste des Kanals
   m_width_controls.Show(aLeft+10,m_y+60);     // Gruppe der Controls der Kanalparameter
   m_y=m_width_controls.GetLastY();            // Koordinate des letzten Steuerelements
   m_w_value2.Show(aLeft+10,m_y+20);           // Feld für die Eingabe der Breite
   m_w_shift.Show(aLeft+10,m_y+40);            // Feld für die Eingabe des Verschiebungsparameters
}

In der OnHideEvent() Methode blenden wir die Controls aus:

void OnHideEvent(){
   m_c_cmb_main.Hide();       // Auswahlliste der zentralen Linie    
   m_central_controls.Hide(); // Gruppe der Controls der Parameter der zentralen Linie
   m_c_shift.Hide();          // Feld für die Eingabe des Verschiebungsparameters
   m_w_cmb_main.Hide();       // Auswahlliste des Kanals
   m_width_controls.Hide();   // Gruppe der Controls der Kanalparameter
   m_w_shift.Hide();          // Feld für die Eingabe des Verschiebungsparameters
   m_w_lockto.Hide();         // Auswahl des Typs des Blockierens der Periode
   m_width_controls.Hide();   // Feld für die Eingabe der Breite
}

Nehmen wir Änderungen in der SetValues() Methode vor. Ändern wir den Set der Parameter, in der Methode setzen wir allen Controls die Werte, die diesen Parametern entsprechen:

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  
){

   // Feld für die Eingabe der Parameter der zentralen Linie
   m_c_value1.SetValue(c_value1);
   m_c_value2.SetValue(c_value2);      
   m_c_value3.SetValue(c_value3);
   m_c_shift.SetValue(c_shift);        

   // Feld für die Eingabe der Parameter des Kanals
   m_w_value1.SetValue(w_value1);
   m_w_value2.SetValue(w_value2);        
   m_w_shift.SetValue(w_shift);            
  
   // Anzeige ausgewählter Typen in den Auswahllisten der Glättungsmethoden
   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);
      }            
   }
  
   // Anzeige ausgewählter Typen in den Auswahllisten des Preistyps
   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);
      }            
   }

   // Anzeige des ausgewählten Typs des Blockierens der Kanalperiode
   for(int i=0;i<ArraySize(e_lockto);i++){
      if(w_lockto==e_lockto[i]){
         m_w_lockto.SetSelectedIndex(i);
         break;
      }
   }                    
}

Statt der SetType() Methode erstellen wir zwei Methoden: SetCentralType() — für das Setzen des Typs der zentralen Linie und SetWidthType() — für das Setzen der Typgrenzen. Am Ende jeder Methode werden nach der Erstellung der Objekte den Controls die Eigenschaften gesetzt, die es ermöglichen, Werte von der Tastatur einzugeben. Setzen wir auch die minimal mögliche Werte und rufen wir die private Methode der Berechnung der Höhe der Form:  

Die SetCentralType() Methode:

void SetCentralType(long type){
   // wenn das Objekt bereits früher erstellt wurde, löschen wir es
   if(CheckPointer(m_central_controls)==POINTER_DYNAMIC){
      delete(m_central_controls);
      m_central_controls=NULL;
   }
   switch((ECType)type){ // je nach dem ausgewählten Typ wird ein Objekt erstellt
      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;
   }    
  
   // Pointer auf Objekte der Controls übergeben
   m_central_controls.SetPointers(m_c_value1,m_c_value2,m_c_value3,m_c_price,m_c_method);
   // Initialisierung der Controls
   m_central_controls.InitControls();
  
   // Eingabe von der Tastatur erlauben
   m_c_value1.SetReadOnly(false);
   m_c_value2.SetReadOnly(false);
   m_c_value3.SetReadOnly(false);
  
   // minimal mögliche Werte setzen
   m_c_value1.SetMinValue(1);        
   m_c_value2.SetMinValue(1);
   m_c_value3.SetMinValue(1);            
  
   // Berechnung der Höhe der Form
   this.SolveHeight();

}

 SetWidthType() Methode:

void SetWidthType(long type){
   // wenn das Objekt bereits früher erstellt wurde, löschen wir es
   if(CheckPointer(m_width_controls)==POINTER_DYNAMIC){
      delete(m_width_controls);
      m_width_controls=NULL;
   }
   switch((EWType)type){ // je nach dem ausgewählten Typ wird ein Objekt erstellt
      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;
   }    

   // Pointer auf Objekte der Controls übergeben
   m_width_controls.SetPointers(m_w_value1,m_w_value2,m_w_price,m_w_method);
   // Initialisierung der Controls
   m_width_controls.InitControls();
  
   // minimal zulässige Werte setzen
   m_w_value1.SetReadOnly(false);
   m_w_value2.SetReadOnly(false);
  
   // minimal zulässige Werte setzen
   m_w_value1.SetMinValue(1);        
   m_w_value2.SetMinValue(0);
  
   // Berechnung der Höhe der Form
   this.SolveHeight();
              
}

Am Ende der Methoden SetCentralType() und SetWidthType()  wird die Methode SolveHeight() für die Berechnung der Höhe der Form aufgerufen:

void SolveHeight(){
   // wenn beide Objekte existieren (der zentralen Linie und der Breite)
   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;
   }      
}  

Nun können wir den Indiaktor und das grafische Interface miteinander verbinden.  

Verknüpfung des Indikators und des grafischen Interfaces

Speichern wird den iUniChannel Indikator unter dem Namen iUniChannelGUI. Analog zum iUniOscGUI Indikator fügen wir dem Fenster der Eigenschaften den externen Parameter UseGUI ganz oben hinzu. Im Anschluß platzieren wir die Variablen UseDefault und KeepPrev, setzen sie standardmäßig auf true und geben sie im Fenster der Eigenschaften aus:

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

Fügen wir zwei Dateien mit dem grafischen Interface hinzu (auch da, wo die Dateien mit den Klassen der Indikatoren hinzugefügt werden):

#include <UniChannel/UniChannelGUI.mqh>

Der OnInit() Funktion fügen wir ganz unten den Code des grafischen Interfaces hinzu, aber zuerst brauchen wir die Arrays mit den Typen der zentralen Linie und der Grenzen. Fügen wir diese unterhalb der externen Parametern des Indikators hinzu:

// Array mit den Typen der zentralen Linie
ECType ctype[]={
   UniCh_C_AMA,
   UniCh_C_DEMA,
   UniCh_C_FrAMA,
   UniCh_C_MA,
   UniCh_C_TEMA,
   UniCh_C_VIDyA,
   UniCh_C_PrCh
};

// Array mit den Typen der Grenzen
EWType wtype[]={
   UniCh_W_ATR,
   UniCh_W_StdDev,
   UniCh_W_Points,
   UniCh_W_Percents,
   UniCh_W_PrCh
};

Hier fügen wir auch die Pointer-Variable der Klasse der Form:

CUniChannelForm * frm;

Ganz am Ende der OnInit() Funktion erstellen wir das Objekt des grafischen Interfaces:

if(UseGUI){
  
   // Erstellung und Initialisieung des Objekts der Form
   frm=new CUniChannelForm();
   frm.Init();
  
   // Hilfsvariablen
   int ind1=0;
   int ind2=0;
  
   // Suche des ausgewählten Typs der zentralen Linie im Array der Typen der zentralen Linie
   for(int i=0;i<ArraySize(ctype);i++){        
      frm.m_c_cmb_main.AddItem(EnumToString(ctype[i]));
      if(ctype[i]==_CentralType){
         ind1=i;
      }
   }
  
   // Suche des ausgewählten Typs der Kanalgrenzen im Array der Typen der Grenzen
   for(int i=0;i<ArraySize(wtype);i++){        
      frm.m_w_cmb_main.AddItem(EnumToString(wtype[i]));
      if(wtype[i]==_WidthType){
         ind2=i;
      }
   }      
  
   // Anzeige des ausgewählten Typs der zentralen Linie in der Liste
   frm.m_c_cmb_main.SetSelectedIndex(ind1);      
   // Vorbereitung der Controls, die dem Typ entsprechen
   frm.SetCentralType(_CentralType);
  
   // Anzeige des ausgewählten Typs der Grenzen in der Liste
   frm.m_w_cmb_main.SetSelectedIndex(ind2);      
   frm.SetWidthType(_WidthType);      
  
   // Werte setzen
   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
   );
  
   // Eigenschaften der Form setzen
   frm.SetSubWindow(0);
   frm.SetPos(10,30);
   // Anzeige der Form
   frm.Show();
}  

Außer der Erstellung des Objekts der Form, werden die Auswahllisten der Indikatoren ausgefüllt, sie werden auf die ausgewählten Varianten gesetzt. Legen wir auch alle anderen Werte in den Controls fest.  Beim Hinzufügen des Indikators auf den Chart wird die Form mit den Steuerelementen angezeigt (Abb. 1).


Abb. 1. Form mit Controls des universellen Kanals

Die Controls wurden angezeigt, nun müssen wir das Funktionieren der Buttons sichern. In der OnChartEvent() Funktion werden sechs verschiedene Ereignisse verarbeitet. Die Verarbeitung einiger von ihnen ist sehr aufwendig, deswegen erfolgt sie in separaten Funktionen:

void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
{
   // Ereignisse der Form
   if(frm.Event(id,lparam,dparam,sparam)==1){
      EventForm();
   }
  
   // Auswahl des Typs der zentralen Linie
   if(frm.m_c_cmb_main.Event(id,lparam,dparam,sparam)==1){
      EventCentralTypeChange();
   }  
  
   // Auswahl des Grenzentyps
   if(frm.m_w_cmb_main.Event(id,lparam,dparam,sparam)==1){
      EventWidthTypeChange();
   }  
  
   // Änderung der Parameter der zentralen Linie
   if(frm.m_central_controls.Event(id,lparam,dparam,sparam)==1){
      EventCentralParametersChange();
   }  
  
   // Änderung der Parameter der Grenzen
   if(frm.m_width_controls.Event(id,lparam,dparam,sparam)==1){
      EventWidthParametersChange();

   }  

   // Änderung der Verschiebungsparameter
   if(frm.m_c_shift.Event(id,lparam,dparam,sparam)!=0 ||
      frm.m_w_shift.Event(id,lparam,dparam,sparam)
   ){
      EventShift();
   }    
}

Betrachten wir alle diese Funktionen. Die Funktion EventForm():

void EventForm(){      
   int win=ChartWindowFind(0,ShortName);  // Definition des Unterfensters des Indikators  
   ChartIndicatorDelete(0,win,ShortName); // Löschen des Indikators
   ChartRedraw();
}  

Diese Funktion wird beim Schließen der Form durch das Anklicken des Buttons mit dem Kreuz ausgeführt, dabei erfolgt die Suche des Fensters des Indikators nach seinem kurzen Namen und das Löschen des Indikators. 

Die Funktion EventCentralTypeChange():

void EventCentralTypeChange(){    
   // Erhalten eines neuen Typs in die Variable
   _CentralType=ctype[frm.m_c_cmb_main.SelectedIndex()];
  
   // Löschen des alten Objekts und die Erstellung eines neuen
   delete(central);
   LoadCentral(true);
  
   // Überprüfung, ob der Indikator geladen wurde
   if(!central.CheckHandle()){
      Alert("Fehler beim Laden des Indikators"+central.Name());
   }

   // Festlegung der Verschiebungen und Namen der Puffer
   SetStyles();

   // Festlegung eines neuen Typs in der Liste
   frm.SetCentralType(ctype[frm.m_c_cmb_main.SelectedIndex()]);
   // Aktualisierung der Werte der Parameter in der Form
   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
   );
   // Aktualisierung der Form
   frm.Refresh();
  
   // Starten des Timers für die Neuberechnung des Indikators
   EventSetMillisecondTimer(100);
}

In dieser Funktion wird der Typ des Indikators der zentralen Linie geändert. Zuerst wird der Typ des ausgewählten Indikators erhalten, das alte Objekt wird gelöscht, ein neues wird erstellt. Bei der Erstellung des neuen Objekts können sich einige seine Parameter ändern (wegen der UseDefault Funktion), deswegen wird die SetValues() Methode für das Setzen neuer Werte für die Controls aufgerufen, die Anzeige der Form wird aktualisiert (Refresh() Methode). Am Ende wird der Timer gestartet, um den Indikator neu zu berechnen.      

Die EventWidthTypeChange() Funktion ist der EventCentralTypeChange() Funktion gleich, darauf gehen wir nicht ausführlich ein. 

In den Funktionen EventCentralParametersChange() und EventWidthParametersChange() wird die Reaktion der Indikatoren auf die Änderung der Parameter des Indikators gesichert. Diese zwei Funktionen sind von der Funktionalität her identisch. Aber die Änderung der Parameter erfordert, das Blockieren der Perioden zu berücksichtigen und die Parameter entsprechend dem Blockieren zu korrigieren, deswegen haben die Funktionen ihre einmaligen Besonderheiten, gehen wir auf beide näher ein.

void EventCentralParametersChange(){          
  
   // Variable, die die Notwendigkeit anzeigt, den Indikator der Grenzen neu zu starten
   bool dolock=false;
  
   // Änderung des Wertes der Periode 1
   if((int)frm.m_c_value1.Value()>0){
      // Zuweisung der Variablen des Wertes vom Steurelement
      _c_Period1=(int)frm.m_c_value1.Value();
      // wenn die Periode 1 mit der Periode des Indikators der Breite verbunden ist
      if(_w_LockPeriod==LockTo_Period1){
         // weisen wir der Variablen mit der Periode des Indikators der Breite den Wert der Periode 1 zu
         _w_Period=_c_Period1;
         // zeigen wir ihn auf der Form an
         frm.m_w_value1.SetValue(_w_Period);
         // weisen wir darauf hin, dass der zweite Indikator neu gestartet werden muss
         dolock=true;
      }
   }
  
   // die Änderung des Wertes der Periode 2 ist gleich der Änderung der Periode 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;
      }        
   }
  
   // die Änderung des Wertes der Periode 3 ist gleich der Änderung der Periode 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;
      }        
   }
  
   // Änderung der Methode
   if(frm.m_c_method.SelectedIndex()!=-1){
      _c_Method=e_method[frm.m_c_method.SelectedIndex()];
   }
  
   // Änderung des Preises
   if(frm.m_c_price.SelectedIndex()!=-1){
      _c_Price=e_price[frm.m_c_price.SelectedIndex()];
   }
  
   // Löschen des alten Objekts und Erstellen eines neuen
   delete(central);
   LoadCentral(false);
   if(!central.CheckHandle()){
      Alert("Fehler beim Laden des Indikators"+central.Name());
   }  

   // Löschen und Erstellen eines neuen Objektes des zweiten Indikators
   if(dolock){
      delete(width);
      LoadWidth(false);
      if(!width.CheckHandle()){
         Alert("Fehler beim Laden des Indikators "+width.Name());
      }  
   }  

   // Verschiebungen und Puffernamen setzen
   SetStyles();

   // Timer für eine neue Berechnung des Indikators starten
   EventSetMillisecondTimer(100);
}  

In dieser Funktion wird bei der Änderung einer der drei Perioden der Wert des Parameters des Blockierens überprüft. Im Falle des Blockierens wird der Parameter für den Indikator der Grenzen geändert und in der Form aktualisiert. Die dolock Variable wird auf true gesetzt. Am Ende wird das alte Objekt des Indikators gelöscht, es wird ein neues erstellt und wenn die dolock Variable gleich true ist, wird das alte Objekt der Grenzen gelöscht und ein neues wird erstellt. Danach wird der Timer gestartet, er berechnet die Indikatoren neu.

void EventWidthParametersChange(){  
      
   // Variable, die die Notwendigkeit anzeigt, den Indikator der zentralen Linie neu zu starten
   bool dolock=false;

   // Änderung der Periode
   if((int)frm.m_w_value1.Value()>0){
      // Zuweisung der Variablen des Wertes vom Steurelement
      _w_Period=(int)frm.m_w_value1.Value();
      // Blockieren
      // der Parameter der Breite ist mir der ersten Periode der zentralen Linie verbunden
      if(_w_LockPeriod==LockTo_Period1){
         // der Variablen des Indikators der zentralen Linie wird ein neuer Wert zugewiesen
         _c_Period1=_w_Period;
         // Aktualisierung der Werte in der Form
         frm.m_c_value1.SetValue(_c_Period1);
         // weisen wir auf die Notwendigkeit hin, den Indikator der Breite neu zu starten
         dolock=true;
      }
      else if(_w_LockPeriod==LockTo_Period2){ // wenn die Blockierung mit der Periode 2
         _c_Period2=_w_Period;
         frm.m_c_value2.SetValue(_c_Period2);
         dolock=true;
      }
      else if(_w_LockPeriod==LockTo_Period3){ // wenn die Blockierung mit der Periode 3
         _c_Period3=_w_Period;
         frm.m_c_value3.SetValue(_c_Period3);
         dolock=true;
      }
   }
  
   // Änderung der Breite des Kanals
   if((double)frm.m_w_value2.Value()>0){
      _w_Width=(double)frm.m_w_value2.Value();
   }      
  
   // Änderung der Methode
   if(frm.m_w_method.SelectedIndex()!=-1){
      _w_Method=e_method[frm.m_w_method.SelectedIndex()];
   }
  
   // Änderung des Preises
   if(frm.m_w_price.SelectedIndex()!=-1){
      _w_Price=e_price[frm.m_w_price.SelectedIndex()];
   }
  
   // Ereignis der Änderung des Typs des Blockieren der Perioden in der Auswahlliste
   if(frm.m_w_lockto.SelectedIndex()>=0){
      // der Variablen den Wert aus dem Steuerungelement zuweisen
      _w_LockPeriod=e_lockto[frm.m_w_lockto.SelectedIndex()];
      // wenn das Blockieren mit einer der Perioden ausgewählt wurde,
      // wird ihr Wert kopiert und in der Form aktualisiert  
      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);
      }
   }      

   // das alte Objekt löschen und ein neues erstellen
   delete(width);
   LoadWidth(false);
   if(!width.CheckHandle()){
      Alert("Fehler beim Laden des Indikators"+width.Name());
   }
  
   // Löschen und Erstellen eines neuen Objektes des zweiten Indikators
   if(dolock){
      delete(central);
      LoadCentral(false);
      if(!central.CheckHandle()){
         Alert("Fehler beim Laden des Indikators"+central.Name());
      }
   }

   // Verschiebungen und Puffernamen setzen
   SetStyles();

   // Starten des Timers, der den Indikator neu berechnet
   EventSetMillisecondTimer(100);      
}  

In dieser Funktion wird der Typ des Blockierens überprüft, und wenn nötig, wird die entsprechende Periode des Indikators der zentralen Linie geändert. Wenn ein Ereignis aus der Liste der Auswahl des Typs des Blockierens auftritt, dann wird der Variablen der Periode des Grenzen-Indikators der Wert aus der entsprechenden Variablen des Indikators der zentralen Linie zugewiesen.

Die Verarbeitung des Ereignisses der Änderung der Verschiebungswerte ist relativ einfach:

void EventShift(){     
   // Erhalten der neuen Werte in der Variablen 
   _c_Shift=(int)frm.m_c_shift.Value();
   _w_Shift=(int)frm.m_w_shift.Value();
   // Festlegen neuer Stile
   SetStyles();
   // Aktualisierung des Charts
   ChartRedraw();
}

Den Variablen werden die Werte aus den Controls zugewiesen, die SetStyles() Funktion wird aufgerufen und der Chart wird aktualisiert.

Damit ist der Indikators mit grafischem Interface quasi fertig.

Beim Testen des Indikators wurde ein Nachteil aufgedeckt. Wenn der externe Parameter UseDefault aktiviert ist und das Blockieren der Periode verwendet wird, funktioniert das Blockieren nicht. Das ist damit verbunden, dass beim Laden des zweiten Indikators (des Indikators der Breite) die Parameter in seinem Konstruktor geändert werden. Um diesen Fehler zu beheben, mussten wir einige abgeleitete Klassen der Indikatoren der Breite modifizieren. Den Konstruktoren der Klassen CChannelUni_Calculate_ATR, CChannelUni_Calculate_StdDev und CChannelUni_Calculate_PriceChannel wurde der freie Parameter locked mit dem Standardwert false hinzugefügt (wenn der Parameter der Klasse nicht übergeben wird, arbeitet alles ohne Änderungen). Beim Setzen locked=true und use_default=true ändern sich die Parameter der Periode im Konstruktor (sofern locked=true). Betrachten wir ein Fragment der CChannelUni_Calculate_ATR Klasse:

if(use_default){
   if(keep_previous){
      if(ma_period==-1 && !locked)ma_period=14// Änderung
      if(ch_width==-1)ch_width=2;
   }
   else{
      if(!locked)ma_period=14// Änderung
      ch_width=2;
   }      
}

Der ma_period Variablen wird der Standardwert nur dann zugewiesen, wenn die Variable locked gleich false ist. Dementsprechend wird die LoadWidth() Funktion modifiziert. Am Anfang der Funktion wird der Locked Wert berechnet:

bool Locked=(w_LockPeriod!=LockTo_Off);

Danach wird diese Variable den Konstruktoren der Klassen bei der Erstellung der Objekte übergeben.

Fügen wir die Möglichkeit hinzu, das Farbschema zu ändern und sichern wir das Speichern der Parameter des Indikators beim Wechsel der Timeframe, wie wir das im universellen Oszillator gemacht haben. Die Verwendung von Farbschemata wird in diesem Artikel nicht betrachtet, denn das Thema wurde bereits bei der Erstellung des universellen Oszillators erläutert. Fangen wir mit dem Speichern der Parameter an.

Erstellen wir grafische Objekte mit den Werten der Parameter in der OnDeinit() Funktion, wenn der Grund der Deinitialisierung der Wechsel des Charts ist. Erstellen wir diese grafische Objekte außerhalb der Sichtweite des Charts:

void SaveOrDeleteParameters(const int reason){
   // wenn der Chart nicht gewechselt wurde, löschen wir die grafischen Objekte 
   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// beim Wechseln des Charts erstellen wir grafische Objekte mit den Werten der Parameter
      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);        
   }
}

// Hilfsfunktion für das Speichern eines Parameters im grafischen Objekt
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);
}

Direkt nach dem Aufruf der PrepareParameters() Funktion rufen wir die LoadSavedParameters() Funktion in der OnInit() Funktion auf:

bool LoadSavedParameters(){
   // wenn alle Objekte mit den Parametern vorliegen 
   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
   ){
      // Erhalten der Werte aus den grafischen Objekten
      _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);
   }
}

In der Funktion wird überprüft, ob diese Objekte existieren, und wenn ja, werden ihre Werte verwendet, dabei liefert die Funktion true. Wenn die Funktion true zurückgegeben hat, rufen wir die Funktionen LoadCentral() und LoadWidth() mit dem Parameter false (damit die Standardparameter nicht gesetzt werden). Fragment der OnInit() Funktion:

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

Auf die gleiche Weise wird die Funktion LoadWidth() aufgerufen:

LoadWidth(!ChartCange);

Damit ist die Erstellung des universellen Kanals vollendet. 

Fazit

Obwohl wir viel vom fertigen Code des universellen Oszillators verwendet haben, erfordert die Erstellung des universellen Kanals viel Arbeit. Der grundlegende Unterschied vom universellen Oszillator besteht darin, dass es zwei unabhängige Blöcke gibt: den der zentralen Linie und den der Grenzen. Dies hat das Arbeitspensum quasi verdoppelt.. Der Algorithmus der Änderung der Parameter ist wegen der Funktion des Blockierens der Periode auch komplizierter geworden. Des Weiteren ist auch das Hinzufügen neuer Indikatoren schwieriger geworden, denn es gibt nun zwei Indikatoren. Darüber hinaus wurde eine neue Möglichkeit hinzugefügt, und zwar das Speichern der Parameter beim Wechsel der Timeframe. Im Endeffekt haben wir einen Indikator erhalten, der genauso hilfreich wie der universelle Oszillator ist. Dieser Indikator erweitert die Möglichkeiten des Kanals, denn nun kann man die zentrale Linie und die Methode des Zeichnens der Grenzen separat auswählen, was in einer großen Anzahl verschiedener Kombinationen resultiert. Die Erhöhung der Geschwindigkeit der Anwendung des Indikators durch das grafische Interface ermöglicht es, alle Kombinationen visuell zu erforschen.

Anhang

Im Anhang finden Sie eine Zip-Datei mit allen notwendigen Dateien. Alle Dateien befinden sich in den Ordnern so, wie es im Terminal sein muss.