MQL5 : Créez votre propre indicateur

MetaQuotes | 22 décembre, 2021

Introduction

Qu’est-ce qu’un indicateur? C’est un ensemble de valeurs calculées que nous voulons afficher à l’écran de manière pratique. Les ensembles de valeurs sont représentés dans les programmes sous forme de tableaux. Ainsi, la création d’un indicateur signifie écrire un algorithme qui gère certains tableaux (tableaux de prix) et enregistre les résultats de la manipulation sur d’autres tableaux (valeurs d’indicateur).

Malgré le fait qu’il existe de nombreux indicateurs disponibles, qui sont déjà devenus classiques, la nécessité de créer ses propres indicateurs existera toujours. Ces indicateurs que nous créons à l’aide de nos propres algorithmes sont appelés indicateurs personnalisés. Dans cet article, nous verrons comment créer un indicateur personnalisé simple.

Les indicateurs sont différents

Un indicateur peut être présenté sous forme de lignes ou de zones colorées, ou il peut être affiché sous forme d’étiquettes spéciales pointant vers des moments favorables pour la prise de position. De plus, ces types peuvent être combinés, ce qui donne encore plus de types d’indicateurs. Nous envisagerons la création d’un indicateur sur l’exemple du célèbre True Strength Index développé par William Blau.

Indice de force réelle

L’indicateur STI est basé sur la dynamique à double lissage pour identifier les tendances, ainsi que les zones de survente/surachat. L’explication mathématique de celui-ci peut être trouvée dans Momentum, Direction, and Divergence de William Blau. Ici, nous n’incluons que sa formule de calcul.

TSI(CLOSE,r,s) =100*EMA(EMA(mtm,r),s) / EMA(EMA(|mtm|,r),s)

Où :

De cette formule, nous pouvons extraire trois paramètres qui influencent le calcul de l’indicateur. Il s’agit des périodes r et s, ainsi que du type de prix utilisés pour les calculs. Dans notre cas, nous utilisons prix CLOSE.

Assistant MQL5

Affichons TSI comme une ligne bleue - ici, nous devons démarrer l’assistant MQL5. À la première étape, nous devons indiquer le type de programme que nous voulons créer - indicateur personnalisé. À la deuxième étape, définissons le nom du programme, les paramètres r et s et leurs valeurs.

Assistant MQL5 : Configuration du nom et du paramètre de l’indicateur

Après cela, définissons que l’indicateur doit être affiché dans une fenêtre séparée sous la forme d’une ligne bleue et définissons l’étiquette TSI pour cette ligne.

Assistant MQL5 : configuration du type d’indicateur

Toutes les données initiales ont été saisies, nous appuyons donc sur Terminé et obtenons un brouillon de notre indicateur. 

//+------------------------------------------------------------------+
//|                                          True Strength Index.mq5 |
//|                        Copyright 2009, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "2009, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property indicator_separate_window
#property indicator_buffers 1
#property indicator_plots   1
//---- plot TSI
#property indicator_label1  "TSI"
#property indicator_type1   DRAW_LINE
#property indicator_color1  Blue
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1
//--- input parameters
input int      r=25;
input int      s=13;
//--- indicator buffers
double         TSIBuffer[];
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0,TSIBuffer,INDICATOR_DATA);
//---
   return(0);
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(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[])
  {
//---
//--- return value of prev_calculated for next call
   return(rates_total);
  }
//+------------------------------------------------------------------+

L’Assistant MQL5 crée l’en-tête de l’indicateur, dans lequel il écrit les propriétés de l’indicateur, à savoir :

Tous les préparatifs sont prêts, maintenant nous pouvons affiner et améliorer notre code.

OnCalculate()

La fonction OnCalculate() est le gestionnaire de l’événement Calculate, qui apparaît lorsqu’il est nécessaire de recalculer les valeurs de l’indicateur et de le retracer sur le graphique. Il s’agit de l’événement d’une nouvelle réception de traits, d’une mise à jour de l’historique des symboles, etc. C’est pourquoi le code principal pour tous les calculs de valeurs d’indicateur doit être situé exactement dans cette fonction.

Bien sûr, les calculs auxiliaires peuvent être implémentés dans d’autres fonctions distinctes, mais ces fonctions doivent être utilisées dans le gestionnaire OnCalculate.

Par défaut, l’Assistant MQL5 crée le deuxième formulaire de OnCalculate(), qui permet d’accéder à tous les types de séries chronologiques :

Mais dans notre cas, nous n’avons besoin que d’un seul tableau de données, c’est pourquoi changeons OnCalculate() le premier formulaire d’appel.

int OnCalculate (const int rates_total,      // size of the price[] array
                 const int prev_calculated,  // number of available bars at the previous call
                 const int begin,            // from what index in price[] authentic data start
                 const double& price[])      // array, on which the indicator will be calculated
  {
//---
//--- return value of prev_calculated for next call
   return(rates_total);
  }  

Cela nous permettra d’appliquer davantage l’indicateur non seulement aux données sur les prix, mais aussi de créer l’indicateur basé sur les valeurs d’autres indicateurs.

Spécification du type de données pour le calcul de l’indicateur personnalisé

Si nous sélectionnons Close dans l’onglet Paramètres (il est proposé par défaut), alors price[] transmis vers OnCalculate() contiendra les prix close. Si nous sélectionnons, par exemple, Typical Price, price[] contiendra des prix de (High+Low+Close)/3 pour chaque période.

Le paramètre rates_total indique la taille du tableau price[] ; il sera utile pour organiser les calculs dans un cycle. L’indexation des éléments dans le prix[] commence à partir de zéro et est dirigée du passé vers le futur. C’est-à-dire que l’élément price[0] contient la valeur la plus ancienne, tandis que price[rates_total-1] contient le dernier élément de tableau.

Organisation des tampons d’indicateurs auxiliaires

Une seule ligne sera affichée dans un graphique, c’est-à-dire les données d’un tableau d’indicateurs. Mais avant cela, nous devons organiser des calculs intermédiaires. Les données intermédiaires sont stockées dans des tableaux d’indicateurs marqués par l’attribut INDICATOR_CALCULATIONS. D’après le formulaire, nous voyons que nous avons besoin de tableaux supplémentaires :

  1. pour les valeurs mtm - tableau MTMBuffer[] ;
  2. pour les valeurs |mtm| - tableau AbsMTMBuffer[] ;
  3. pour EMA(mtm,r) - tableau EMA_MTMBuffer[] ;
  4. pour EMA(EMA(mtm,r),s) - tableau EMA2_MTMBuffer[] ;
  5. pour EMA(|mtm|,r) - tableau EMA_AbsMTMBuffer[] ;
  6. pour EMA(EMA(|mtm|,r),s) - tableau EMA2_AbsMTMBuffer[].

Au total, nous devons ajouter 6 tableaux supplémentaires de double type au niveau global et lier ces tableaux avec les tampons d’indicateur de la fonction Onlnit(). N’oubliez pas d’indiquer le nouveau nombre de tampons d’indicateurs ; la propriété indicator_buffers doit être égale à 7 (il y en avait 1, et 6 tampons supplémentaires ont été ajoutés).

#property indicator_buffers 7

Maintenant, le code de l’indicateur ressemble à ceci :

#property indicator_separate_window
#property indicator_buffers 7
#property indicator_plots   1
//---- plot TSI
#property indicator_label1  "TSI"
#property indicator_type1   DRAW_LINE
#property indicator_color1  Blue
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1
//--- input parameters
input int      r=25;
input int      s=13;
//--- indicator buffers
double         TSIBuffer[];
double         MTMBuffer[];
double         AbsMTMBuffer[];
double         EMA_MTMBuffer[];
double         EMA2_MTMBuffer[];
double         EMA_AbsMTMBuffer[];
double         EMA2_AbsMTMBuffer[];
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0,TSIBuffer,INDICATOR_DATA);
   SetIndexBuffer(1,MTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(2,AbsMTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(3,EMA_MTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(4,EMA2_MTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(5,EMA_AbsMTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(6,EMA2_AbsMTMBuffer,INDICATOR_CALCULATIONS);
//---
   return(0);
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate (const int rates_total,    // size of the price[] array;
                 const int prev_calculated,// number of available bars;
                                           // during the previous call;
                 const int begin,          // from what index in  
                                           // price[] authentic data start;
                 const double& price[])    // array, on which the indicator will be calculated;
  {
//---
//--- return value of prev_calculated for next call
   return(rates_total);
  }

Calculs intermédiaires

Il est très facile d’organiser le calcul des valeurs pour les tampons MTMBuffer[] et AbsMTMBuffer[]. Dans la boucle, un par un, passez par des valeurs allant de price[1] à price[rates_total-1] et écrivez la différence dans un tableau, et la valeur absolue de la différence dans le second.

//--- calculate values of mtm and |mtm|
   for(int i=1;i<rates_total;i++)
     {
      MTMBuffer[i]=price[i]-price[i-1];
      AbsMTMBuffer[i]=fabs(MTMBuffer[i]);
     }

L’étape suivante est le calcul de la moyenne exponentielle de ces tableaux. Il y a deux façons de le faire. Dans le premier cas, nous écrivons tout l’algorithme en essayant de ne pas faire des erreurs. Dans le second cas, nous utilisons des fonctions prêtes à l’emploi qui sont déjà déboguées et destinées exactement à ces fins.

Dans MQL5, il n’y a pas de fonctions intégrées pour calculer les moyennes mobiles par les valeurs de tableau, mais il existe une bibliothèque disponible de fonctions MovingAverages.mqh, dont le chemin complet est terminal_directory/MQL5/Include/MovingAverages.mqh, où le terminal_directory est un catalogue dans lequel le terminal MetaTrader 5 est installé. La bibliothèque est un fichier Include ; elle contient des fonctions de calcul de moyennes mobiles sur des tableaux utilisant l’une des quatre méthodes classiques :

Pour utiliser ces fonctions, dans n’importe quel programme MQL5, ajoutez ce qui suit dans l’en-tête du code :

#include <MovingAverages.mqh>

Nous avons besoin de la fonction ExponentialMAOnBuffer(), qui calcule la moyenne mobile exponentielle sur le tableau de valeurs et enregistre les valeurs de la moyenne dans un autre tableau.

La fonction de lissage d’un tableau

Totalement, le fichier Include MovingAverages.mqh contient huit fonctions qui peuvent être divisées en deux groupes de fonctions du même type, chacun contenant 4 d’entre elles. Le premier groupe contient des fonctions qui reçoivent un tableau et renvoient simplement une valeur de moyenne mobile à une position spécifiée :

Ces fonctions sont destinées à obtenir la valeur d’une moyenne une fois pour un tableau et ne sont pas optimisées pour plusieurs appels. Si vous devez utiliser une fonction de ce groupe dans une boucle (pour calculer les valeurs d’une moyenne et écrire davantage chaque valeur calculée dans un tableau), vous devrez organiser un algorithme optimal.

Le deuxième groupe de fonctions est destiné à remplir le tableau des destinataires par les valeurs d’une moyenne mobile basée sur le tableau des valeurs initiales :

Toutes les fonctions spécifiées, à l’exception des tableaux buffer[], price[] et de la période de moyenne period, obtiennent 3 paramètres supplémentaires, dont le but est analogue aux paramètres de la fonction OnCalculate() - rates_total, prev_calculated et begin. Les fonctions de ce groupe traitent correctement les tableaux passés de price[] et buffer[], en tenant compte de la direction du drapeau d’indexation(AS_SERIES).

Le paramètre begin indique l’indice d’un tableau source, à partir duquel commencent des données significatives, c’est-à-dire des données qui doivent être traitées. Pour le tableau MTMBuffer[], les données réelles commencent par l’indice 1, parce que MTMBuffer[1]=price[1]-price[0]. La valeur de MTMBuffer[0] n’est pas définie, c’est pourquoi begin=1.

//--- calculate the first moving
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         1,  // index, starting from which data for smoothing are available 
                         r,  // period of the exponential average
                         MTMBuffer,       // buffer to calculate average
                         EMA_MTMBuffer);  // into this buffer locate value of the average
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         1,r,AbsMTMBuffer,EMA_AbsMTMBuffer);

Lors de la moyenne, la valeur de la période doit être prise en compte, car dans le tableau de sortie, les valeurs calculées sont remplies avec un petit délai, qui est plus important pour des périodes de moyenne plus longues. Par exemple, si period=10, les valeurs du tableau qui en résultent commenceront par begin+period-1=begin+10-1. Lors des appels ultérieurs de buffer[], il faut en tenir compte, le traitement devrait commencer avec l'indice begin+period-1.

Ainsi, nous pouvons facilement obtenir la deuxième moyenne exponentielle à partir des tableaux de MTMBuffer[] et AbsMTMBuffer :

//--- calculate the second moving average on arrays
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         r,s,EMA_MTMBuffer,EMA2_MTMBuffer);
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         r,s,EMA_AbsMTMBuffer,EMA2_AbsMTMBuffer);

La valeur de begin est maintenant égale à r, car begin=1+r-1 (r est la période de la moyenne exponentielle primaire, la manipulation commence par l’indice 1). Dans les tableaux de sortie de EMA2_MTMBuffer[] et EMA2_AbsMTMBuffer[], les valeurs calculées commencent par l’indice r+s-1, car nous avons commencé à gérer les tableaux d’entrée avec l’indice r, et la période de la deuxième moyenne exponentielle est égale à s.

Tous les pré-calculs sont prêts, nous pouvons maintenant calculer les valeurs de l’indicateur tampon TSIBuffer[], qui seront tracées dans le graphique.

//--- now calculate values of the indicator
   for(int i=r+s-1;i<rates_total;i++)
     {
      TSIBuffer[i]=100*EMA2_MTMBuffer[i]/EMA2_AbsMTMBuffer[i];
     }
Compilez le code en appuyant sur la touche F5 et démarrez-le dans le terminal MetaTrader 5. Ça marche !

La première version de True Strength Index

Il reste encore quelques questions.

Optimisation des calculs

En fait, il ne suffit pas d’écrire un indicateur fonctionnel. Si nous examinons attentivement la mise en œuvre actuelle de OnCalculate(), nous verrons qu’elle n’est pas optimale.

int OnCalculate (const int rates_total,    // size of the price[] array;
                 const int prev_calculated,// number of available bars;
                 // at the previous call;
                 const int begin,// from what index of the 
                 // price[] array true data start;
                 const double &price[]) // array, at which the indicator will be calculated;
  {
//--- calculate values of mtm and |mtm|
   MTMBuffer[0]=0.0;
   AbsMTMBuffer[0]=0.0;
   for(int i=1;i<rates_total;i++)
     {
      MTMBuffer[i]=price[i]-price[i-1];
      AbsMTMBuffer[i]=fabs(MTMBuffer[i]);
     }
//--- calculate the first moving average on arrays
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         1,  // index, starting from which data for smoothing are available 
                         r,  // period of the exponential average
                         MTMBuffer,       // buffer to calculate average
                         EMA_MTMBuffer);  // into this buffer locate value of the average
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         1,r,AbsMTMBuffer,EMA_AbsMTMBuffer);

//--- calculate the second moving average on arrays
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         r,s,EMA_MTMBuffer,EMA2_MTMBuffer);
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         r,s,EMA_AbsMTMBuffer,EMA2_AbsMTMBuffer);
//--- now calculate values of the indicator
   for(int i=r+s-1;i<rates_total;i++)
     {
      TSIBuffer[i]=100*EMA2_MTMBuffer[i]/EMA2_AbsMTMBuffer[i];
     }
//--- return value of prev_calculated for next call
   return(rates_total);
  }

À chaque démarrage de fonction, nous calculons des valeurs dans des tableaux de MTMBuffer[] et AbsMTMBuffer[]. Dans ce cas, si la taille du prix est égale à des centaines de milliers, voire des millions, des calculs répétés inutiles peuvent prendre toutes les ressources du processeur, quelle que soit sa puissance.

Pour organiser les calculs optimaux, nous utilisons le paramètre d’entrée prev_calculated, qui est égal à la valeur renvoyée par OnCalculate() lors de l’appel précédent. Dans le premier appel de la fonction, la valeur de prev_calculated est toujours égale à 0. Dans ce cas, nous calculons toutes les valeurs dans le tampon de l’indicateur. Lors du prochain appel, nous n’aurons pas à calculer l’ensemble du tampon - seule la dernière valeur sera calculée. Écrivons-le comme ceci :

//--- if it is the first call 
   if(prev_calculated==0)
     {
      //--- set zero values to zero indexes
      MTMBuffer[0]=0.0;
      AbsMTMBuffer[0]=0.0;
     }
//--- calculate values of mtm and |mtm|
   int start;
   if(prev_calculated==0) start=1;  // start filling out MTMBuffer[] and AbsMTMBuffer[] from the 1st index 
   else start=prev_calculated-1;    // set start equal to the last index in the arrays 
   for(int i=start;i<rates_total;i++)
     {
      MTMBuffer[i]=price[i]-price[i-1];
      AbsMTMBuffer[i]=fabs(MTMBuffer[i]);
     }

Les blocs de calcul de EMA_MTMBuffer[], EMA_AbsMTMBuffer[], EMA2_MTMBuffer[] et EMA2_AbsMTMBuffer[] ne nécessitent pas d’optimisation des calculs, car ExponentialMAOnBuffer() est déjà écrit de manière optimale. Nous devons optimiser uniquement le calcul des valeurs pour le tableau TSIBuffer[]. Nous utilisons la même méthode que celle utilisée pour MTMBuffer[].

//--- now calculate the indicator values
   if(prev_calculated==0) start=r+s-1; // set the starting index for input arrays
   for(int i=start;i<rates_total;i++)
     {
      TSIBuffer[i]=100*EMA2_MTMBuffer[i]/EMA2_AbsMTMBuffer[i];
     }
//--- return value of prev_calculated for next call
   return(rates_total);

La dernière remarque pour la procédure d’optimisation : OnCalculate() renvoie la valeur de rates_total. Il s’agit du nombre d’éléments dans le tableau d’entrée price[] qui est utilisé pour les calculs d’indicateurs.

La valeur renvoyée par OnCalculate() est enregistrée dans la mémoire du terminal et, lors de l’appel suivant de OnCalculate(), elle est transmise à la fonction en tant que valeur du paramètre d’entrée prev_calculated.

Cela permet de toujours connaître la taille du tableau d’entrée lors de l’appel précédent de OnCalculate() et de commencer le calcul des tampons d’indicateurs à partir d’un indice correct sans recalculs inutiles.

Vérification des données d’entrée

Il y a encore une chose que nous devons faire pour que OnCalculate() fonctionne parfaitement. Ajoutons la vérification du tableau price[], sur lequel les valeurs des indicateurs sont calculées. Si la taille du tableau (rates_total) est trop petite, aucun calcul n’est nécessaire - nous devons attendre le prochain appel de OnCalculate(), lorsque les données sont suffisantes.

//--- if the size of price[] is too small
  if(rates_total<r+s) return(0); // do not calculate or draw anything
//--- if it's the first call 
   if(prev_calculated==0)
     {
      //--- set zero values for zero indexes
      MTMBuffer[0]=0.0;
      AbsMTMBuffer[0]=0.0;
     }

Étant donné que le lissage exponentiel est utilisé deux fois séquentiellement pour calculer l’indice de force réelle, la taille du prix[] doit être au moins égale ou supérieure à la somme des périodes r et s ; sinon, l’exécution est terminée et OnCalculate() renvoie 0. La valeur zéro renvoyée signifie que l’indicateur ne sera pas tracé dans le graphique, car ses valeurs ne sont pas calculées.

Configuration de la représentation

En ce qui concerne l’exactitude des calculs, l’indicateur est prêt à l’emploi. Mais si nous l’appelons à partir d’un autre programme mql5, il sera construit par les prix close par défaut. Nous pouvons spécifier un autre type de prix par défaut - spécifiez une valeur de l’énumération ENUM_APPLIED_PRICE dans la propriété indicator_applied_price de l’indicateur. 

Par exemple, afin de définir un prix typique ( (high+low+close)/3) pour un prix, écrivons ce qui suit :

#property indicator_applied_price PRICE_TYPICAL


Si nous prévoyons d’utiliser uniquement ses valeurs à l’aide des fonctions iCustom() ou IndicatorCreate(), aucun autre raffinement n’est nécessaire. Mais s’ils sont utilisés directement, c’est-à-dire tracés dans le graphique, des paramètres supplémentaires sont recommandés :

Ces paramètres peuvent être réglés dans le gestionnaire OnInit(), à l’aide des fonctions du groupe Indicateurs personnalisés. Ajoutez de nouvelles lignes et enregistrez l’indicateur sous True_Strength_Index_ver2.mq5.

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0,TSIBuffer,INDICATOR_DATA);
   SetIndexBuffer(1,MTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(2,AbsMTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(3,EMA_MTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(4,EMA2_MTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(5,EMA_AbsMTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(6,EMA2_AbsMTMBuffer,INDICATOR_CALCULATIONS);
//--- bar, starting from which the indicator is drawn
   PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,r+s-1);
   string shortname;
   StringConcatenate(shortname,"TSI(",r,",",s,")");
//--- set a label do display in DataWindow
   PlotIndexSetString(0,PLOT_LABEL,shortname);   
//--- set a name to show in a separate sub-window or a pop-up help
   IndicatorSetString(INDICATOR_SHORTNAME,shortname);
//--- set accuracy of displaying the indicator values
   IndicatorSetInteger(INDICATOR_DIGITS,2);
//---
   return(0);
  }

Si nous commençons les deux versions de l’indicateur et faisons défiler le graphique jusqu’au début, nous verrons toutes les différences.


La deuxième version de l’indicateur True Strength Index est meilleure

Conclusion

Sur la base de l’exemple de création de l’indicateur True Strength Index, nous pouvons décrire les moments de base dans le processus d’écriture de n’importe quel indicateur dans MQL5 :