English Русский 中文 Español Deutsch 日本語 Português 한국어 Italiano Türkçe
Le MQL5 Cookbook : Développement d'un indicateur de volatilité multi-symboles dans MQL5

Le MQL5 Cookbook : Développement d'un indicateur de volatilité multi-symboles dans MQL5

MetaTrader 5Exemples | 13 janvier 2022, 08:58
272 0
Anatoli Kazharski
Anatoli Kazharski

Introduction

Dans cet article, nous examinerons le développement d'un indicateur de volatilité multi-symboles. Le développement d'indicateurs multi-symboles peut présenter quelques difficultés pour les développeurs MQL5 novices que cet article aide à clarifier. Les problèmes majeurs qui se posent au cours du développement d'un indicateur multi-symboles concernent la synchronisation des données d'autres symboles par rapport au symbole courant, le manque de certaines données d'indicateur et l'identification du début des "vraies" barres d'une trame de temps donnée. Toutes ces questions seront examinées de près dans l'article.

Nous obtiendrons les valeurs de l'indicateur Average True Range (ATR) déjà calculées pour chaque symbole en fonction de la poignée. À des fins d'illustration, il y aura six symboles dont les noms peuvent être définis dans les paramètres externes de l'indicateur. Les noms saisis seront vérifiés pour être corrects. Si un certain symbole spécifié dans les paramètres n'est pas disponible dans la liste générale, aucun calcul ne sera effectué pour celui-ci. Tous les symboles disponibles seront ajoutés à la fenêtre Market Watch, sauf s'ils y sont déjà disponibles.

Dans l'article précédent intitulé "MQL5 Cookbook : Commandes de la sous-fenêtre de l'indicateur - Barre de défilement" nous avons déjà parlé du canevas sur lequel vous pouvez imprimer du texte et même dessiner. Cette fois, nous n'allons pas dessiner sur le canevas mais nous l'utiliserons pour afficher des messages sur les processus du programme en cours afin de permettre à l'utilisateur de savoir ce qui se passe à un moment donné.

 

Développement de l'indicateur

Commençons le développement du programme. À l'aide de MQL5 Wizard, créez un modèle d'indicateur personnalisé. Après quelques modifications, vous devriez obtenir le code source comme indiqué ci-dessous :

//+------------------------------------------------------------------+
//|                                               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()
  {
  }
//+------------------------------------------------------------------+

Pour mettre en œuvre notre idée, nous allons encore remplir ce modèle avec tout ce qui est requis. La nécessité d'une minuterie sera expliquée plus loin dans l'article. Ajoutons des constantes au début, juste après les propriétés spécifiques de l'indicateur :

//--- 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

La constante LEVELS_COUNT contient la valeur du nombre de niveaux représentés par des objets graphiques de type "Horizontal Line" (OBJ_HLINE). Les valeurs de ces niveaux peuvent être spécifiées dans les paramètres externes de l'indicateur.

Incluons dans le projet un fichier avec la classe pour travailler avec les graphiques custom :

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

Dans les paramètres externes, nous allons spécifier la période de calcul de la moyenne de iATR, les noms des symboles dont la volatilité doit être affichée et les valeurs du niveau horizontal. Les symboles sont numérotés à partir de 2 car le premier symbole est considéré comme celui auquel l'indicateur est rattaché.

//--- 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

Plus loin dans le code, nous devrions créer toutes les variables globales et les tableaux avec lesquels travailler plus tard. Tous sont fournis dans le code ci-dessous avec des commentaires détaillés :

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

Lors du chargement de l'indicateur dans le graphique, la OnInit() effectuera les actions suivantes :

  • la définition des propriétés de l'indicateur ;
  • la détermination des tableaux pour dessiner des séries de tracés ;
  • l’initialisation des tableaux ;
  • l'ajout de symboles spécifiés dans les paramètres externes à la fenêtre Market Watch ;
  • la vérification de l'exactitude des paramètres et faire la première tentative pour obtenir des poignées d'indicateur.

Toutes ces actions seront traitées de manière plus pratique si elles sont organisées en fonctions distinctes. En conséquence, les OnInit() deviendra très facile à comprendre comme indiqué ci-dessous :

//+------------------------------------------------------------------+
//| 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);
  }

Examinons de plus près les fonctions personnalisées utilisées dans le code ci-dessus. Dans la fonction CheckInputParameters(), nous vérifions l'exactitude des paramètres externes. Dans notre cas, nous ne vérifions qu'un seul paramètre - la période de l'indicateur ATR. J'ai défini la valeur de restriction de 500. C'est-à-dire que si vous définissez une valeur de période supérieure à la valeur spécifiée, l'indicateur cessera de fonctionner et imprimera le message sur la raison de l'arrêt du programme dans le journal et le commentaire du graphique. Le code de la fonction CheckInputParameters() est fourni ci-dessous.

//+------------------------------------------------------------------+
//| 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);
  }
Soit dit en passant, pour accéder rapidement à une certaine définition de fonction, vous devez placer le curseur sur le nom de la fonction et appuyer sur Alt+G ou cliquer avec le bouton droit sur la fonction pour appeler le menu contextuel et sélectionner "Go to Definition". Si la fonction est définie dans un autre fichier, ce fichier s'ouvrira dans l'éditeur. Vous pouvez également ouvrir les bibliothèques et les classes d'inclusion. C'est très pratique.

Ensuite, nous passons à trois fonctions d'initialisation de tableau : InitArrays(), InitSymbolNames() and InitLevels(). Leurs codes sources respectifs sont fournis ci-dessous :

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

Dans la fonction InitSymbolNames(), nous utilisons une autre fonction personnalisée - AddSymbolToMarketWatch(). Il reçoit le nom du symbole et si ce symbole est disponible dans la liste générale, il sera ajouté à la fenêtre Market Watch et la fonction renverra la chaîne avec le nom du symbole. Si ce symbole n'est pas disponible, la fonction renverra la chaîne "VIDE" et aucune action ne sera effectuée pour cet élément dans le tableau de symboles lors de l'exécution de vérifications dans d'autres fonctions.

//+------------------------------------------------------------------+
//| 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() est une autre fonction appelée à l'initialisation de l'indicateur. Il tente d'obtenir les poignées d'indicateur ATR pour chaque symbole spécifié. Si la poignée n'a pas été obtenue pour un symbole, la fonction renverra false mais cela ne sera en aucun cas traité dans les OnInit() car la disponibilité de la poignée sera vérifiée dans d'autres parties du programme.

//+------------------------------------------------------------------+
//| 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);
  }

La fonction ShowCanvasMessage() sera revue un peu plus tard avec d'autres fonctions pour travailler avec le canevas.

Les propriétés de l'indicateur sont définies dans la fonction SetIndicatorProperties(). Étant donné que les propriétés de chaque série de tracés sont similaires, il est plus pratique de les définir à l'aide de boucles :

//+------------------------------------------------------------------+
//| 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);
  }

Après une initialisation réussie du programme, nous devons effectuer le premier appel des OnCalculate(). La valeur de la variable prev_calculated est zéro au premier appel de fonction. Il est également remis à zéro par le terminal lorsqu'un historique plus profond est en cours de chargement ou que des lacunes dans l'historique sont comblées. Dans de tels cas, les tampons des indicateurs sont entièrement recalculés. Si cette valeur de paramètre est non nulle, c'est-à-dire le résultat précédemment renvoyé par la même fonction, qui est la taille des séries temporelles d'entrée, il suffit de ne mettre à jour que les dernières valeurs des tampons.

Il se peut que vous ne parveniez pas toujours à faire tous les calculs correctement du premier coup. Dans ce cas, pour revenir, nous utiliserons la constante RESET qui contient la valeur zéro. Au prochain appel des OnCalculate() (par exemple au prochain tick), le paramètre prev_calculated contiendra une valeur nulle, ce qui signifie que nous devrons faire une autre tentative pour effectuer tous les calculs nécessaires avant d'afficher les séries de tracés de l'indicateur dans le graphique.

Mais le graphique restera vide lorsque le marché sera fermé et qu'il n'y aura pas de nouveaux ticks ou de calculs infructueux consécutifs. Dans ce cas, vous pouvez essayer un moyen simple de donner une commande pour faire une autre tentative - en modifiant manuellement la trame de temps du graphique. Mais nous utiliserons une approche différente. C'est pourquoi au tout début, nous avons ajouté la minuterie, les OnTimer(), à notre modèle de programme et défini l'intervalle de temps de 1 seconde dans les OnInit().

Chaque seconde, le minuteur vérifiera si la OnCalculate() a renvoyé zéro. À cette fin, nous écrirons une fonction CopyDataOnCalculate() qui copiera tous les paramètres des OnCalculate() dans les variables globales avec les noms et les tableaux correspondants avec le préfixe OC_.

//+------------------------------------------------------------------+
//| 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);
  }

Cette fonction doit être appelée au tout début des OnCalculateOnCalculate(). De plus, au tout début, nous devrions également ajouter une autre fonction personnalisée, ResizeCalculatedArrays(), qui définira la taille des tableaux pour la préparation des données avant de les placer dans des tampons d'indicateurs. La taille de ces tableaux doit être égale à la taille de la série temporelle d'entrée.

//+------------------------------------------------------------------+
//| 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);
     }
  }

De plus, nous allons créer une fonction ZeroCalculatedArrays() qui initialise les tableaux pour la préparation des données à zéro avant de les afficher dans le graphique.

//+------------------------------------------------------------------+
//| 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);
     }
  }

La même fonction sera requise pour remettre à zéro les tampons d'indicateurs préliminaires. Appelons-le ZeroIndicatorBuffers().

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

Le code actuel des OnCalculate() sera comme indiqué ci-dessous. J'ai également fourni des commentaires pour les opérations principales à remplir ultérieurement (commentaires et points en dessous).

//+------------------------------------------------------------------+
//| 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);
  }

Actuellement, les OnTimer() est le suivant :

//+------------------------------------------------------------------+
//| 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);
     }
  }

Considérons maintenant d'autres fonctions qui seront utilisées lorsque la variable prev_calculated est égale à zéro. Ces fonctions vont :

  • charger et générer la quantité de données nécessaire (barres);
  • vérifier la disponibilité de toutes les poignées ;
  • vérifier l'état de préparation de la quantité de données requise ;
  • synchroniser les données avec le serveur ;
  • déterminer les barres à partir desquelles les séries de tracés seront dessinées.

De plus, nous identifierons la première barre 'true' pour chaque symbole. Ce terme concis a été inventé pour le rendre plus pratique plus tard. Voici ce que cela signifie. Tous les trames temporelles dans MetaTrader 5 sont construits à partir de données minutes. Mais si, par exemple, les données quotidiennes sur le serveur sont disponibles à partir de 1993, alors que les données minutes ne sont disponibles qu'à partir de 2000, alors si nous sélectionnons, disons, une trame temporelle de graphique horaire, des barres seront construites à partir de la date à laquelle les données minutes deviennent disponibles, c'est-à-dire à partir de l'an 2000. Tout ce qui est antérieur à 2000 sera soit représenté par des données quotidiennes, soit par les données les plus proches de la trame temporelle actuelle. Par conséquent, pour éviter toute confusion, vous ne devez pas afficher les données d'indicateur pour les données qui ne sont pas liées à la trame temporelle actuelle. C'est la raison pour laquelle nous allons identifier la première barre 'true' de la trame temporelle courante et la marquer d'un trait vertical de la même couleur que celle du tampon indicateur du symbole.

L'identification des barres "true" est également importante lors du développement d'Expert Advisors, car si les paramètres sont optimisés pour une certaine trame temporelle, les données d'autres trames temporelle seraient alors inappropriées.

Avant d'exécuter les vérifications ci-dessus, nous allons ajouter le canevas à la sous-fenêtre de l'indicateur. Nous devons donc d'abord écrire toutes les fonctions dont nous aurons besoin pour gérer le canevas. Avant d'ajouter le canevas à la sous-fenêtre, nous devons déterminer sa taille, ainsi que les coordonnées en fonction desquelles les messages texte seront affichés sur le canevas. Pour cela, écrivons une fonction GetSubwindowGeometry() :

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

Lorsque les propriétés de la sous-fenêtre sont obtenues, vous pouvez ajouter le canevas. Son arrière-plan sera 100% transparent (opacité égale à 0), ne devenant visible que lors du chargement et de la génération de données pour informer l'utilisateur de ce qui se passe actuellement. Lorsqu'elle est visible, l'opacité de l'arrière-plan sera égale à 190. Vous pouvez définir la valeur d'opacité entre 0 et 255. Pour plus d'informations, veuillez vous référer à la description de la fonction ColorToARGB() disponible sous Aide .

Pour définir le canevas, écrivons une fonction 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();
     }
  }

Nous aurons également besoin d'une fonction qui vérifie si la sous-fenêtre de l'indicateur a été redimensionnée. Si tel est le cas, la taille du canevas sera automatiquement ajustée à la nouvelle taille de la sous-fenêtre. Appelons cette fonction 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);
  }

Les fonctions mises en évidence dans le code ci-dessus peuvent être explorées ci-dessous. Veuillez noter les types de vérifications qui sont exécutées avant de redimensionner la sous-fenêtre. Si une propriété s'avère incorrecte, la fonction arrête son opération.

Le code de la fonction SubwindowSizeChanged() est le suivant :

//+------------------------------------------------------------------+
//| 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);
  }

Le code de la fonction ResizeCanvas() est le suivant :

//+------------------------------------------------------------------+
//| 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);
  }

Et enfin, vous trouverez ci-dessous le code de la fonction ShowCanvasMessage() que nous avons également utilisé précédemment lors de l'obtention des poignées d'indicateur :

//+------------------------------------------------------------------+
//| 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();
        }
     }
  }

La toile sera supprimée avec effet de disparition. Pour l'implémenter, juste avant de supprimer le canevas, nous devons changer progressivement l'opacité de la valeur actuelle à zéro dans une boucle, tout en actualisant le canevas à chaque itération.

Le code de la fonction DeleteCanvas() est le suivant :

//+------------------------------------------------------------------+
//| 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();
     }
  }

Examinons ensuite les fonctions requises pour vérifier l'état de préparation des données avant de les placer dans des tampons d'indicateurs et de les afficher sur le graphique. Commençons par la fonction LoadAndFormData(). Nous l'utilisons pour comparer la taille du tableau de symboles actuel avec les données disponibles pour d'autres symboles. Si nécessaire, les données sont chargées depuis le serveur. Le code de fonction est fourni avec des commentaires détaillés pour votre considération.

//+------------------------------------------------------------------+
//| 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();
        }
     }
  }

Après avoir tenté de charger la quantité de données requise, nous vérifions à nouveau les poignées d'indicateur. À cette fin, nous utilisons la fonction GetIndicatorHandles() considérée ci-dessus.

Une fois les poignées vérifiées, le programme vérifie la disponibilité des données et des valeurs d'indicateur des symboles spécifiés pour chaque symbole à l'aide de la fonction CheckAvailableData(). Ci-dessous, vous pouvez voir de plus près comment cela se fait :

//+------------------------------------------------------------------+
//| 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);
  }

La fonction CheckAvailableData() ne permettra pas d'effectuer d'autres calculs tant que les données de tous les symboles ne seront pas prêtes. Le fonctionnement de toutes les fonctions de contrôle suit un modèle similaire.

La fonction suivante est requise pour surveiller l'événement de chargement d'un historique plus approfondi des devis. Appelons-le CheckEventLoadHistory(). Si une plus grande quantité de données est chargée, l'indicateur doit être entièrement recalculé. Le code source de cette fonction est fourni ci-dessous :

//+------------------------------------------------------------------+
//| 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);
  }

Écrivons une autre fonction pour vérifier la synchronisation entre les données dans le terminal et sur le serveur. Cette vérification ne sera exécutée que si la connexion au serveur est établie. Le code de la fonction CheckSymbolIsSynchronized() est fourni ci-dessous :

//+------------------------------------------------------------------+
//| 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);
  }

La fonction de conversion de la trame temporelle en chaîne de caractères sera reprise des articles précédents de la série "MQL5 Cookbook":

//+------------------------------------------------------------------+
//| 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);
  }

Et enfin, nous devons identifier et enregistrer la première vraie barre pour chaque symbole en la marquant dans le graphique avec une ligne verticale. Pour ce faire, écrivons une fonction DefineFirstTrueBar() et une fonction auxiliaire GetFirstTrueBarTime() qui renvoie l'heure de la première vraie barre.

//+-----------------------------------------------------------------------+
//| 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);
  }

L'heure de la première vraie barre est marquée dans le graphique par une ligne verticale à l'aide de la fonction CreateVerticalLine() :

//+------------------------------------------------------------------+
//| 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);
     }
  }

Les fonctions de tarification sont prêtes. En conséquence, la partie des OnCalculate() lorsque la variable prev_calculated est égale à zéro se présentera désormais comme indiqué ci-dessous :

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

Désormais, chaque fois qu'une certaine vérification échoue, le programme recule pour faire une autre tentative au prochain tick ou événement de minuterie. Dans la minuterie, nous devrions également exécuter la vérification pour charger un historique plus profond en dehors des OnCalculateOnCalculate() :

//+------------------------------------------------------------------+
//| 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);
     }
  }

Il ne nous reste plus qu'à écrire deux boucles principales à placer dans les OnCalculate() :

  • La première boucle préparera les données sur la base du principe "obtenir la valeur par tous les moyens" pour éviter les lacunes dans les séries d'indicateurs. L'idée sous-jacente est simple : un nombre donné de tentatives sera effectué en cas d'échec pour obtenir la valeur. Dans cette boucle, les valeurs temporelles des symboles et les valeurs de l'indicateur de volatilité (ATR) seront enregistrées dans des tableaux séparés.
  • Dans la deuxième boucle principale, lors du remplissage des tampons d'indicateurs, des tableaux temporels d'autres symboles seront nécessaires pour la comparaison avec l'heure du symbole actuel et la synchronisation de toutes les séries de traçage.

Le code de la première boucle est fourni ci-dessous :

//--- 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();
           }
        }
     }

La fonction principale de copie et d'enregistrement des valeurs, PrepareData(), est mise en évidence dans le code ci-dessus. Il existe également une nouvelle fonction qui n'a pas encore été envisagée - ProgressPercentage(). Il calcule le pourcentage de progression de l'opération en cours pour informer l'utilisateur de sa durée.

Le code de la fonction PrepareData() est le suivant :

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

Le code de la fonction ProgressPercentage() est le suivant :

//+------------------------------------------------------------------+
//| 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+"% ... ";
  }

Les tampons d'indicateurs sont remplis dans la deuxième boucle principale des OnCalculate() :

//--- 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();
           }
        }
     }

La chaîne de caractères en évidence dans le code ci-dessus contient la fonction FillIndicatorBuffers(). C'est ici que s'effectuent les dernières opérations avant d'afficher les séries de traçage de l'indicateur dans le graphique :

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

À la fin des OnCalculate(), nous devons supprimer le canevas, définir les niveaux, mettre à zéro les variables des messages et actualiser le graphique. Enfin, la taille du tableau rates_total sera renvoyée, après quoi seule la dernière valeur sera recalculée à chaque tick ou événement de minuterie ultérieur dans les OnCalculate().

Voici les chaînes de code à insérer entre la deuxième boucle principale et la valeur renvoyée par la fonction :

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

Le code de la fonction SetIndicatorLevels() pour définir les niveaux horizontaux est le suivant :

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

Le code de la fonction CreateHorizontalLine() pour définir un niveau horizontal avec les propriétés spécifiées est le suivant :

//+------------------------------------------------------------------+
//| 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);
     }
  }

Fonctions de suppression d'objets graphiques :

//+------------------------------------------------------------------+
//| 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!");
     }
  }

Le code suivant doit être ajouté aux OnDeinit() :

Maintenant, tout est prêt et peut être testé à fond. Le nombre maximum de barres dans la fenêtre peut être défini dans l'onglet Graphiques des paramètres du terminal. La vitesse à laquelle l'indicateur sera prêt à fonctionner dépend du nombre de barres dans la fenêtre.

Fig. 1. Réglage du nombre maximum de barres dans les paramètres du terminal

Fig. 1. Réglage du nombre maximum de barres dans les paramètres du terminal

Après avoir défini le nombre maximum de barres, le terminal doit être redémarré pour que l'indicateur prenne en compte les modifications, sinon la valeur précédente sera utilisée.

Lors du chargement de l'indicateur dans le graphique, vous pouvez voir la progression de la préparation des données pour tous les symboles :

Fig. 2. Le message sur le canevas pendant la préparation des données

Fig. 2. Le message sur le canevas lors de la préparation des données

Ci-dessous, vous pouvez voir la capture d'écran affichant l'indicateur sur une trame temporelle de 20 minutes :

Fig. 3. Indicateur ATR multi-symboles sur une trame temporelle de 20 minutes

Fig. 3. Indicateur ATR multi-symboles sur une trame temporelle de 20 minutes

Le début des "vraies" barres est marqué dans le graphique par des lignes verticales. La capture d'écran ci-dessous montre que les vraies barres pour NZDUSD (ligne jaune) commencent à partir de 2000 (serveur MetaQuotes-Demo), tandis que pour toutes les autres paires de devises, les vraies barres apparaissent au début de 1999, c'est pourquoi une seule ligne est affichée (toutes sont allumées la même date). On peut aussi remarquer que les séparateurs de période ont un intervalle plus petit avant 1999 et si vous analysez l'heure des barres, vous pourrez voir que ce sont des barres quotidiennes.

Fig. 4. Les lignes verticales marquent le début des vraies barres pour chaque symbole

 

Conclusion

L'article peut être terminé ici. Le code source décrit est joint à l'article et est disponible en téléchargement. Dans l'un des futurs articles, nous essaierons de mettre en place un système de trading qui analyserait la volatilité et verrait ce qui en sort.

Traduit du russe par MetaQuotes Ltd.
Article original : https://www.mql5.com/ru/articles/752

Fichiers joints |
multisymbolatr.mq5 (47.42 KB)
Création d'un Expert Advisor multi-devises multi-systèmes Création d'un Expert Advisor multi-devises multi-systèmes
L'article présente une structure pour un Expert Advisor qui trade plusieurs symboles et utilise plusieurs systèmes de trading simultanément. Si vous avez déjà identifié les paramètres d'entrée optimaux pour tous vos EA et obtenu de bons résultats de backtesting pour chacun d'eux séparément, demandez-vous quels résultats vous obtiendriez si vous testiez tous les EA simultanément, avec toutes vos stratégies réunies.
Le MQL5 Cookbook : Commande de la sous-fenêtre d’indicateur - Barre de défilement Le MQL5 Cookbook : Commande de la sous-fenêtre d’indicateur - Barre de défilement
Continuons à explorer les différentes commandes et cette fois, tournons notre attention vers la barre de défilement. Tout comme dans l'article précédent intitulé "MQL5 Cookbook : Commande de la sous-fenêtre d’indicateur - Boutons", toutes les opérations seront effectuées dans la sous-fenêtre d'indicateur. Prenez un moment pour lire l'article mentionné ci-dessus car il fournit une description détaillée de l'utilisation des événements dans la fonction OnChartEvent(), alors que ce point ne sera abordé qu'avec désinvolture dans cet article. À des fins d'illustration, cette fois-ci, nous allons créer une barre de défilement verticale pour une grande liste de toutes les propriétés d'instruments financiers qui peuvent être obtenues à l'aide des ressources MQL5.
Indicateur pour la cartographie Kagi Indicateur pour la cartographie Kagi
L'article propose un indicateur de graphique Kagi avec diverses options de cartographie et des fonctions supplémentaires. En outre, le principe de cartographie des indicateurs et ses fonctionnalités de mise en œuvre MQL5 sont pris en compte. Les cas les plus populaires de sa mise en œuvre dans le trading sont affichés - stratégie d'échange Yin/Yang, s'éloignant de la ligne de tendance et augmentant constamment les "épaules" / diminuant la "taille".
Le MQL5 Cookbook : Commandes de la sous-fenêtre d'indicateur - Boutons Le MQL5 Cookbook : Commandes de la sous-fenêtre d'indicateur - Boutons
Dans cet article, nous examinerons un exemple de développement d'une interface utilisateur avec des commandes de bouton. Pour transmettre l'idée d'interactivité à l'utilisateur, les boutons changeront de couleur lorsque le curseur les survolera. Avec le curseur sur un bouton, la couleur du bouton sera légèrement assombrie, devenant nettement plus sombre lorsque le bouton est cliqué. De plus, nous ajouterons des info-bulles à chaque bouton, créant ainsi une interface intuitive.