Das MQL5-Kochbuch: Entwicklung eines mehrwährungsfähigen Kursschwankungsindikators in MQL5

Anatoli Kazharski | 1 Juni, 2016

Einleitung

In diesem Beitrag befassen wir uns mit der Entwicklung eines mehrwährungsfähigen Kursschwankungsindikators. Jemanden, der gerade erst beginnt, in MQL5 zu programmieren, kann die Entwicklung von Indikatoren für mehrere Währungen vor einige Schwierigkeiten stellen, aber nach der Lektüre dieses Beitrages sollte alles wesentlich einfacher sein. Die grundlegenden Fragen bei der Entwicklung mehrwährungsfähiger Indikatoren beziehen sich auf die Abstimmung der Daten anderer Kürzel auf die des aktuellen Kürzels, die Lösung des Problems des Nichtvorhandenseins eines Teils der Indikatordaten sowie auf die Ermittlung des Anfangs der „echten“ Balken des jeweiligen Zeitraums. All das wird in dem hier vorliegenden Beitrag ausführlich behandelt.

Die Werte des Indikators Average True Range (ATR) sind bereits für jedes Kürzel bzw. jeden Bezeichner berechnet, wenn wir sie erhalten. Als Beispiel nehmen wir sechs Kürzel, deren Bezeichnungen in den externen Indikatorparametern angegeben werden können. Die Richtigkeit der eingegebenen Bezeichnungen wird überprüft. Sollte eines der in den Parametern angegebenen Kürzel in der allgemeinen Aufstellung nicht vorhanden sein, so werden zu diesem keine Berechnungen durchgeführt. Alle gefundenen Kürzel werden in dem Fenster der Marktübersicht (Market Watch) abgelegt, sofern sie dort nicht bereits vorhanden sind.

In dem vorhergehenden Beitrag mit dem Titel Das MQL5-Kochbuch: Steuerelemente des Indikatorunterfensters - Die Bildlaufleiste wurde die Leinwand bereits vorgestellt, auf der wir Text ausgeben und sogar zeichnen können. Hier werden wir nicht auf der Leinwand zeichnen, sondern stattdessen Meldungen zum Stand der Programmausführung ausgeben, die uns helfen zu verstehen, was gerade vor sich geht.

 

Der Ablauf der Entwicklung des Indikators

Beginnen wir mit der Entwicklung des Programms. Legen Sie mithilfe des MQL5-Assistenten eine Schablone, ein Template, für einen benutzerdefinierten Indikator an. Nach einigen geringfügigen Anpassungen müsste etwa Folgendes herauskommen:

//+------------------------------------------------------------------+
//|                                               MultiSymbolATR.mq5 |
//|                        Copyright 2010, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
//--- Indicator properties
#property copyright "Copyright 2010, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
#property version   "1.00"
#property indicator_separate_window // Indicator is in a separate subwindow
#property indicator_minimum 0       // Minimum value of the indicator
#property indicator_buffers 6       // Number of buffers for indicator calculation
#property indicator_plots   6       // Number of plotting series
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Initialization completed successfully
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Deinitialization                                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int      rates_total,     // Size of input time series
                const int      prev_calculated, // Bars processed at the previous call
                const datetime &time[],         // Opening time
                const double   &open[],         // Open prices
                const double   &high[],         // High prices
                const double   &low[],          // Low prices
                const double   &close[],        // Close prices
                const long     &tick_volume[],  // Tick volumes
                const long     &volume[],       // Real volumes
                const int      &spread[])       // Spread
  {
//--- Return the size of the data array of the current symbol
   return(rates_total);
  }
//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer()
  {
  }
//+------------------------------------------------------------------+

Wir werden diese Schablone mit allem füllen, was erforderlich ist, damit unsere Idee Wirklichkeit wird. Die Notwendigkeit des Vorhandenseins eines Zeitgebers wird in diesem Beitrag weiter untersucht. Ganz am Anfang, gleich nach den besonderen Eigenschaften des Indikators fügen wir folgende Konstanten hinzu:

//--- Constants 
#define RESET           0 // Returning the indicator recalculation command to the terminal
#define LEVELS_COUNT    6 // Number of levels
#define SYMBOLS_COUNT   6 // Number of symbols

Die Konstante LEVELS_COUNT enthält den Wert der Anzahl der Grenzen (Ebenen), die durch grafische Objekte der Art „waagerechte Linie“ (OBJ_HLINE) wiedergegeben werden. In den externen Indikatorparametern können die Werte für diese Grenzen (Ebenen) eingegeben werden.

Wir hängen an unser Projekt eine Datei mit der für die Arbeit mit benutzerdefinierten Grafiken erforderlichen Klasse an:

//--- Include the class for working with the canvas
#include <Canvas\Canvas.mqh>

In den externen Parametern werden der Mittelungszeitraum für den Kursschwankungsindikator iATR sowie die Bezeichnungen der Kürzel, deren Schwankungen abgebildet werden sollen, und die Werte der waagerechten Linien angegeben. Die Nummerierung der Kürzel beginnt mit 2, da das erste dasjenige ist, auf dessen Diagramm der Indikator geladen wird.

//--- External parameters
input  int              IndicatorPeriod=14;       // Averaging period
sinput string dlm01=""; //- - - - - - - - - - - - - - - - - - - - - - - - - - -
input  string           Symbol02       ="GBPUSD"; // Symbol 2
input  string           Symbol03       ="AUDUSD"; // Symbol 3
input  string           Symbol04       ="NZDUSD"; // Symbol 4
input  string           Symbol05       ="USDCAD"; // Symbol 5
input  string           Symbol06       ="USDCHF"; // Symbol 6
sinput string dlm02=""; //- - - - - - - - - - - - - - - - - - - - - - - - - - -
input  int              Level01        =10;       // Level 1
input  int              Level02        =50;       // Level 2
input  int              Level03        =100;      // Level 3
input  int              Level04        =200;      // Level 4
input  int              Level05        =400;      // Level 5
input  int              Level06        =600;      // Level 6

Weiterhin müssen im Code alle für die Arbeit benötigten globalen Variablen und Datenfelder angelegt werden. Sie werden alle in den unten aufgeführten Code ausführlich kommentiert und vorgestellt:

//--- Global variables and arrays
CCanvas           canvas;                 // Loading the class
//--- Variables/arrays for copying data from OnCalculate()
int               OC_rates_total     =0;  // Size of input time series
int               OC_prev_calculated =0;  // Bars processed at the previous call
datetime          OC_time[];              // Opening time
double            OC_open[];              // Open prices
double            OC_high[];              // High prices
double            OC_low[];               // Low prices
double            OC_close[];             // Close prices
long              OC_tick_volume[];       // Tick volumes
long              OC_volume[];            // Real volumes
int               OC_spread[];            // Spread
//--- Structure of buffers for drawing indicator values
struct buffers {double data[];};
buffers           atr_buffers[SYMBOLS_COUNT];
//--- Structure of time arrays for data preparation
struct temp_time {datetime time[];};
temp_time         tmp_symbol_time[SYMBOLS_COUNT];
//--- Structure of arrays of the ATR indicator values for data preparation
struct temp_atr {double value[];};
temp_atr          tmp_atr_values[SYMBOLS_COUNT];
//--- For the purpose of storing and checking the time of the first bar in the terminal
datetime          series_first_date[SYMBOLS_COUNT];
datetime          series_first_date_last[SYMBOLS_COUNT];
//--- Time of the bar from which we will start drawing
datetime          limit_time[SYMBOLS_COUNT];
//--- Indicator levels
int               indicator_levels[LEVELS_COUNT];
//--- Symbol names
string            symbol_names[SYMBOLS_COUNT];
//--- Symbol handles
int               symbol_handles[SYMBOLS_COUNT];
//--- Colors of indicator lines
color             line_colors[SYMBOLS_COUNT]={clrRed,clrDodgerBlue,clrLimeGreen,clrGold,clrAqua,clrMagenta};
//--- String representing the lack of the symbol
string            empty_symbol="EMPTY";
//--- Indicator subwindow properties
int               subwindow_number        =WRONG_VALUE;              // Subwindow number
int               chart_width             =0;                        // Chart width
int               subwindow_height        =0;                        // Subwindow height
int               last_chart_width        =0;                        // Last saved chart width
int               last_subwindow_height   =0;                        // Last saved subwindow height
int               subwindow_center_x      =0;                        // Horizontal center of the subwindow
int               subwindow_center_y      =0;                        // Vertical center of the subwindow
string            subwindow_shortname     ="MS_ATR";                 // Short name of the indicator
string            prefix                  =subwindow_shortname+"_";  // Prefix for objects
//--- Canvas properties
string            canvas_name             =prefix+"canvas";          // Canvas name
color             canvas_background       =clrBlack;                 // Canvas background color
uchar             canvas_opacity          =190;                      // Opacity
int               font_size               =16;                       // Font size
string            font_name               ="Calibri";                // Font
ENUM_COLOR_FORMAT clr_format              =COLOR_FORMAT_ARGB_RAW;    // Color components should be correctly set by the user
//--- Canvas messages
string            msg_invalid_handle      ="Invalid indicator handle! Please wait...";
string            msg_prepare_data        ="Preparing data! Please wait...";
string            msg_not_synchronized    ="Unsynchronized data! Please wait...";
string            msg_load_data           ="";
string            msg_sync_update         ="";
string            msg_last                ="";
//--- Maximum number of bars specified in the terminal settings
int               terminal_max_bars=0;

Beim Laden des Indikators in das Diagramm werden in der Funktion OnInit() folgende Maßnahmen ausgeführt:

Praktischer wäre es, all dies in einzelne Funktionen aufzuspalten. In der Folge erhält der Code der Funktion OnInit() eine gut lesbare Form, wie man hier sehen kann:

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Check input parameters for correctness
   if(!CheckInputParameters())
      return(INIT_PARAMETERS_INCORRECT);
//--- Set the timer at 1-second intervals
   EventSetTimer(1);
//--- Set the font to be displayed on the canvas
   canvas.FontSet(font_name,font_size,FW_NORMAL);
//--- Initialization of arrays
   InitArrays();
//--- Initialize the array of symbols 
   InitSymbolNames();
//--- Initialize the array of levels
   InitLevels();
//--- Get indicator handles
   GetIndicatorHandles();
//--- Set indicator properties
   SetIndicatorProperties();
//--- Get the number of bars specified in the terminal settings
   terminal_max_bars=TerminalInfoInteger(TERMINAL_MAXBARS);
//--- Clear the comment
   Comment("");
//--- Refresh the chart
   ChartRedraw();
//--- Initialization completed successfully
   return(INIT_SUCCEEDED);
  }

Wir betrachten die benutzerdefinierten Funktionen in dem obigen Code eingehender. In der Funktion CheckInputParameters() erfolgt die Überprüfung der externen Parameter auf ihre Richtigkeit. In unserem Fall wird nur ein Parameter geprüft, der Zeitraum des IndikatorsATR. Ich habe den Grenzwert auf 500 festgelegt. Das bedeutet, dass der Indikator, wenn ein größerer Zeitraum eingestellt wird, seine Tätigkeit einstellt und sowohl im Protokoll als auch in dem Kommentar auf dem Diagramm eine Meldung über den Grund des Programmabbruchs erscheint. Unten folgt der Code der Funktion CheckInputParameters().

//+------------------------------------------------------------------+
//| Checking input parameters for correctness                        |
//+------------------------------------------------------------------+
bool CheckInputParameters()
  {
   if(IndicatorPeriod>500)
     {
      Comment("Decrease the indicator period! Indicator Period: ",IndicatorPeriod,"; Limit: 500;");
      printf("Decrease the indicator period! Indicator Period: %d; Limit: %d;",IndicatorPeriod,500);
      return(false);
     }
//---
   return(true);
  }

Übrigens muss man für den schnelleren Übergang zur Definition einer bestimmten Funktion den Mauszeiger auf die Bezeichnung der Funktion setzen und die Tastenkombination Alt+G betätigen oder mit der rechten Maustaste das Kontextmenü der gewünschten Funktion aufrufen und dortWeiter zur Definition (Go to Definition) auswählen. Wenn sich die Definition der Funktion in einer anderen Datei befindet, wird diese Datei in dem Bearbeitungsprogramm geöffnet. Ebenso können verknüpfte Bibliotheken und Klassen geöffnet werden. Das ist überaus praktisch.

Es folgen drei Funktionen zur Bereitstellung der Datenfelder: InitArrays(), InitSymbolNames() und InitLevels(). Ihre entsprechenden Programmcodes werden unten aufgeführt:

//+------------------------------------------------------------------+
//| First initialization of arrays                                   |
//+------------------------------------------------------------------+
void InitArrays()
  {
   ArrayInitialize(limit_time,NULL);
   ArrayInitialize(series_first_date,NULL);
   ArrayInitialize(series_first_date_last,NULL);
   ArrayInitialize(symbol_handles,INVALID_HANDLE);
//---
   for(int s=0; s<SYMBOLS_COUNT; s++)
      ArrayInitialize(atr_buffers[s].data,EMPTY_VALUE);
  }
//+------------------------------------------------------------------+
//| Initializing array of symbols                                    |
//+------------------------------------------------------------------+
void InitSymbolNames()
  {
   symbol_names[0]=AddSymbolToMarketWatch(_Symbol);
   symbol_names[1]=AddSymbolToMarketWatch(Symbol02);
   symbol_names[2]=AddSymbolToMarketWatch(Symbol03);
   symbol_names[3]=AddSymbolToMarketWatch(Symbol04);
   symbol_names[4]=AddSymbolToMarketWatch(Symbol05);
   symbol_names[5]=AddSymbolToMarketWatch(Symbol06);
  }
//+------------------------------------------------------------------+
//| Initializing array of levels                                     |
//+------------------------------------------------------------------+
void InitLevels()
  {
   indicator_levels[0]=Level01;
   indicator_levels[1]=Level02;
   indicator_levels[2]=Level03;
   indicator_levels[3]=Level04;
   indicator_levels[4]=Level05;
   indicator_levels[5]=Level06;
  }

In der Funktion InitSymbolNames() kommt eine weitere benutzerdefinierte Funktion zur Anwendung, die Funktion AddSymbolToMarketWatch(). In ihr wird die Bezeichnung des Währungspaares (Kürzels) weitergegeben, und wenn dieses in der allgemeinen Aufstellung aufgeführt ist, wird es in das Fenster der Marktübersicht eingestellt und die Funktion gibt eine Zeile mit der Bezeichnung des Kürzels aus. Ist dieses Kürzel nicht vorhanden, so gibt die Funktion die Zeichenfolge EMPTY aus, und in der Folge werden bei der Überprüfung der anderen Funktionen in Bezug auf dieses Element in dem Datenfeld mit den Kürzeln keinerlei Handlungen ausgeführt.

//+------------------------------------------------------------------+
//| Adding the specified symbol to the Market Watch window           |
//+------------------------------------------------------------------+
string AddSymbolToMarketWatch(string symbol)
  {
   int      total=0; // Number of symbols
   string   name=""; // Symbol name
//--- If an empty string is passed, return the empty string
   if(symbol=="")
      return(empty_symbol);
//--- Total symbols on the server
   total=SymbolsTotal(false);
//--- Iterate over the entire list of symbols
   for(int i=0;i<total;i++)
     {
      //--- Symbol name on the server
      name=SymbolName(i,false);
      //--- If this symbol is available,
      if(name==symbol)
        {
         //--- add it to the Market Watch window and
         SymbolSelect(name,true);
         //--- return its name
         return(name);
        }
     }
//--- If this symbol is not available, return the string representing the lack of the symbol
   return(empty_symbol);
  }

GetIndicatorHandles() ist eine weitere Funktion, die bei der Bereitstellung des Indikators aufgerufen wird. In ihr erfolgt der Versuch, die Bezeichner des Indikators ATR für jedes angegebene Kürzel zu beziehen. Konnte der Bezeichner für ein Kürzel nicht bezogen werden, gibt die Funktion „false“ aus, was jedoch in OnInit() nicht weiterverarbeitet wird, da das Vorhandensein der Bezeichner auch noch in anderen Teilen des Programms überprüft wird.

//+------------------------------------------------------------------+
//| Getting indicator handles                                        |
//+------------------------------------------------------------------+
bool GetIndicatorHandles()
  {
//--- An indication of all handles being valid
   bool valid_handles=true;
//--- Iterate over all symbols in a loop and ...
   for(int s=0; s<SYMBOLS_COUNT; s++)
     {
      //--- If the symbol is available
      if(symbol_names[s]!=empty_symbol)
        {
         // And if the handle of the current symbol is invalid
         if(symbol_handles[s]==INVALID_HANDLE)
           {
            //--- Get it
            symbol_handles[s]=iATR(symbol_names[s],Period(),IndicatorPeriod);
            //--- If the handle could not be obtained, try again next time
            if(symbol_handles[s]==INVALID_HANDLE)
               valid_handles=false;
           }
        }
     }
//--- Print the relevant message if the handle for one of the symbols could not be obtained
   if(!valid_handles)
     {
      msg_last=msg_invalid_handle;
      ShowCanvasMessage(msg_invalid_handle);
     }
//---
   return(valid_handles);
  }

Die Funktion ShowCanvasMessage() sehen wir uns später mit den übrigen Funktionen für die Arbeit mit der „Leinwand“ an.

Die Indikatoreigenschaften werden in der Funktion SetIndicatorProperties() eingerichtet. Da die Eigenschaften für jede grafische Reihe gleichartig sind, ist es praktischer, das alles in Zyklen abzuarbeiten:

//+------------------------------------------------------------------+
//| Setting indicator properties                                     |
//+------------------------------------------------------------------+
void SetIndicatorProperties()
  {
//--- Set the short name
   IndicatorSetString(INDICATOR_SHORTNAME,subwindow_shortname);
//--- Set the number of decimal places
   IndicatorSetInteger(INDICATOR_DIGITS,_Digits);
//--- Define buffers for drawing
   for(int s=0; s<SYMBOLS_COUNT; s++)
      SetIndexBuffer(s,atr_buffers[s].data,INDICATOR_DATA);
//--- Set labels for the current symbol
   for(int s=0; s<SYMBOLS_COUNT; s++)
      PlotIndexSetString(s,PLOT_LABEL,"ATR ("+IntegerToString(s)+", "+symbol_names[s]+")");
//--- Set the plotting type: lines
   for(int s=0; s<SYMBOLS_COUNT; s++)
      PlotIndexSetInteger(s,PLOT_DRAW_TYPE,DRAW_LINE);
//--- Set the line width
   for(int s=0; s<SYMBOLS_COUNT; s++)
      PlotIndexSetInteger(s,PLOT_LINE_WIDTH,1);
//--- Set the line color
   for(int s=0; s<SYMBOLS_COUNT; s++)
      PlotIndexSetInteger(s,PLOT_LINE_COLOR,line_colors[s]);
//--- Empty value for plotting where nothing will be drawn
   for(int s=0; s<SYMBOLS_COUNT; s++)
      PlotIndexSetDouble(s,PLOT_EMPTY_VALUE,EMPTY_VALUE);
  }

Nach der erfolgreichen Programmbereitstellung erfolgt zwangsläufig der erste Aufruf der Funktion OnCalculate(). Die Variable prev_calculated weist beim ersten Aufruf der Funktion den Wert „0“ auf. Sie wird ebenfalls genullt, wenn ein zu weit zurückreichender Kursverlauf geladen wird oder die Lücken in einem Verlauf aufgefüllt werden. In diesen Fällen werden die Indikatorzwischenspeicher vollständig neu berechnet. Falls dieser Parameter einen anderen Wert als Null aufweist, das heißt, wenn das Ergebnis der vorhergehenden Ausgabe eben dieser Funktion, das die Größe der eingehenden Zeitreihen wiedergibt, reicht es aus, den jeweils letzten Wert der Zwischenspeicher (Puffer) zu aktualisieren.

Möglicherweise gelingt es nicht immer, alle Berechnungen beim ersten Mal korrekt auszuführen. In diesem Fall verwenden wir die den Wert Null enthaltende Konstante RESET um zum Ausgang zurückzukehren. Beim nächsten Aufruf von OnCalculate() (etwa bei der nächsten Kursänderung) enthält der Parameter prev_calculated den Wert „0“, was bedeutet, dass vor der Ausgabe der grafischen Reihe des Indikators auf dem Bildschirm ein weiterer Versuch zur Ausführung aller erforderlichen Berechnungen nötig sein wird.

Aber wenn der Markt geschlossen ist, und keine Kursänderungen stattfinden, bleibt das Diagramm ebenso leer wie nach fehlgeschlagenen Berechnungen. Es ist in diesem Fall nicht schwer, den Befehl zu einem erneuten Versuch zu erteilen, wir müssen lediglich den Diagrammzeitraum manuell umschalten. Aber wir gehen einen anderen Weg. Genau dazu wurde die Zeitgeberfunktion OnTimer() in den Anfang der Schablone aufgenommen, während das Intervall der Funktion OnInit() auf eine Sekunde eingestellt wird.

Der Zeitgeber überprüft jede Sekunde einmal, ob von der Funktion OnCalculate() der Wert „0“ ausgegeben wurde. Dazu schreiben wir die Funktion CopyDataOnCalculate(), durch die alle Parameter aus OnCalculate() in die gleichnamigen globalen Variablen und Datenfelder mit dem Präfix OC_ kopiert werden.

//+------------------------------------------------------------------+
//| Copying data from OnCalculate                                    |
//+------------------------------------------------------------------+
void CopyDataOnCalculate(const int      rates_total,
                         const int      prev_calculated,
                         const datetime &time[],
                         const double   &open[],
                         const double   &high[],
                         const double   &low[],
                         const double   &close[],
                         const long     &tick_volume[],
                         const long     &volume[],
                         const int      &spread[])
  {
   OC_rates_total=rates_total;
   OC_prev_calculated=prev_calculated;
   ArrayCopy(OC_time,time);
   ArrayCopy(OC_open,open);
   ArrayCopy(OC_high,high);
   ArrayCopy(OC_low,low);
   ArrayCopy(OC_close,close);
   ArrayCopy(OC_tick_volume,tick_volume);
   ArrayCopy(OC_volume,volume);
   ArrayCopy(OC_spread,spread);
  }

Der Aufruf dieser Funktion muss ganz am Anfang des Hauptteils der Funktion OnCalculate() erfolgen. Dort muss ebenfalls eine weitere benutzerdefinierte Funktion untergebracht werden, und zwar ResizeCalculatedArrays(), in der die Größe für die Datenfelder zur Aufbereitung der Daten eingestellt wird, bevor sie in den Indikatorzwischenspeicher abgelegt werden. Die Größe dieser Datenfelder muss der Größe der eingehenden Zeitreihen entsprechen.

//+------------------------------------------------------------------+
//| Resizing the size of arrays to the size of the main array        |
//+------------------------------------------------------------------+
void ResizeCalculatedArrays()
  {
   for(int s=0; s<SYMBOLS_COUNT; s++)
     {
      ArrayResize(tmp_symbol_time[s].time,OC_rates_total);
      ArrayResize(tmp_atr_values[s].value,OC_rates_total);
     }
  }

Außerdem legen wir die Funktion ZeroCalculatedArrays() an, die die Datenfelder zur Aufbereitung der Daten vor ihrer Ausgabe auf dem Bildschirm mit Werten von „0“ bereitstellt.

//+------------------------------------------------------------------+
//| Zeroing out arrays for data preparation                          |
//+------------------------------------------------------------------+
void ZeroCalculatedArrays()
  {
   for(int s=0; s<SYMBOLS_COUNT; s++)
     {
      ArrayInitialize(tmp_symbol_time[s].time,NULL);
      ArrayInitialize(tmp_atr_values[s].value,EMPTY_VALUE);
     }
  }

Und genau diese Funktion wird auch zur vorläufigen Nullsetzung der Indikatorpuffer verwendet. Wir nennen sie ZeroIndicatorBuffers().

//+------------------------------------------------------------------+
//| Zeroing out indicator buffers                                    |
//+------------------------------------------------------------------+
void ZeroIndicatorBuffers()
  {
   for(int s=0; s<SYMBOLS_COUNT; s++)
      ArrayInitialize(atr_buffers[s].data,EMPTY_VALUE);
  }

Im Augenblick hat die Funktion OnCalculate() die unten vorgestellte Form. Zudem habe ich die grundlegenden Operationen mit später zu füllenden Hinweisen versehen (in Form von Kommentaren mit darunter stehenden Auslassungspunkten).

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int      rates_total,     // Size of input time series
                const int      prev_calculated, // Bars processed at the previous call
                const datetime &time[],         // Opening time
                const double   &open[],         // Open prices
                const double   &high[],         // High prices
                const double   &low[],          // Low prices
                const double   &close[],        // Close prices
                const long     &tick_volume[],  // Tick volumes
                const long     &volume[],       // Real volumes
                const int      &spread[])       // Spread
  {
//--- For the purpose of determining the bar from which the calculation shall be made
   int limit=0;
//--- Make a copy of the OnCalculate() parameters
   CopyDataOnCalculate(rates_total,prev_calculated,
                       time,open,high,low,close,
                       tick_volume,volume,spread);
//--- Set the size to arrays for data preparation
   ResizeCalculatedArrays();
//--- If this is the first calculation or if a deeper history has been loaded or gaps in the history have been filled
   if(prev_calculated==0)
     {
      //--- Zero out arrays for data preparation
      ZeroCalculatedArrays();
      //--- Zero out indicator buffers
      ZeroIndicatorBuffers();
      //--- Other checks
      // ...
      //--- If you reached this point, it means that OnCalculate() will return non-zero value and this needs to be saved
      OC_prev_calculated=rates_total;
     }
//--- If only the last values need to be recalculated
   else
      limit=prev_calculated-1;

//--- Prepare data for drawing
// ...
//--- Fill arrays with data for drawing
// ...

//--- Return the size of the data array of the current symbol
   return(rates_total);
  }

Der Code der Funktion OnTimer() sieht momentan wie folgt aus:

//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer()
  {
//--- If for some reason calculations have not been completed or
//    a deeper history has been loaded or
//    gaps in the history have been filled, 
//    then make another attempt without waiting for the new tick
   if(OC_prev_calculated==0)
     {
      OnCalculate(OC_rates_total,OC_prev_calculated,
                  OC_time,OC_open,OC_high,OC_low,OC_close,
                  OC_tick_volume,OC_volume,OC_spread);
     }
  }

Wir kommen jetzt zu den übrigen Funktionen, die zum Einsatz kommen, wenn die Variable prev_calculated den Wert „0“ hat. In diesen Funktionen werden:

Darüber hinaus wird zu jedem Kürzel der erste „echte“ Balken ermittelt. Die kurze Definition habe ich mir ausgedacht, um das weitere Vorgehen zu vereinfachen. Das bedeutet Folgendes. Alle Zeiträume in MetaTrader 5 beruhen auf Minutendaten. Aber wenn zum Beispiel auf dem Server ab 1993 Tagesdaten vorhanden sind, Minutendaten dagegen erst seit 2000, werden, wenn, sagen wir, ein Stundenzeitraum in das Diagramm aufgenommen werden soll, die Balken erst ab dem Beginn der Minutendaten, also ab 2000, angelegt. Alles, was vor 2000 liegt, wird entweder in Form von Tagesdaten oder in Form der dem aktuellen Zeitraum am nächsten kommenden Daten wiedergegeben. Deshalb sollten zu den nicht zu dem aktuellen Zeitraum gehörenden Daten keine Indikatordaten abgebildet werden, um keine Verwirrung zu stiften. Genau dazu ermitteln wir den ersten „echten“ Balken des aktuellen Zeitraums und kennzeichnen ihn durch eine senkrechte Linie in derselben Farbe wie der Indikatorzwischenspeicher für das Kürzel.

Zur Entwicklung automatischer Handelssysteme ist die Ermittlung „echter“ Balken ebenfalls wichtig, da, wenn die Parameter für einen bestimmten Zeitraum ermittelt werden, die Daten aus anderen Zeiträumen unangemessen sind.

Bevor die oben dargestellten Überprüfungen vorgenommen werden, wird in dem Unterfenster des Indikators eine Leinwand angelegt. Deshalb programmieren wir zunächst alle Funktionen, die nötig sind, um die Leinwand zu verwalten. Vor der Einfügung der Leinwand in das Unterfenster müssen ihre Maße sowie die Koordinaten zur Anzeige von Textmitteilungen auf der Leinwand festgelegt werden. Dazu dient die Funktion GetSubwindowGeometry(), wir schreiben:

//+------------------------------------------------------------------+
//| Getting geometry of the indicator subwindow                      |
//+------------------------------------------------------------------+
void GetSubwindowGeometry()
  {
//--- Get the indicator subwindow number
   subwindow_number=ChartWindowFind(0,subwindow_shortname);
//--- Get the subwindow width and height
   chart_width=(int)ChartGetInteger(0,CHART_WIDTH_IN_PIXELS);
   subwindow_height=(int)ChartGetInteger(0,CHART_HEIGHT_IN_PIXELS,subwindow_number);
//--- Calculate the center of the subwindow
   subwindow_center_x=chart_width/2;
   subwindow_center_y=subwindow_height/2;
  }

Sobald die Eigenschaften des Unterfensters bekannt sind, kann die Leinwand hinzugefügt werden. Ihr Hintergrund ist 100% transparent (Deckkraft = 0) und wird erst sichtbar, wenn das Laden und Anlegen der Daten beginnt, damit der Anwender weiß, was gerade vorgeht. In sichtbarem Zustand beträgt die Deckkraft der Leinwand 190. Der Einstellungsbereich für die Deckkraft reicht von 0 bis 255. Ausführliche Informationen dazu bietet die Darstellung der Funktion ColorToARGB() in der „Hilfe“ zu dem Programm.

Um die Leinwand einrichten zu können, schreiben wir die Funktion SetCanvas():

//+------------------------------------------------------------------+
//| Setting canvas                                                   |
//+------------------------------------------------------------------+
void SetCanvas()
  {
//--- If there is no canvas, set it
   if(ObjectFind(0,canvas_name)<0)
     {
      //--- Create the canvas
      canvas.CreateBitmapLabel(0,subwindow_number,canvas_name,0,0,chart_width,subwindow_height,clr_format);
      //--- Make the canvas completely transparent
      canvas.Erase(ColorToARGB(canvas_background,0));
      //--- Redraw the canvas
      canvas.Update();
     }
  }

Außerdem benötigen wir eine Funktion, die überprüft, ob die Größe des Unterfensters verändert wurde. Wenn dem so sein sollte, wird die Größe der Leinwand automatisch an die neue Größe des Unterfensters angepasst. Wir nennen diese Funktion OnSubwindowChange():

//+------------------------------------------------------------------+
//| Checking the subwindow size                                      |
//+------------------------------------------------------------------+
void OnSubwindowChange()
  {
//--- Get subwindow properties
   GetSubwindowGeometry();
//--- If the subwindow size has not changed, exit
   if(!SubwindowSizeChanged())
      return;
//--- If the subwindow height is less than one pixel or if the center has been calculated incorrectly, exit
   if(subwindow_height<1 || subwindow_center_y<1)
      return;
//--- Set the new canvas size
   ResizeCanvas();
//--- Show the last message
   ShowCanvasMessage(msg_last);
  }

Die in dem vorstehenden Code gekennzeichneten Funktionen können unten eingehender betrachtet werden. Achten Sie bitte darauf, welche Überprüfungen vor der Änderung der Größe des Unterfensters vorgenommen werden. Sollten sich Eigenschaften als unrichtig erweisen, stellt die Funktion ihre Arbeit ein.

Hier ist der Code der Funktion SubwindowSizeChanged():

//+------------------------------------------------------------------+
//| Checking if the subwindow has been resized                       |
//+------------------------------------------------------------------+
bool SubwindowSizeChanged()
  {
//--- If the subwindow size has not changed, exit
   if(last_chart_width==chart_width && last_subwindow_height==subwindow_height)
      return(false);
//--- If the size has changed, save it
   else
     {
      last_chart_width=chart_width;
      last_subwindow_height=subwindow_height;
     }
//---
   return(true);
  }

Und hier der Code der Funktion ResizeCanvas():

//+------------------------------------------------------------------+
//| Resizing canvas                                                  |
//+------------------------------------------------------------------+
void ResizeCanvas()
  {
//--- If the canvas has already been added to the indicator subwindow, set the new size
   if(ObjectFind(0,canvas_name)==subwindow_number)
      canvas.Resize(chart_width,subwindow_height);
  }

Und zu guter Letzt der Code der Funktion ShowCanvasMessage(), die bisher ebenfalls beim Bezug der Bezeichner der Indikatoren verwendet wurde:

//+------------------------------------------------------------------+
//| Displaying message on the canvas                                 |
//+------------------------------------------------------------------+
void ShowCanvasMessage(string message_text)
  {
   GetSubwindowGeometry();
//--- If the canvas has already been added to the indicator subwindow
   if(ObjectFind(0,canvas_name)==subwindow_number)
     {
      //--- If the string passed is not empty and correct coordinates have been obtained, display the message
      if(message_text!="" && subwindow_center_x>0 && subwindow_center_y>0)
        {
         canvas.Erase(ColorToARGB(canvas_background,canvas_opacity));
         canvas.TextOut(subwindow_center_x,subwindow_center_y,message_text,ColorToARGB(clrRed),TA_CENTER|TA_VCENTER);
         canvas.Update();
        }
     }
  }

Entfernt wird Leinwand, indem wir sie vollständig ausblenden. Dazu muss der aktuelle Wert der Deckkraft vor dem Löschen in dem Zyklus unter Aktualisierung der Leinwand bei jeder Iteration schrittweise auf Null herabgesetzt werden.

Es folgt der Code der Funktion DeleteCanvas():

//+------------------------------------------------------------------+
//| Deleting canvas                                                  |
//+------------------------------------------------------------------+
void DeleteCanvas()
  {
//--- Delete the canvas if it exists
   if(ObjectFind(0,canvas_name)>0)
     {
      //--- Before deleting, implement the disappearing effect
      for(int i=canvas_opacity; i>0; i-=5)
        {
         canvas.Erase(ColorToARGB(canvas_background,(uchar)i));
         canvas.Update();
        }
      //--- Delete the graphical resource
      canvas.Destroy();
     }
  }

Als Nächstes betrachten wir alle Funktionen, die zur Prüfung der Bereitschaft der Daten vor ihrer Eintragung in die Indikatorpuffer sowie ihrer Abbildung im Diagramm erforderlich sind. Den Anfang macht die Funktion LoadAndFormData(). In ihr wird die Größe des Datenfeldes mit den Daten des aktuellen Kürzels mit den vorliegenden Daten zu anderen Kürzeln verglichen. Gegebenenfalls wird veranlasst, Daten vom Server herunterzuladen. Zu Studienzwecken ist der Code mit umfangreichen Kommentaren versehen.

//+------------------------------------------------------------------+
//| Loading and generating the necessary/available amount of data    |
//+------------------------------------------------------------------+
void LoadAndFormData()
  {
   int bars_count=100; // Number of loaded bars
//---
   for(int s=0; s<SYMBOLS_COUNT; s++)
     {
      int      attempts          =0;    // Counter of data copying attempts
      int      array_size        =0;    // Array size
      datetime firstdate_server  =NULL; // Time of the first bar on the server
      datetime firstdate_terminal=NULL; // Time of the first bar in the terminal base
      //--- Get the first date by the symbol/time frame in the terminal base
      SeriesInfoInteger(symbol_names[s],Period(),SERIES_FIRSTDATE,firstdate_terminal);
      //--- Get the first date of the symbol/time frame on the server
      SeriesInfoInteger(symbol_names[s],Period(),SERIES_SERVER_FIRSTDATE,firstdate_server);
      //--- Print the message
      msg_last=msg_load_data="Loading and generating data: "+
               symbol_names[s]+"("+(string)(s+1)+"/"+(string)SYMBOLS_COUNT+") ... ";
      ShowCanvasMessage(msg_load_data);
      //--- Load/generate data.
      //    If the array size is smaller than the maximum number of bars in the terminal, and if
      //    the number of bars between the first date of the series in the terminal and the first date of the series on the server is more than specified
      while(array_size<OC_rates_total && 
            firstdate_terminal-firstdate_server>PeriodSeconds()*bars_count)
        {
         datetime copied_time[];
         //--- Get the first date by the symbol/time frame in the terminal base
         SeriesInfoInteger(symbol_names[s],Period(),SERIES_FIRSTDATE,firstdate_terminal);
         //--- Load/copy the specified number of bars
         if(CopyTime(symbol_names[s],Period(),0,array_size+bars_count,copied_time)!=-1)
           {
            //--- If the time of the first bar in the array, excluding the number of the bars being loaded, is earlier 
            //    than the time of the first bar in the chart, terminate the loop
            if(copied_time[0]-PeriodSeconds()*bars_count<OC_time[0])
               break;
            //--- If the array size hasn't increased, increase the counter
            if(ArraySize(copied_time)==array_size)
               attempts++;
            //--- Otherwise get the current size of the array
            else
               array_size=ArraySize(copied_time);
            //--- If the array size hasn't increased over 100 attempts, terminate the loop
            if(attempts==100)
              {
               attempts=0;
               break;
              }
           }
         //--- Check the subwindow size once every 2000 bars 
         //    and if the size has changed, adjust the canvas size to it
         if(!(array_size%2000))
            OnSubwindowChange();
        }
     }
  }

Nachdem der Versuch unternommen wurde, die erforderliche Datenmenge herunterzuladen, wird das Vorhandensein der Indikatorbezeichner erneut geprüft. Dazu dient die oben bereits besprochenen Funktion GetIndicatorHandles().

Nach der Überprüfung der Bezeichner prüft das Programm mithilfe der Funktion CheckAvailableData() die Verfügbarkeit der Daten zu den angegebenen Kürzeln sowie die Indikatorwerte zu jedem Kürzel. Unten besteht Gelegenheit, sich genauer anzusehen, wie das geschieht:

//+------------------------------------------------------------------+
//| Checking the amount of available data for all symbols            |
//+------------------------------------------------------------------+
bool CheckAvailableData()
  {
   for(int s=0; s<SYMBOLS_COUNT; s++)
     {
      //--- If this symbol is available
      if(symbol_names[s]!=empty_symbol)
        {
         double   data[]; // Array for checking the amount of indicator data
         datetime time[]; // Array for checking the number of bars
         int      calculated_values =0;    // Amount of indicator data
         int      available_bars    =0;    // Number of bars of the current period
         datetime firstdate_terminal=NULL; // First date of the current time frame data available in the terminal
         //--- Get the number of calculated values of the indicator
         calculated_values=BarsCalculated(symbol_handles[s]);
         //--- Get the first date of the current time frame data in the terminal
         firstdate_terminal=(datetime)SeriesInfoInteger(symbol_names[s],Period(),SERIES_TERMINAL_FIRSTDATE);
         //--- Get the number of available bars from the date specified
         available_bars=Bars(symbol_names[s],Period(),firstdate_terminal,TimeCurrent());
         //--- Check the readiness of bar data: 5 attempts to get values
         for(int i=0; i<5; i++)
           {
            //--- Copy the specified amount of data
            if(CopyTime(symbol_names[s],Period(),0,available_bars,time)!=-1)
              {
               //--- If the required amount has been copied, terminate the loop
               if(ArraySize(time)>=available_bars)
                  break;
              }
           }
         //--- Check the readiness of indicator data: 5 attempts to get values
         for(int i=0; i<5; i++)
           {
            //--- Copy the specified amount of data
            if(CopyBuffer(symbol_handles[s],0,0,calculated_values,data)!=-1)
              {
               //--- If the required amount has been copied, terminate the loop
               if(ArraySize(data)>=calculated_values)
                  break;
              }
           }
         //--- If the amount of data copied is not sufficient, one more attempt is required
         if(ArraySize(time)<available_bars || ArraySize(data)<calculated_values)
           {
            msg_last=msg_prepare_data;
            ShowCanvasMessage(msg_prepare_data);
            OC_prev_calculated=0;
            return(false);
           }
        }
     }
//---
   return(true);
  }

Die Funktion CheckAvailableData() unterbindet die Ausführung weiterer Berechnungen, solange nicht zu allen Kürzeln Daten bereitstehen. Alle Prüffunktionen gehen ähnlich vor.

Die nächste Funktion benötigen wir zur Überwachung des Ereignisses des Ladens eines weiter zurückreichenden Kursverlaufs. Wir nennen sie CheckEventLoadHistory(). Wenn eine größere Datenmenge geladen wird, muss der Indikator vollständig neuberechnet werden. Der Quellcode für diese Funktion folgt sofort:

//+------------------------------------------------------------------+
//| Checking the event of loading a deeper history                   |
//+------------------------------------------------------------------+
bool CheckLoadedHistory()
  {
   bool loaded=false;
//---
   for(int s=0; s<SYMBOLS_COUNT; s++)
     {
      //--- If this symbol is available
      if(symbol_names[s]!=empty_symbol)
        {
         //--- If the series need to be updated
         if(OC_prev_calculated==0)
           {
            //--- Get the first date by the symbol/time frame
            series_first_date[s]=(datetime)SeriesInfoInteger(symbol_names[s],Period(),SERIES_FIRSTDATE);
            //--- If this is the first time (no value is available), then
            if(series_first_date_last[s]==NULL)
               //--- Store the first date by the symbol/time frame for further comparison 
               //    in order to determine if a deeper history has been loaded
               series_first_date_last[s]=series_first_date[s];
           }
         else
           {
            //--- Get the first date by the symbol/time frame
            series_first_date[s]=(datetime)SeriesInfoInteger(symbol_names[s],Period(),SERIES_FIRSTDATE);
            //--- If the dates are different, i.e. the date in the memory is later than the one we have just obtained,
            //     this means that a deeper history has been loaded
            if(series_first_date_last[s]>series_first_date[s])
              {
               //--- Print the relevant message to the log
               Print("(",symbol_names[s],",",TimeframeToString(Period()),
                     ") > A deeper history has been loaded/generated: ",
                     series_first_date_last[s]," > ",series_first_date[s]);
               //--- Store the date
               series_first_date_last[s]=series_first_date[s];
               loaded=true;
              }
           }
        }
     }
//--- If a deeper history has been loaded/generated, then
//    send the command to refresh the plotting series of the indicator
   if(loaded)
      return(false);
//---
   return(true);
  }

Jetzt schreiben wir eine Funktion zur Überprüfung der Synchronizität der Daten auf dem Ausgabegerät und dem Server. Diese Überprüfung erfolgt nur bei bestehender Verbindung mit dem Server. Es folgt der Code der Funktion CheckSymbolIsSynchronized():

//+------------------------------------------------------------------+
//| Checking synchronization by symbol/time frame                    |
//+------------------------------------------------------------------+
bool CheckSymbolIsSynchronized()
  {
//--- If the connection to the server is established, check the data synchronization
   if(TerminalInfoInteger(TERMINAL_CONNECTED))
     {
      for(int s=0; s<SYMBOLS_COUNT; s++)
        {
         //--- If the symbol is available
         if(symbol_names[s]!=empty_symbol)
           {
            //--- If the data are not synchronized, print the relevant message and try again
            if(!SeriesInfoInteger(symbol_names[s],Period(),SERIES_SYNCHRONIZED))
              {
               msg_last=msg_not_synchronized;
               ShowCanvasMessage(msg_not_synchronized);
               return(false);
              }
           }
        }
     }
//---
   return(true);
  }

Die Hilfsfunktion zur Umwandlung des Zeitraums in eine Zeichenfolge (Zeile) entnehmen wir vorhergehenden Artikeln aus der Reihe „Das MQL5-Kochbuch“:

//+------------------------------------------------------------------+
//| Converting time frame to a string                                |
//+------------------------------------------------------------------+
string TimeframeToString(ENUM_TIMEFRAMES timeframe)
  {
   string str="";
//--- If the value passed is incorrect, take the current chart time frame
   if(timeframe==WRONG_VALUE || timeframe== NULL)
      timeframe= Period();
   switch(timeframe)
     {
      case PERIOD_M1  : str="M1";  break;
      case PERIOD_M2  : str="M2";  break;
      case PERIOD_M3  : str="M3";  break;
      case PERIOD_M4  : str="M4";  break;
      case PERIOD_M5  : str="M5";  break;
      case PERIOD_M6  : str="M6";  break;
      case PERIOD_M10 : str="M10"; break;
      case PERIOD_M12 : str="M12"; break;
      case PERIOD_M15 : str="M15"; break;
      case PERIOD_M20 : str="M20"; break;
      case PERIOD_M30 : str="M30"; break;
      case PERIOD_H1  : str="H1";  break;
      case PERIOD_H2  : str="H2";  break;
      case PERIOD_H3  : str="H3";  break;
      case PERIOD_H4  : str="H4";  break;
      case PERIOD_H6  : str="H6";  break;
      case PERIOD_H8  : str="H8";  break;
      case PERIOD_H12 : str="H12"; break;
      case PERIOD_D1  : str="D1";  break;
      case PERIOD_W1  : str="W1";  break;
      case PERIOD_MN1 : str="MN1"; break;
     }
//---
   return(str);
  }

Und schließlich muss der erste echte Balken für jedes Kürzel ermittelt und vermerkt werden, indem wir ihn im Diagramm mit einer senkrechten Linie kennzeichnen. Dazu schreiben wir die Funktion DetermineFirstTrueBar() sowie die Hilfsfunktion GetFirstTrueBarTime(), die den Zeitpunkt des Auftretens des ersten echten Balkens ausgibt.

//+-----------------------------------------------------------------------+
//| Determining the time of the first true bar for the purpose of drawing |
//+-----------------------------------------------------------------------+
bool DetermineFirstTrueBar()
  {
   for(int s=0; s<SYMBOLS_COUNT; s++)
     {
      datetime time[];           // Bar time array
      int      available_bars=0; // Number of bars
      //--- If this symbol is not available, move to the next one
      if(symbol_names[s]==empty_symbol)
         continue;
      //--- Get the total number of bars for the symbol
      available_bars=Bars(symbol_names[s],Period());
      //--- Copy the bar time array. If this action failed, try again.
      if(CopyTime(symbol_names[s],Period(),0,available_bars,time)<available_bars)
         return(false);
      //--- Get the time of the first true bar corresponding to the current time frame
      limit_time[s]=GetFirstTrueBarTime(time);
      //--- Place a vertical line on the true bar
      CreateVerticalLine(0,0,limit_time[s],prefix+symbol_names[s]+": begin time series",
                          2,STYLE_SOLID,line_colors[s],false,TimeToString(limit_time[s]),"\n");
     }
//---
   return(true);
  }
//+-----------------------------------------------------------------------+
//| Returning the time of the first true bar of the current time frame    |
//+-----------------------------------------------------------------------+
datetime GetFirstTrueBarTime(datetime &time[])
  {
   datetime true_period =NULL; // Time of the first true bar
   int      array_size  =0;    // Array size
//--- Get the array size
   array_size=ArraySize(time);
   ArraySetAsSeries(time,false);
//--- Check each bar one by one
   for(int i=1; i<array_size; i++)
     {
      //--- If the bar corresponds to the current time frame
      if(time[i]-time[i-1]==PeriodSeconds())
        {
         //--- Save it and terminate the loop
         true_period=time[i];
         break;
        }
     }
//--- Return the time of the first true bar
   return(true_period);
  }

Den Zeitpunkt des Auftretens des ersten echten Balkens kennzeichnen wir im Diagramm mithilfe der Funktion CreateVerticalLine() durch eine senkrechte Linie:

//+------------------------------------------------------------------+
//| Creating a vertical line at the specified time point             |
//+------------------------------------------------------------------+
void CreateVerticalLine(long            chart_id,           // chart id
                        int             window_number,      // window number
                        datetime        time,               // time
                        string          object_name,        // object name
                        int             line_width,         // line width
                        ENUM_LINE_STYLE line_style,         // line style
                        color           line_color,         // line color
                        bool            selectable,         // cannot select the object if FALSE
                        string          description_text,   // text of the description
                        string          tooltip)            // no tooltip if "\n"
  {
//--- If the object has been created successfully
   if(ObjectCreate(chart_id,object_name,OBJ_VLINE,window_number,time,0))
     {
      //--- set its properties
      ObjectSetInteger(chart_id,object_name,OBJPROP_TIME,time);
      ObjectSetInteger(chart_id,object_name,OBJPROP_SELECTABLE,selectable);
      ObjectSetInteger(chart_id,object_name,OBJPROP_STYLE,line_style);
      ObjectSetInteger(chart_id,object_name,OBJPROP_WIDTH,line_width);
      ObjectSetInteger(chart_id,object_name,OBJPROP_COLOR,line_color);
      ObjectSetString(chart_id,object_name,OBJPROP_TEXT,description_text);
      ObjectSetString(chart_id,object_name,OBJPROP_TOOLTIP,tooltip);
     }
  }

Damit sind die Prüffunktionen fertig. Im Ergebnis sieht der Teil des Codes der Funktion OnCalculate(), wenn die Variable prev_calculated gleich „0“ ist, jetzt folgendermaßen aus:

//--- If this is the first calculation or if a deeper history has been loaded or gaps in the history have been filled
   if(prev_calculated==0)
     {
      //--- Zero out arrays for data preparation
      ZeroCalculatedArrays();
      //--- Zero out indicator buffers
      ZeroIndicatorBuffers();
      //--- Get subwindow properties
      GetSubwindowGeometry();
      //--- Add the canvas
      SetCanvas();
      //--- Load and generate the necessary/available amount of data
      LoadAndFormData();
      //--- If there is an invalid handle, try to get it again
      if(!GetIndicatorHandles())
         return(RESET);
      //--- Check the amount of data available for all symbols
      if(!CheckAvailableData())
         return(RESET);
      //--- Check if a deeper history has been loaded
      if(!CheckLoadedHistory())
         return(RESET);
      //--- Check synchronization by symbol/time frame at the current moment
      if(!CheckSymbolIsSynchronized())
         return(RESET);
      //--- For each symbol, determine the bar from which we should start drawing
      if(!DetermineFirstTrueBar())
         return(RESET);
      //--- If you reached this point, it means that OnCalculate() will return non-zero value and this needs to be saved
      OC_prev_calculated=rates_total;
     }

Jedes Mal, wenn eine Überprüfung nicht erfolgreich endet, wird zum vorhergehenden Schritt zurückgegangen, um bei der nächsten Kursänderung oder dem nächsten Zeitgeberereignis einen weiterenn Versuch zu unternehmen. Im Zeitgeber muss auch die Überprüfung auf das Herunterladen eines weiter zurückreichenden Kursverlaufs außerhalb der Funktion OnCalculate() eingerichtet werden:

//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer()
  {
//--- If a deeper history has been loaded
   if(!CheckLoadedHistory())
      OC_prev_calculated=0;
//--- If for some reason calculations have not been completed or
//    a deeper history has been loaded or
//    gaps in the history have been filled, 
//    then make another attempt without waiting for the new tick
   if(OC_prev_calculated==0)
     {
      OnCalculate(OC_rates_total,OC_prev_calculated,
                  OC_time,OC_open,OC_high,OC_low,OC_close,
                  OC_tick_volume,OC_volume,OC_spread);
     }
  }

Bleiben noch die beiden Hauptarbeitsgänge (Zyklen) zu schreiben, um sie in die Funktion OnCalculate() einzufügen:

Der Code für den ersten Arbeitsgang sehen wir unten:

//--- Prepare data for drawing
   for(int s=0; s<SYMBOLS_COUNT; s++)
     {
      //--- If the symbol is available
      if(symbol_names[s]!=empty_symbol)
        {
         double percent=0.0; // For the purpose of calculating the progress percentage
         msg_last=msg_sync_update="Preparing data ("+IntegerToString(rates_total)+" bars) : "+
                      symbol_names[s]+"("+(string)(s+1)+"/"+(string)(SYMBOLS_COUNT)+") - 00% ... ";
         //--- Print the message
         ShowCanvasMessage(msg_sync_update);
         //--- Control every value of the array
         for(int i=limit; i<rates_total; i++)
           {
            PrepareData(i,s,time);
            //--- Refresh the message once every 1000 bars
            if(i%1000==0)
              {
               //--- Progress percentage
               ProgressPercentage(i,s,percent);
               //--- Print the message
               ShowCanvasMessage(msg_sync_update);
              }
            //--- Check the subwindow size once every 2000 bars
            //    and if the size has changed, adjust the canvas size to it
            if(i%2000==0)
               OnSubwindowChange();
           }
        }
     }

Die Hauptfunktion zum Kopieren und Speichern der Werte, PrepareData(), ist in dem Code oben besonders hervorgehoben. Es gibt noch eine weitere neue Funktion, die wir bisher noch nicht betrachtet haben, sie heißt ProgressPercentage() und berechnet den Stand der Ausführung des aktuellen Vorgangs in Prozent, damit der Anwender weiß, wie lange es dauern wird.

Es folgt der Code der Funktion PrepareData():

//+------------------------------------------------------------------+
//| Preparing data before drawing                                    |
//+------------------------------------------------------------------+
void PrepareData(int bar_index,int symbol_number,datetime const &time[])
  {
   int attempts=100; // Number of copying attempts
//--- Time of the bar of the specified symbol and time frame
   datetime symbol_time[];
//--- Array for copying indicator values
   double atr_values[];
//--- If within the area of the current time frame bars
   if(time[bar_index]>=limit_time[symbol_number])
     {
      //--- Copy the time
      for(int i=0; i<attempts; i++)
        {
         if(CopyTime(symbol_names[symbol_number],0,time[bar_index],1,symbol_time)==1)
           {
            tmp_symbol_time[symbol_number].time[bar_index]=symbol_time[0];
            break;
           }
        }
      //--- Copy the indicator value
      for(int i=0; i<attempts; i++)
        {
         if(CopyBuffer(symbol_handles[symbol_number],0,time[bar_index],1,atr_values)==1)
           {
            tmp_atr_values[symbol_number].value[bar_index]=atr_values[0];
            break;
           }
        }
     }
//--- If outside the area of the current time frame bars, set an empty value
   else
      tmp_atr_values[symbol_number].value[bar_index]=EMPTY_VALUE;
  }

und hier der Code der Funktion ProgressPercentage():

//+------------------------------------------------------------------+
//| Calculating progress percentage                                  |
//+------------------------------------------------------------------+
void ProgressPercentage(int bar_index,int symbol_number,double &percent)
  {
   string message_text="";
   percent=(double(bar_index)/OC_rates_total)*100;
//---
   if(percent<=9.99)
      message_text="0"+DoubleToString(percent,0);
   else if(percent<99)
      message_text=DoubleToString(percent,0);
   else
      message_text="100";
//---
   msg_last=msg_sync_update="Preparing data ("+(string)OC_rates_total+" bars) : "+
            symbol_names[symbol_number]+
            "("+(string)(symbol_number+1)+"/"+(string)SYMBOLS_COUNT+") - "+message_text+"% ... ";
  }

Im zweiten Hauptarbeitsgang der Funktion OnCalculate() werden die Zwischenspeicher des Indikators, die Indikatorpuffer, gefüllt:

//--- Fill indicator buffers
   for(int s=0; s<SYMBOLS_COUNT; s++)
     {
      //--- If the specified symbol does not exist, zero out the buffer
      if(symbol_names[s]==empty_symbol)
         ArrayInitialize(atr_buffers[s].data,EMPTY_VALUE);
      else
        {
         //--- Generate a message
         msg_last=msg_sync_update="Updating indicator data: "+
                      symbol_names[s]+"("+(string)(s+1)+"/"+(string)SYMBOLS_COUNT+") ... ";
         //--- Print the message
         ShowCanvasMessage(msg_sync_update);
         //--- Fill indicator buffers with values
         for(int i=limit; i<rates_total; i++)
           {
            FillIndicatorBuffers(i,s,time);
            //--- Check the subwindow size once every 2000 bars
            //    and if the size has changed, adjust the canvas size to it
            if(i%2000==0)
               OnSubwindowChange();
           }
        }
     }

Die hervorgehobene Zeile in dem Code oben enthält die Funktion FillIndicatorBuffers(). In ihr erfolgen die abschließenden Operationen, bevor die grafischen Reihen des Indikators in dem Diagramm abgebildet werden:

//+------------------------------------------------------------------+
//| Filling indicator buffers                                        |
//+------------------------------------------------------------------+
void FillIndicatorBuffers(int bar_index,int symbol_number,datetime const &time[])
  {
//--- For the purpose of checking the obtained indicator value
   bool check_value=false;
//--- Counter of the current time frame bars
   static int bars_count=0;
//--- Zero out the counter of the current time frame bars at the beginning of the symbol's time series
   if(bar_index==0)
      bars_count=0;
//--- If within the area of current time frame bars and the counter is smaller 
//    than the specified indicator period, increase the counter
   if(bars_count<IndicatorPeriod && time[bar_index]>=limit_time[symbol_number])
      bars_count++;
//--- If within the indicator area and the time of the current symbol coincides with the time of the specified symbol
   if(bars_count>=IndicatorPeriod && 
      time[bar_index]==tmp_symbol_time[symbol_number].time[bar_index])
     {
      //--- If the value obtained is not empty
      if(tmp_atr_values[symbol_number].value[bar_index]!=EMPTY_VALUE)
        {
         check_value=true;
         atr_buffers[symbol_number].data[bar_index]=tmp_atr_values[symbol_number].value[bar_index];
        }
     }
//--- Set an empty value in case of failure to set a higher value
   if(!check_value)
      atr_buffers[symbol_number].data[bar_index]=EMPTY_VALUE;
  }

Am Ende der Funktion OnCalculate() müssen die Leinwand gelöscht, die Grenzen (Ebenen) festgelegt, die variablen Mitteilungen auf Null gesetzt und das Diagramm aktualisiert werden. Den Schlussakkord bildet die Ausgabe der Größe des Datenfeldes rates_total, wonach bei jeder weiteren Kursänderung oder bei jedem Zeitgeberereignis in der Funktion OnCalculate() nur der letzte Wert neu berechnet wird.

Diese Codezeilen müssen nach dem zweiten Hauptarbeitsgang vor dem von der Funktion ausgegebenen Wert eingefügt werden:

//--- Delete the canvas
   DeleteCanvas();
//--- Set indicator levels
   SetIndicatorLevels();
//--- Zero out variables
   msg_last="";
   msg_sync_update="";
//--- Refresh the chart
   ChartRedraw();

Der Code der Funktion SetIndicatorLevels() zur Festlegung der waagerechten Grenzen (Ebenen) sieht aus wie folgt:

//+------------------------------------------------------------------------+
//| Setting indicator levels                                               |
//+------------------------------------------------------------------------+
void SetIndicatorLevels()
  {
//--- Get the indicator subwindow number
   subwindow_number=ChartWindowFind(0,subwindow_shortname);
//--- Set levels
   for(int i=0; i<LEVELS_COUNT; i++)
      CreateHorizontalLine(0,subwindow_number,
                            prefix+"level_0"+(string)(i+1)+"",
                            CorrectValueBySymbolDigits(indicator_levels[i]*_Point),
                            1,STYLE_DOT,clrLightSteelBlue,false,false,false,"\n");
  }
//+------------------------------------------------------------------------+
//| Adjusting the value based on the number of digits in the price (double)|
//+------------------------------------------------------------------------+
double CorrectValueBySymbolDigits(double value)
  {
   return(_Digits==3 || _Digits==5) ? value*=10 : value;
  }

Der Code der Funktion CreateHorizontalLine() zur Festlegung einer senkrechten Linie mit vorgegebenen Eigenschaften:

//+------------------------------------------------------------------+
//| Creating a horizontal line at the price level specified          |
//+------------------------------------------------------------------+
void CreateHorizontalLine(long            chart_id,      // chart id
                          int             window_number, // window number
                          string          object_name,   // object name
                          double          price,         // price level
                          int             line_width,    // line width
                          ENUM_LINE_STYLE line_style,    // line style
                          color           line_color,    // line color
                          bool            selectable,    // cannot select the object if FALSE
                          bool            selected,      // line is selected
                          bool            back,          // background position
                          string          tooltip)       // no tooltip if "\n"
  {
//--- If the object has been created successfully
   if(ObjectCreate(chart_id,object_name,OBJ_HLINE,window_number,0,price))
     {
      //--- set its properties
      ObjectSetInteger(chart_id,object_name,OBJPROP_SELECTABLE,selectable);
      ObjectSetInteger(chart_id,object_name,OBJPROP_SELECTED,selected);
      ObjectSetInteger(chart_id,object_name,OBJPROP_BACK,back);
      ObjectSetInteger(chart_id,object_name,OBJPROP_STYLE,line_style);
      ObjectSetInteger(chart_id,object_name,OBJPROP_WIDTH,line_width);
      ObjectSetInteger(chart_id,object_name,OBJPROP_COLOR,line_color);
      ObjectSetString(chart_id,object_name,OBJPROP_TOOLTIP,tooltip);
     }
  }

Die Funktionen zum Löschen grafischer Objekte:

//+------------------------------------------------------------------+
//| Deleting levels                                                  |
//+------------------------------------------------------------------+
void DeleteLevels()
  {
   for(int i=0; i<LEVELS_COUNT; i++)
      DeleteObjectByName(prefix+"level_0"+(string)(i+1)+"");
  }
//+------------------------------------------------------------------+
//| Deleting vertical lines of the beginning of the series           |
//+------------------------------------------------------------------+
void DeleteVerticalLines()
  {
   for(int s=0; s<SYMBOLS_COUNT; s++)
      DeleteObjectByName(prefix+symbol_names[s]+": begin time series");
  }
//+------------------------------------------------------------------+
//| Deleting the object by name                                      |
//+------------------------------------------------------------------+
void DeleteObjectByName(string object_name)
  {
//--- If such object exists
   if(ObjectFind(0,object_name)>=0)
     {
      //--- If an error occurred when deleting, print the relevant message
      if(!ObjectDelete(0,object_name))
         Print("Error ("+IntegerToString(GetLastError())+") when deleting the object!");
     }
  }

Die Funktion OnDeinit() muss um folgenden Code erweitert werden:

Jetzt ist alles fertig und kann eingehend geprüft werden. Die Höchstzahl der Balken in einem Fenster kann in den Einstellungen der Programminstanz auf dem Ausgabegerät (Terminal) unter der RegisterkarteDiagramme (Charts) festgelegt werden. Von der Anzahl der Balken im Fenster hängt ab, wie schnell der Indikator einsatzbereit ist.

Abb. 1. Festlegung der Höchstzahl an Balken in den Einstellungen der Programminstanz auf dem Ausgabegerät

Abb. 1. Festlegung der Höchstzahl an Balken in den Einstellungen der Programminstanz auf dem Ausgabegerät

Nach der Festlegung der Höchstzahl an Balken muss das Ausgabegerät neugestartet werden, damit der Indikator die Änderungen wahrnimmt, ansonsten werden die vorhergehenden Werte verwendet.

Beim Laden des Indikators in das Diagramm wird in Prozent angegeben, wie die abwechselnde Aufbereitung der Daten für alle Kürzel fortschreitet:

Abb. 2. Fortschrittsmeldung auf der Leinwand während der Datenaufbereitung

Abb. 2. Fortschrittsmeldung auf der Leinwand während der Datenaufbereitung

Es folgt eine Bildschirmaufnahme von dem Indikator für einen Zeitraum mit Abtastintervallen von 20 Minuten:

Abb. 3. Mehrwährungsfähiger ATR-Indikator für einen Zeitraum mit Abtastintervallen von 20 Minuten

Abb. 3. Mehrwährungsfähiger ATR-Indikator für einen Zeitraum mit Abtastintervallen von 20 Minuten

Der Beginn der „echten“ Balken wird im Diagramm durch senkrechte Linien gekennzeichnet. In der Bildschirmaufnahme unten wird deutlich, dass die „echten“ Balken für das Währungspaar NZDUSD (gelbe Linie) ab 2000 beginnen (vom Server MetaQuotes-Demo), für alle übrigen jedoch ab Anfang 1999, weswegen nur eine Linie zu sehen ist (alle an ein und demselben Datum). Ebenfalls sichtbar wird, dass die Abgrenzungen zwischen den Zeiträumen vor 1999 ein geringeres Intervall aufweisen, und wenn wir die Zeitpunkte des Auftretens der Balken analysieren, können wir uns davon überzeugen, dass es sich um Tagesbalken handelt.

Abb. 4. Der durch eine senkrechte Linie gekennzeichnete Anfang der „echten“ Balken für jedes Kürzel

 

Fazit

Damit kann dieser Beitrag beendet werden. Der beschriebene Quellcode befindet sich im Anhang und kann heruntergeladen werden. In einem weiteren Beitrag, werden wir versuchen, ein Handelssystem zur Analyse von Kursschwankungen umzusetzen, um zu sehen, was dabei herauskommt.