mardi 1 avril 2014

Les objets graphiques

MetaTrader nous permet de créer en manuel des objets graphiques pour nous aider dans l'analyse des cours. Ces objets sont divers et standardisés dans le monde du trading: des droites de tendances, des canaux, des lignes de supports ou résistances, des niveaux de Fibonacci, des angles de Gann, des marques d'ouverture de position, des annotations de texte, etc. Ces objets nous aident à visualiser, à analyser les cours et à gérer notre trading manuel. Tous ces objets graphiques peuvent être créés, modifiés et effacés par programmation avec le langage MQL4. Nous voyons une introduction à tout cela dans ce post.

Les types d'objets


Les objets sont de divers types et ces types sont relativement nombreux. la seule chose qu'on ne puisse pas créer, c'est un segment de ligne. Cela pourrait nous servir à tracer une courbe, mais ceci est déjà géré par MetaTrader, et uniquement par le logiciel quand on créé un indicateur. C'est mieux ainsi, car MetaTrader peut ainsi gérer la mémoire importante nécessaire et la rapidité d'exécution. Nous devons nous contenter de symboles,, de texte, de primitives géométriques et d'objets d'analyse technique des cours. Voici une liste non exhaustive avec les constantes prédéfinies MQL4 à utiliser pour les obtenir:
  • Le texte
    • Labels  OBJ_LABEL 
    • Textes  OBJ_TEXT 
    • Zones d'édition de texte  OBJ_EDIT 
    • Label Prix gauche  OBJ_ARROW_LEFT_PRICE 
    • Label Prix droite  OBJ_ARROW_RIGHT_PRICE 
  • Les primitives géométriques
    • Rectangles  OBJ_RECTANGLE 
    • Triangles  OBJ_TRIANGLE 
    • Ellipses  OBJ_ELLIPSE 
  • Les symboles
    • Stop  OBJ_ARROW_STOP 
    • Check  OBJ_ARROW_CHECK 
    • Main avec pouce vers le haut  OBJ_ARROW_THUMB_UP 
    • Main avec pouce vers le bas  OBJ_ARROW_THUMB_DOWN 
    • Achat  OBJ_ARROW_BUY 
    • Vente  OBJ_ARROW_SELL 
    • Flèche vers le haut  OBJ_ARROW_UP 
    • Flèche vers le bas  OBJ_ARROW_DOWN 
  • Les objets d'analyse technique
    • Ligne verticale de période  OBJ_VLINE 
    • Ligne horizontale de support ou résistance  OBJ_HLINE 
    • Ligne oblique de tendance  OBJ_TREND 
    • Ligne oblique de tendance définie par l'angle  OBJ_TRENDBYANGLE 
    • Cycles temporels  OBJ_CYCLES 
    • Canal équidistant  OBJ_CHANNEL 
    • Canal de déviation standard  OBJ_STDDEVCHANNEL 
    • Ligne de Gann  OBJ_GANNLINE 
    • Angles de Gann  OBJ_GANNFAN 
    • Grille de Gann  OBJ_GANNGRID 
    • Retracements de Fibonacci  OBJ_FIBO 
    • Zones temporelles de Fibonacci  OBJ_FIBOTIMES 
    • Angles de Fibonacci  OBJ_FIBOFAN 
    • Arcs de Fibonacci  OBJ_FIBOARC 
Et il y existe d'autres objets non cités ici. Nous ne les verrons pas tous dans ce post, seulement le label et comment les objets sont créés et gérés.

Le système de coordonnées des graphiques


Avant toute chose, il faut préciser le système de coordonnées utilisé dans les graphiques. Un graphique en trading est temporel, c'est-à-dire que les abscisses sont exprimées en temps: du type datetime. En horizontal nous avons le temps. Pour les ordonnées, on se place verticalement en fonction du cours, c'est-à-dire du prix du sous-jacent (ici pour le forex, c'est le taux de conversion de la paire de devises). C'est donc un nombre décimal, du type double. Il y a certains objets qui ne doivent pas être placés sur un temps et un prix précis, mais de manière fixe sur la fenêtre, de sorte qu'on la voit toujours. Un temps ou un prix pouvant être hors affichage. On va d'ailleurs commencer à utiliser de tels objets.

Les sous-graphiques


Autre détail important: un graphique peut posséder des sous graphiques pour y placer des indicateurs. Un indicateur pouvant se placer dans le graphique principal de la paire de devises, ou bien dans une fenêtre à part, qui est sous-fenêtre de la principale. Ces fenêtres sont numérotées dans leur ordre d'apparition: 0 pour la principale, 1 pour la première sous-fenêtre, 2 pour la deuxième, etc.

La création d'objets


Créer un objet graphique se fait à l'aide d'une fonction prédéfinie en MQL4:  ObjectCreate() . Cette fonction retourne un booléen (type bool), c'est-à-dire vrai ou faux pour dire si la création de l'objet a réussi ou non. Depuis la nouvelle version du langage MQL4 mise à jour récemment en février 2014, il y a deux versions de cette fonction, (elle est surchargée, pour les connaisseurs): il y a une version où on indique la référence de la fenêtre principale où on veut créer l'objet, puis le numéro de la sous-fenêtre (commençant à zéro), et une version où on n'indique pas la fenêtre principale car c'est celle où est exécutée le programme (indicateur ou expert advisor). Seule la deuxième était existante avant.
Il faut lui fournir de nombreux arguments et dans cet ordre:
  •  long identifiantDeGraphique : C'est le pointeur de la fenêtre pour identifier la fenêtre principale sur laquelle on veut créer l'objet et qui donc peut être différente de celle où le programme fonctionne. Paramètre nouveau et optionnel.
  •  string nomDeLobjet : Nom de l'objet à créer. Il faut évidemment lui donner un nom pour pourvoir ensuite le modifier ou le supprimer.
  •  ENUM_OBJECT typeDeLobjet : On utilise ici une constante précédemment listée.
  •  int noSousFenetre : Le numéro de la sous-fenêtre graphique dans laquelle créer l'objet. 0 pour la principale, 1 pour la première si elle existe, 2 pour la deuxième si elle existe, etc. La fenêtre principale est celle où est exécutée le programme ou bien celle désignée par l'identifiant de fenêtre en option.
  •  datetime premiereCoordonneeTemps : l'horodatage du premier point qui place l'objet.
  •  double premiereCoordonneePrix : position veticale (prix) du premier point qui place l'objet.
  • On peut répéter ces deux coordonnées jusqu'à trois fois pour la vieille version de la fonction, parce qu'il peut falloir jusqu'à trois points pour définir la position, l'orientation et la taille de l'objet sur le graphique. Mais toujours par paire. Avec la nouvelle version, lorsqu'on précise une fenêtre par son identifiant, on n'est plus limité à trois, mais autant de points qu'on veut.
On créé ainsi un objet, mais sans préciser de contenu, car beaucoup n'en n'ont pas besoin. Ceux du type texte ou label, on besoin qu'on leur ajoute un contenu texte. Ce sera fait avec une modification de l'objet dans le paragraphe suivant. Une précision supplémentaire: les objets label, ne prennent pas en compte les coordonnées fournies à la création car un label va rester fixe par rapport à l'affichage, alors qu'un point placé sur un horodatage précis pourra ne pas être vu si l'horodatage est en dehors de l'intervalle affiché, de même pour la coordonnée verticale du prix. Un label devant être toujours affiché, il faut lui préciser dans un second temps des coordonnées en pixels par rapport à un coin de la fenêtre. Ce sera fait avec la fonction de modification.

La modification d'objets


Pour modifier un objet, c'est pas compliqué, il suffit d'utiliser la fonction spécialement prévue à cet effet:  ObjectSet() . Cette fonction change une propriété de l'objet. On doit lui donner le nom de l'objet à modifier, la propriété à changer sous forme de nombre entier soit une constante prédéfinie et la nouvelle valeur sous forme de nombre décimal. Elle retourne un booléen vrai ou faux pour dire si la modification a réussi ou pas. Les propriétés sont désignées par ces constantes (liste non exhaustive):
  •  OBJPROP_TIME1  horodatage du premier point de positionnement
  •  OBJPROP_PRICE1  prix du premier point de positionnement
  •  OBJPROP_COLOR  couleur de l'objet
  •  OBJPROP_STYLE  style de traçage pour les lignes uniquement
  •  OBJPROP_WIDTH  epaisseur du trait pour les lignes uniquement
  •  OBJPROP_ANGLE  angle de l'objet
  •  OBJPROP_FONTSIZE  taille de la fonte (police) pour les objet avec du texte.
  •  OBJPROP_CORNER  coin de la fenêtre origine de coordonnées pour placer le label (objets label uniquement), c'est un numéro de 0 à 3 pour les quatre coins du graphique.
  •  OBJPROP_XDISTANCE  position horizontale en pixels pour placer l'objet de manière fixe par rapport à la fenêtre, et non pas par rapport au temps et au prix. (pour les labels)
  •  OBJPROP_YDISTANCE  position verticale en pixels. (même commentaire)
Il y en a bien d'autres, voir la documentation fournie, dans l'aide de MetaEditor. On peut aussi simplement obtenir la valeur d'une propriété d'un objet avec la fonction  ObjectGet()  à qui on fournit le nom de l'objet et la propriété voulue et qui nous retourne sa valeur.
Nous allons dans le prochain post créer des labels, à qui il faut évidemment fournir un texte, et la fonction  ObjectSet()  ne peut le faire car la valeur transmise n'est pas un texte, mais est numérique. Il y a pour cela une fonction spéciale pour fixer le texte d'objets contenant du texte:  ObjectSetText() . On doit lui passer le nom de l'objet, le texte à mettre et en option car il y a des valeurs par défaut: la taille de la fonte (0 par défaut pour ne pas la changer), le nom de la fonte à utiliser (chaine de caractères pour désigner une police de caractères, par défaut NULL) et la couleur du texte (par défaut: clrNone). Elle retourne un booléen pour dire si la mise à jour du texte à réussi.

La suppression d'objets


Enfin, lors de la déinitialisation, il faut supprimer les objets créés pour libérer de la place sur le graphique. Il y a deux fonctions pour cela:  ObjectDelete()  qui prend un nom d'objet en argument et qui efface seulement celui-là. La fonction  ObjectsDeleteAll()  qui prend en option un numéro de sous-fenêtre et/ou un type d'objet pour se limiter à une sous-fenêtre et/ou un type d'objets et qui les efface tous. Ces deux fonctions retournent le nombre d'objets effacés et existent aussi maintenant en version avec la référence de la fenêtre principale en premier argument pour viser une fenêtre autre que celle sur laquelle le programme s'exécute.

Voilà pour une présentation qui permet de démarrer l'utilisation des objets graphiques, nous allons commencer dès le prochain post un fichier include d'outils pour les objets graphiques. Des fonctions pour les créer et les gérer et on commencera avec les labels.

vendredi 28 mars 2014

Test du fichier include d'outils pour les indicateurs

Dans le post précédent nous avons créé un fichier include avec des fonctions d'initialisation d'indicateur et d'index. Le résultat de ces fonctions ne peut pas être analysé par un programme, d'ailleurs les fonctions ne retournent rien, elles sont du type void. Le résultat ne peut être que visuel, et il faut en plus initialiser plusieurs fois l'indicateur avec des paramètres différents pour voir comment il réagit.

Une petite précision: il y a eu de gros changements dans MQL4 avec une mise à jour récente. Les nouveautés seront en partie utilisées et d'autres non utilisées pour l'instant car je ne les ai pas testées. Notamment les arguments fournis à la fonction  OnCalculate()  qui font double emploi avec les variables prédéfinies du même nom mais avec une majuscule. Ces arguments sont présents pour une meilleure compatibilité avec le langage C et MQL5. L'argument &spread[] m'a étonné, alors que jusqu'à présent les spreads des périodes n'étaient pas disponibles. En réalité, il est présent, au cas où le broker les fournisse, ce qui a peu de chances de se produire. L'argument rates_total serait équivalent à Bars et l'argument prev_calculated à  IndicatorCounted() . Je mets bien ça au conditionnel car je ne connais pas la gestion interne de ces arguments par MetaTrader, notamment vis-à-vis de la gestion mémoire. Il y aurait une différence car on doit désormais retourner à la fin de nos calculs le nombre de valeurs calculées, pour que Metatrader le donne à nouveau lors de l'appel suivant dans l'argument prev_calculated. Il faudra tester tout ceci à l'avenir. Pour le moment nous utiliserons toujours les anciennes méthodes et toute l'armada très «dirty code» (code sale) des arguments ne nous servira pas. (les variables globales sont aussi du code sale, mais elles ne gênent pas la lisibilité au moins !) Nous pourrions tout simplement utiliser encore la fonction  start() , mais on va commencer à s'habituer aux nouveautés juste avec  OnCalculate() . On constate aussi, que des paramètres des indicateurs et des index peuvent être fixés par des  #property  au début du code. Cela n'empêche en rien de les déclarer à nouveau par nos fonctions d'initialisation, d'autant plus qu'ils n'y sont pas tous et que cela permet de les modifier à la volée lors de l'initialisation, ce que ne permet pas les  #property .

Un signal d'index vite fait pour remplir l'historique:


Vite fait, c'est vite dit ! Nous allons utiliser le signal d'un indicateur déjà présent dans MetaTrader. Donc dans la boucle principale de l'indicateur nous récupérons simplement la valeur d'un indicateur de moyenne mobile  iMA()  et nous la mettons dans notre tableau d'historique. Pour rappel, nous devons à chaque appel de la fonction  OnCalculate()  (on utilisait avant  start()  et on peut toujours) parcourir toutes les périodes à recalculer comme le demande MetaTrader. Le nombre de périodes total dans l'historique est donné par  Bars  et le nombre de périodes depuis le début de l'historique à ne pas recalculer est donné par  IndicatorCounted (). Donc le nombre de périodes à traiter est  Bars - IndicatorCounted()  et le numéro de la première période à traiter est alors  Bars - IndicatorCounted - 1  car la numérotation des cases du tableau est décalée d'une unité. Un schéma vaut mieux qu'un long discours:
tableau d'historique
supposons un historique de 10 périodes au total et 2 périodes déjà calculées
9876543210
Bars - 1  =  10 - 1  =  9       et       Bars - IndicatorCounted - 1  =  10 - 2 - 1  =  7
On commence bien à traiter la période n°7 qui est la première à devoir être calculée!

 Dans notre code nous calculons donc le numéro de la première période à traiter et nous commençons la boucle for à ce numéro et on la continue en décrémentant (diminuant) de 1 à chaque passage le numéro de la période. La boucle s'arrête selon les nécessités des calculs des indicateurs au n°1 ou 0. A chaque passage on récupère la valeur de la moyenne mobile et on l'affecte à la case correspondante de notre tableau, c'est tout ! Pour les besoins de calculs de cet indicateur nous ne devons pas commencer les calculs dès le début de l'historique, puisque pour faire une moyenne mobile de 12 périodes par exemples, il nous faut 12 périodes précédentes. On commence donc à  Bars-1-nbPeriodesMoyenne . Le code de l'indicateur de test est simple:
  1. Initialisation du nombre de périodes à traiter
  2. Si ce nombre est supérieur à  Bars-nbPeriodesMoyenne-1  alors fixer le début à cette valeur.
  3. Sinon fixer le début  nbPeriodesATraiter-1 
  4. Initialiser la boucle For
    1. récupérer la moyenne mobile de l'indicateur déjà existant dans MetaTrader
    2. mettre cette valeur dans notre tableau d'historique.
  5. C'est nouveau: retourner  rates_total  !
Voici le code:

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[]) {
   double moyenneMobile;
   int noPeriode, noPeriodeDebut;
   int nbPeriodeATraiter = Bars - IndicatorCounted();
   int noPeriodeLimiteCalcul = Bars - nbPeriodesMoyenne - 1;
   if(nbPeriodesATraiter>noPeriodeLimiteCalcul) {
      noPeriodeDebut = noPeriodeLimiteCalcul;
   }
   else { noPeriodeDebut = nbPeriodesATraiter - 1; }
   for(noPeriode=noPeriodeDebut ; noPeriode>=0 ; noPeriode--) {
      moyenneMobile = iMA(NULL, 0, nbPeriodesMoyenne, shift,
                           MODE_SMA, PRICE_MEDIAN, noPeriode);
      bufferMoyenne[noPeriode] = moyenneMobile;
   }
   return(rates_total);
}
Quelques commentaires: variables colorées en rouge sont des variables globales qui servent de paramètre à l'indicateur. Elles sont déclarées désormais input au début du code au lieu de extern jusqu'à la récente mise à jour du langage. Ces paramètres externes permettent de modifier des valeurs pour le programme lorsqu'on le lance sur un graphique. Il suffit de les modifier dans la fenêtre qui s'ouvre, dans l'onglet Paramètres. Autre détail, de taille ! Tous ces arguments à la fonction ! Ils font double emploi avec les variables prédéfinies, et nous ne les utilisons pas pour l'instant. Nous devons impérativement retourner cette valeur désormais. La fonction iMA nous donne la moyenne mobile, et elle a besoin de nombreux paramètres que nous expliquerons dans un post ultérieur. Cette moyenne, à peine récupérée, elle est mise dans notre buffer: C'est le seul traitement que nous faisons dans cet indicateur, car le but n'est pas de calculer une valeur, mais bien de vérifier si nos fonctions réalisent bien leur travail.

Tester l'initialisation de l'indicateur:


La fonction d'initialisation d'indicateurs effectue deux choses: fixer le nombre de décimales et donner le nom court de l'indicateur. Nous allons passer des valeurs à la fonction, en l'occurrence seulement le nom court, et vérifier que l'indicateur la prend bien en compte. Il faudra fournir ce nom en paramètre externe, c'est-à-dire fixé lors de l'initialisation de l'indicateur dans la fenêtre qui s'ouvre. Et il faudra constater le changement sur l'indicateur. Il faudra aussi vérifier que le nombre de décimales de l'indicateur est bien celui de la paire. La plupart des brokers travaillent avec 5 décimales désormais. On va donc tester sur une paire à 5 décimales très probablement et on le fera sur une paire avec le yen qui n'en a que deux ou trois.
En résumé, nous devons choisir un nom en paramètre externe de l'indicateur et vérifier qu'il apparait bien dans le menu contextuel que nous donne MetaTrader avec un clic droit sur un signal de cet indicateur. Nous devons aussi tester cet indicateur sur un paire sans le Yen et une paire avec le Yen et constater dans l'infobulle de MetaTrader lorsqu'on pointe sur le signal graphique qu'il y a 5 décimales pour une paire sans le Yen et 2 ou 3 décimales pour une paire avec le Yen.
Sans oublier la déclaration du paramètre externe nomCourtIndicateur avec sa valeur par défaut, ni oublier l'inclusion du fichier d'outils Indicateurs, la ligne de code de l'initialisation est simplement dans la fonction  OnInit() :

input string nomCourtIndicateur="indicateur-test";

#include <OutilsIndicateurs.mqh>

int OnInit() {
   initIndicateur(nomCourtIndicateur);
}
Le paramètre externe du nom court de l'indicateur en rouge est déclaré avec une valeur par défaut mais pourra être modifié à la volée lors de l'initialisation de l'indicateur voire même après. Un paramètre externe est simplement une variable que MetaTrader présente dans la fenêtre d'initialisation pour qu'on puisse la modifier quand on utlise le programme. Avec la nouvelle version du langage, elles sont déclarées en tant qu'entrées du programme (input) et non plus comme variables externes. C'est juste un changement de dénomination, ça reste la même chose. Maintenant l'avantage, c'est qu'elles sont colorées en rouge par l'éditeur.

Tester l'initialisation d'un index:


Pour initialiser un index, notre fonction transmet de nombreux paramètres à MetaTrader, dont la plupart en valeur par défaut. Nous transmettrons le numéro de l'index et le tableau qui lui sert de buffer et le simple fait que MetaTrader trace le signal suffit à confirmer que ça fonctionne car ces deux éléments sont primordiaux pour faire fonctionner l'indicateur, sans ça, pas de signal.

Nous utiliserons le décalage traditionnellement appelé shift dans Metatrader en tant que paramètre externe pour le décalage des périodes de l'indicateur par rapport à l'historique du cours. Comme ça nous pourrons le modifier et voir le décalage modifié en direct.
input int shift=0;
Concernant le début du traçage, nous allons aussi utiliser un paramètre externe pour le modifier à la volée, mais il nous faut savoir combien il y a de périodes dans l'historique car nous risquons sinon de chercher longtemps le début de l'historique. Eh oui ! il y en a beaucoup dans l'historique. Nous allons donc faire afficher à l'initialisation le nombre de périodes dans l'historique pour savoir quel nombre mettre afin de voir le début du traçage vers la fin de l'historique:
input int debutTracage=0;
Print("nombre de périodes dans l'historique: ", Bars);
Il y a ensuite le nom donné à l'index, celui-ci apparaitra dans les infobulles de MetaTrader, il suffit de lui en donner un par une constante déclarée au début que nous concatènerons pendant l'initialisation avec le nombre de périodes de calculs de la moyenne elle aussi en paramètre externe (concaténer signifie mettre les chaines de caractères bout à bout):
input int nbPeriodesMoyenne=12;

string NOM_INDEX = "moy-mobile-";

string nomIndex;

dans OnInit() nomIndex = StringConcatenate(NOM_INDEX, nbPeriodesMoyenne);
Puis vient le tour des caractéristiques de traçage, dont la couleur. Première chose, ces caractéristiques sont aussi définies de manière statique par des  #property  au début du code, et c'est l'assitant de création de nouveau fichier qui nous l'a mis. Le problème, c'est qu'avec ces définitions, statiques, nos tentatives de définitions des caractéristiques de manière dynamique dans le code échoueront et même il n'y aura pas de traçage. Nous devons donc supprimer ces définitions statiques car nous les définissons de manière dynamique avec notre fonction d'initialisation d'index. Dans l'appel de la fonction d'initialisation d'index, nous mettons simplement le nom de la couleur nouvellement défini dans la mise à jour du langage: les noms de couleur commencent tous par le préfixe «clr». Ici nous mettons  clrBlue .

La même chose pour le style, le mode de traçage et l'épaisseur du trait qu'on définit en paramètres externes histoire de les changer un peu pour voir le résultat.
input int styleDeTracage=STYLE_SOLID;
input int modeDeTracage=DRAW_LINE;
input int epaisseurDuTrait=EMPTY;

Dernier paramètre et c'est celui qui a le moins de chances d'être modifié (c'est pour ça qu'il est placé en dernier): la valeur signifiant «pas de valeur calculée». C'est la valeur vide de l'indicateur. Quand elle est présente dans une case du tableau d'historique de l'index, elle signifie pour MetaTrader qu'aucune valeur n'est calculée pour cette période, et MetaTrader ne trace rien à cette période. Normalement on y place la valeur  EMPTY_VALUE  mais on peut la modifier. Nous la mettons en paramètre externe, avec sa valeur par défaut, mais lorsque l'indicateur fonctionnera on y placera une valeur particulière qui est utilisée sur le moment dans l'indicateur pour voir que MetaTrader ne trace plus lorsqu'il y a cette valeur. (voir la vidéo pour bien comprendre)
input double valeurNonCalculee=EMPTY_VALUE;

Le tutoriel et le code de test:




//+------------------------------------------------------------------+
//|                                        testsOutilsIndicateurs.mq4|
//|                   Copyright 2014, argent-facile-avec-robots-forex|
//|                     http://argent-facile-robots-forex.blogspot.fr|
//+------------------------------------------------------------------+
#property copyright "Copyright 2014, argent-facile-avec-robots-forex"
#property link   "http://argent-facile-avec-robots-forex.blogspot.fr"
#property version   "1.00"
#property strict
#property indicator_chart_window
#property indicator_buffers 1
#property indicator_plots 1
//--plot moyenne
//#property indicator_label1 "moyenne"
//#property indicator_type2 DRAW_LINE
//#property indicator_color1 clrRed
//#property indicator_style1 STYLE_SOLID
//#property indicator_witdh1 1

//+------------------------------------------------------------------+
//| Tampons d'historique des index                                   |
//+------------------------------------------------------------------+

double bufferMoyenne[];

//+------------------------------------------------------------------+
//| Paramètres externes                                              |
//+------------------------------------------------------------------+

input string nomCourtIndicateur="indicateur-test";
input int shift=0;
input int debutTracage=0;
input int nbPeriodesMoyenne=12;
input int styleDeTracage=STYLE_SOLID;
input int modeDeTracage=DRAW_LINE;
input int epaisseurDuTrait=EMPTY;
input double valeurNonCalculee=EMPTY_VALUE;

//+------------------------------------------------------------------+
//| Includes                                                         |
//+------------------------------------------------------------------+

#include <OutilsIndicateurs.mqh>

//+------------------------------------------------------------------+
//| Constantes globales                                              |
//+------------------------------------------------------------------+

string NOM_INDEX = "moy-mobile-";

//+------------------------------------------------------------------+
//| Variables globales                                               |
//+------------------------------------------------------------------+

string nomIndex;

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+

int OnInit() {
   nomIndex = StringConcatenate(NOM_INDEX, nbPeriodesMoyenne);
   Print("nombre de périodes dans l'historique: ", Bars);
   initIndicateur(nomCourtIndicateur);
   initIndex(0, bufferMoyenne, shift, debutDuTracage, nomIndex, clrBlue,
            styleDeTracage, modeDeTracage, epaisseurDuTrait, valeurNonCalculee);
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| 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[]) {
   double moyenneMobile;
   int noPeriode, noPeriodeDebut;
   int nbPeriodeATraiter = Bars - IndicatorCounted();
   int noPeriodeLimiteCalcul = Bars - nbPeriodesMoyenne - 1;
   if(nbPeriodesATraiter>noPeriodeLimiteCalcul) {
      noPeriodeDebut = noPeriodeLimiteCalcul;
   }
   else { noPeriodeDebut = nbPeriodesATraiter - 1; }
   for(noPeriode=noPeriodeDebut ; noPeriode>=0 ; noPeriode--) {
      moyenneMobile = iMA(NULL, 0, nbPeriodesMoyenne, shift,
                           MODE_SMA, PRICE_MEDIAN, noPeriode);
      bufferMoyenne[noPeriode] = moyenneMobile;
   }
   return(rates_total);
}

//+------------------------------------------------------------------+

vendredi 7 mars 2014

Un fichier include .mqh d'outils pour les indicateurs

Après un fichier de fonctions pour les calculs temporels et les calculs sur les cotations, que nous étofferons par la suite, nous allons faire un fichier pour nous faciliter la tache lors de la création d'indicateurs. Il n'y aura que deux fonctions pour l'instant, très simples algorithmiquement, elles nous soulageront des nombreuses lignes de codes à répéter pour créer des indicateurs. Et les valeurs par défaut nous éviteront de les préciser à chaque fois.

Ce que fait un indicateur:


Un indicateur va chercher les valeurs du cours dans l'historique et fait des calculs pour donner un résultat chiffré par période. Ce résultat doit être stocké dans un historique spécialement créé pour l'indicateur, et MetaTrader se sert de cet historique pour tracer une courbe ou un histogramme correspondant à l'évolution de notre valeur calculée. La courbe ainsi créée se rajoute au cours sur le graphique pour nous donner l'évolution de notre valeur. C'est cette valeur qui va nous donner des indications pour trader, ou bien qui va servir à des experts advisors pour prendre des décisions.

Les initialisations nécessaires


L'historique de cet indicateur est simplement un tableau, dont la taille est gérée par MetaTrader. Nous n'avons qu'à déclarer son existence et le remplir. MetaTrader doit, pour effectuer ce travail, être averti de certaines choses. Pour l'indicateur globalement:
  • Donner un nom à l'indicateur.
  • Fixer la précision de l'indicateur: le nombre de décimales.
Dans chaque indicateur il peut y avoir jusqu'à huit valeurs basées sur huit calculs différents avec chacune leur historique. Chaque valeur calculée avec son historique est appelée un index. Pour chacun des huit index possibles, il faut:
  • Préciser le tableau à utiliser pour l'historique.
  • Fixer le décalage entre les périodes de l'indicateur et celle du cours.
  • Fixer le style graphique pour le traçage de la valeur.
  • Préciser la période de début de traçage.
  • Préciser le label à afficher pour l'index.
  • Préciser la valeur signifiant aucune donnée pour la période.

Une fonction d'initialisation de l'indicateur


Nous devons préciser à MetaTrader le nombre de décimales que l'indicateur doit fournir, en général, le même nombre de décimales que celui de la paire sur laquelle il s'exécute. Nous utilisons donc la fonction  MarketInfo()  qui va nous donner cette information grâce aux paramètres  Symbol()  qui donne le nom de la paire de devises courante. (sur laquelle on est) et  MODE_DIGITS  qui est un code signifiant à la fonction  MarketInfo()  qu'on veut le nombre de décimales (digits). Et ce nombre de décimales est fourni à la fonction  IndicatorDigits()  qui va le fixer pour notre indicateur.

string paireCourante = Symbol();
int nombreDecimales = MarketInfo(paireCourante, MODE_DIGITS);
IndicatorDigits(nombreDecimales);

On doit également donner un nom court à l'indicateur, utilisable par MetaTrader notamment pour l'afficher dans les graphiques et pour être appelé par un expert advisor. Ce nom on le donne à la fonction IndicatorShortName() qui va le fixer pour cet indicateur.

IndicatorShortName(nomCourt);

La fonction d'initialisation de l'indicateur reçoit alors juste le nom court de l'indicateur et exécute les lignes de code précédentes. Elle ne retourne aucune valeur, elle est donc du type void:

void initIndicateur(string nomCourt) {
   string paireCourante = Symbol();
   int nombreDecimales = MarketInfo(paireCourante, MODE_DIGITS);
   IndicatorDigits(nombreDecimales);
   IndicatorShortName(nomCourt);
}

Une fonction d'initialisation d'un index


L'initialisation d'un index doit indiquer le numéro de l'index sur les huit possibles: de 0 à 7. Il faut donc transmettre ce numéro en argument à la fonction et le fournir à toutes les fonctions que nous allons appeler pour paramétrer cet index.
Nous devons déclarer le tableau qui nous sert d'historique pour l'indicateur, il y a en au moins un. Nous pouvons en créer jusqu'à huit par indicateur. Pour ces déclarations, nous ne précisons pas la taille du tableau, MetaTrader s'en chargera. Cette déclaration de tableau sera faite manuellement pour chaque indicateur, car nous ne pouvons pas retourner un tableau avec une fonction, donc pas de déclaration de ces tableaux dans une fonction. Nous devrons par contre passer le tableau à la fonction et par référence (pas de copie) pour qu'elle puisse passer cette référence à MetaTrader qui saura alors quel tableau utiliser pour l'historique. C'est la fonction SetIndexBuffer() qui transmet la référence.

SetIndexBuffer(noIndex, tamponHistorique);

Nous devons ensuite régler le décalage de l'indicateur par rapport aux périodes des courts. C'est-à-dire le décalage voulu entre la valeur calculée pour une période et la période qu'elle représente. Normalement il n'y en a pas, on calcule une valeur pour une période et cette valeur ressort quand on demande cette période précisément. Mais on peut vouloir décaler toutes les valeurs de chaque période pour avancer ou reculer le signal par rapport aux cotations de la paire. Ce décalage est fourni en argument, sans valeur par défaut, car elle peut être modifiée à chaque initialisation de l'indicateur. On doit donc la transmettre à chaque fois:

SetIndexShift(noIndex, decalagePeriodes);

Le calcul d'une valeur d'index pour une période, demande presque tout le temps d'utiliser les cours antérieurs à la période calculée. Or pour la toute première période et les quelques suivantes de l'historique, il n'y a pas suffisamment de cotations antérieures pour faire ce calcul. On ne doit donc pas tracer l'indicateur pour ces toutes premières périodes car s'il on y calcule des valeurs, elles ne seront pas bonnes par manque de cotations antérieures. On peut préciser à MetaTrader de ne pas tracer l'index pour un certain nombre de périodes au début de l'historique:

SetIndexDrawBegin(noIndex, debutTracage);

Pour afficher le nom de l'index lorsqu'on pointe dessus avec la souris, ou pour avoir un intitulé dans la fenêtre de description, on peut assigner un label à un index:

SetIndexLabel(noIndex, intitule);

Ensuite, on peut, et c'est conseillé de donner un style de traçage à l'index. On peut modifier
  • le mode (ligne, section, histogramme, flèches, zigzag, remplissage ou aucun traçage).
  • le style du tracé (continu, traits interrompus, points, trait-point et trait-point-point).
  • l'épaisseur du trait en pixel (de 1 à 5) ou EMPTY pour aucun changement d'épaisseur.
  • la couleur (clrNONE pour aucun changement de couleur).

SetIndexStyle(noIndex, modeTracage, style, epaisseur, couleur);

Pour finir, il faut indiquer la valeur vide à utiliser pour signaler à MetaTrader qu'il n'y a pas de valeur calculée dans une période, lui indiquant ainsi qu'il ne faut pas tracer pour cette période. Cette valeur vide par défaut pour chaque index est  EMPTY_VALUE . On la reçoit en argument avec cette valeur par défaut.

SetIndexEmptyValue(noIndex, valeurVide);

La fonction d'initialisation de l'index reçoit tous ces paramètres et exécute les lignes de code précédentes. Elle ne retourne aucune valeur, elle est donc du type void:

void initIndex(int noIndex, double &tamponHistorique[], int decalagePeriodes,
               int debutTracage=0, string intitule="",
               color couleur=clrNONE, int style=STYLE_SOLID,
               int modeTracage=DRAW_LINE, int epaisseur=EMPTY,
               double valeurVide=EMPTY_VALUE) {
   SetIndexBuffer(noIndex, tamponHistorique);
   SetIndexShift(noIndex, decalagePeriodes);
   SetIndexDrawBegin(noIndex, debutTracage);
   SetIndexLabel(noIndex, intitule);
   SetIndexStyle(noIndex, modeTracage, style, epaisseur, couleur);
   SetIndexEmptyValue(noIndex, valeurVide);
}

Le tutoriel vidéo et le code complet de l'include




Voici le fichier include «OutilsIndicateurs.mqh» que nous créons dans sa première version:

//+------------------------------------------------------------------+
//|                                            OutilsIndicateurs.mqh |
//|                  Copyright 2014, argent-facile-avec-robots-forex |
//|               http://argent-facile-avec-robots-forex.blogspot.fr |
//+------------------------------------------------------------------+
#property copyright "Copyright 2014, argent-facile-avec-robots-forex"
#property  link  "http://argent-facile-avec-robots-forex.blogspot.fr"
#property  strict
//+------------------------------------------------------------------+
//| Defines                                                          |
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//| Constantes                                                       |
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//| Fonctions d'initialisation                                       |
//+------------------------------------------------------------------+

void initIndicateur(string nomCourt) {
   string paireCourante = Symbol();
   int nombreDecimales = MarketInfo(paireCourante, MODE_DIGITS);
   IndicatorDigits(nombreDecimales);
   IndicatorShortName(nomCourt);
}

void initIndex(int noIndex, double &tamponHistorique[], int decalagePeriodes,
               int debutTracage=0, string intitule="",
               color couleur=clrNONE, int style=STYLE_SOLID,
               int modeTracage=DRAW_LINE, int epaisseur=EMPTY,
               double valeurVide=EMPTY_VALUE) {
   SetIndexBuffer(noIndex, tamponHistorique);
   SetIndexShift(noIndex, decalagePeriodes);
   SetIndexDrawBegin(noIndex, debutTracage);
   SetIndexLabel(noIndex, intitule);
   SetIndexStyle(noIndex, modeTracage, style, epaisseur, couleur);
   SetIndexEmptyValue(noIndex, valeurVide);
}

//+------------------------------------------------------------------+
Et voilà de quoi nous soulager pour programmer des indicateurs ! Nous allons évidemment tester ce code dès le prochain post pour garantir que le tout fonctionne !

vendredi 28 février 2014

Création d'un fichier include .mqh d'arithmétique des cotations


L'objectif:


Nous avons commencé la création de fichiers include (.mqh) pour y mettre des fonctions réutilisables et dans lesquelles on peut avoir confiance car les fonctions sont testées et nous n'avons plus qu'à nous concentrer sur notre code principal et plus sur les petits détails. Ces fonctions pour le premier include concernent les calculs sur le temps: c'est le fichier «ArithmetiqueTemporelle.mqh», et nous y avons mis quatre fonctions pour l'instant. Nous allons maintenant attaquer un fichier include pour l'arithmétique des cotations: «ArithmetiqueCotations.mqh». Nous y mettrons dès le présent post, des fonctions de moyennes et d'écarts.
  • obtenirCoursReelActuel()
  • obtenirCoursMedian()
  • obtenirCoursPondere()
  • obtenirSpreadActuel()
Nullement besoin d'expliquer longtemps car les noms sont assez expressifs. Je vais tout de même donnez des détails techniques sur ces fonctions.

Une fonction de contrôle du décalage de l'historique:


Nous aurons besoin pour deux des fonctions de cet include, d'un code de contrôle du décalage dans l'historique. Nous allons passer à deux des fonctions, un nombre entier qui représente un numéro de période en remontant dans l'historique. Mais ce numéro ne doit pas être négatif, ni être supérieur ou égal au nombre de périodes dans l'historique. Le nombre de périodes de l'historique est stocké dans la variable prédéfinie  Bars , et l'historique va de la période n°0 (celle en cours) à la période n°Bars-1 (la plus vieille de l'historique). On va donc créer cette fonction qui reçoit le décalage à contrôler et retourne une valeur booléenne pour dire s'il est valide ou non. Le code de cette fonction est simplement un test de ces conditions dont le résultat vrai ou faux est stockée dans une variable booléenne. Si le décalage est non valide, on affiche le message d'erreur avec le nom de la fonction reçu aussi en argument et la valeur erronée. Ensuite la valeur du test est retournée au code appelant:


bool leDecalageEstIlValide(int decalage, string nomFonction) {
   bool decalageEstValide = (decalage >= 0) && (decalage < Bars);
   if(!decalageEstValide) { Alert(nomFonction, msgErrDecalageNonValide, decalage); }
   return(decalageEstValide);
}

obtenirCoursReelActuel():


Il s'agit ici d'obtenir le dernier cours réel estimé reçu par MetaTrader. Je fais la simple moyenne du Bid et du Ask, car le cours réel est grosso modo au milieu des deux. Ceci est en conflit avec les cotations dans l'historique qui ne concernent que le Bid. Si on veut juste le dernier Bid reçu, pas besoin de fonction: il suffit simplement d'utiliser la variable prédéfinie  Bid  ! Et c'est bien plus court :) Pour cette fonction nous n'avons aucun paramètre à lui fournir et nous demandons qu'une seule chose: le dernier cours reçu. Elle utilise les deux variables prédéfinies  Bid  et  Ask , les additionne et divise par deux la somme. Et il n'y a qu'à retourner ce résultat. Elle est très courte:

double obtenirCoursReelActuel() {
   double coursReelActuel = (Bid + Ask) / 2.0;
   return(coursReelActuel);
}

obtenirCoursMedian():


Par cours médian, les traders comprennent la moyenne du plus bas et du plus haut de la période. Nous voulons ici, non pas avoir le tout dernier cours médian reçu, cela n'a aucun sens, mais avoir le cours médian d'une période dans l'historique. Elle peut être n'importe laquelle de l'historique ou bien celle qui est en cours et qui n'est pas encore finie, et dans ce cas le cours médian évolue en temps réel, au fur et à mesure que les cotations arrivent. Dès que la période est finie, ce cours est fixé et ne changera plus pour cette période. Elle était numérotée 0, et devient d'un coup, la numéro 1 en remontant l'historique.
Il faut au moins un paramètre: le décalage à faire dans l'historique pour tomber sur la période voulue. 0 étant la période actuelle. C'est un nombre entier positif ou nul qui grandit en remontant l'historique des périodes. Ce décalage doit aussi être inférieur au nombre de périodes contenues dans l'historique, et ce nombre est stocké dans la variable prédéfinie  Bars . La numérotation des périodes de l'historique allant de 0 à  Bars-1 . On doit alors tester que ce paramètre ne soit pas négatif et aussi qu'il ne soit pas strictement supérieur à  Bars . S'il ne l'est pas, on peut récupérer le  High[]  et le  Low[]  de la période. (ce sont des tableaux dont l'indice est le décalage pour remonter l'historique.) On additionne ces deux valeurs et on divise par deux. Si le décalage est  négatif ou bien supérieur à  Bars , on ne peut pas faire ça, et l'alerte avec un message d'erreur et la valeur erronée a déjà été levée dans la fonction de test du décalage. On doit alors retourner un code d'erreur. Ce code d'erreur est placé dès le départ, à l'initialisation de la variable à retourner, et si le calcul normal est fait, ce code est écrasé par le résultat que la fonction doit retourner. Il ne reste plus qu'à retourner cette variable:

double obtenirCoursMedian(int decalage) {
   double coursMedian = ERREUR_DOUBLE;
   bool decalageEstValide = leDecalageEstIlValide(decalage, nomObtenirCoursMedian);
   if(decalageEstValide) { coursMedian = (High[decalage] + Low[decalage]) / 2.0; }
   return(coursMedian);
}

obtenirCoursPondere():


Par pondéré, j'entends pondéré par les quatre valeurs de cotations de chaque période: le plus haut, le plus bas, l'ouverture et la fermeture. Cette valeur que j'appelle pondérée est plus précise que la valeur médiane si on vise la valeur moyenne de la période. (les traders ont l'habitude de n'utiliser que la valeur médiane en cherchant à avoir une pseudo-valeur moyenne !). J'utilise et vous propose un calcul plus proche de la vraie valeur moyenne. Pour une vraie moyenne, il faudrait avoir toutes les valeurs du cours de la période, or il n'en reste que quatre. Autant faire le calcul avec ces quatre, ce sera plus proche de la vraie moyenne. (Les traders utilisent même des moyennes pondérées par une valeur psychologique: il mettent 2 fois le close, en pensant que cette valeur à plus d'importance parce qu'elle clôt la période et donne le sens d'évolution des cours de la période. Ce qui est totalement faux, car le close d'une période est juste à côté de l'open de la suivante, et n'a pas de position privilégiée. La preuve, si on décale l'horloge d'une seconde, l'open devient le close de la période précédente et le close n'est plus considérée car oubliée comme la plupart des valeurs de la période. Autant faire le trading avec un gri-gri pour porter chance =D En fait seul les chandeliers journaliers pour les actions ont un close qui a une signification plus importante car à la fin de la journée de trading il y a en général une impulsion plus forte qui donne un mouvement représentatif de toute la séance et de plus il y a une coupure temporelle entre un close journalier et l'open suivant provoquant une psychologie de la foule des traders différente pour le chandelier suivant puisque demain est un autre jour, ce qui n'est pas le cas pour le forex qui ne ferme pas la nuit, mais seulement en fin de semaine. On observe tout de même une psychologie de marché sur le forex différente d'un jour à l'autre.)
Sur le même modèle que la fonction précédente, on fait le calcul avec les quatre valeurs  High[], Low[], Open[] et Close[] . On les additionne et on divise par quatre. Même algorithme que la précédente:
  • on reçoit en argument qui doit être positif ou nul, le décalage dans l'historique.
  • on initialise la variable qui contiendra le résultat avec le code d'erreur
  • on teste la validité du décalage reçu en argument
  • s'il l'est, on fait le calcul qu'on met dans la variable en écrasant le code d'erreur
  • on retourne le contenu de la variable.

double obtenirCoursPondere(int decalage) {
   double coursPondere = ERREUR_DOUBLE;
   bool decalageEstValide = leDecalageEstIlValide(decalage, nomObtenirCoursPondere);
   if(decalageEstValide) {
      coursPondere = (High[decalage]+Low[decalage]+Open[decalage]+Close[decalage]) / 4.0;
   }
   return(coursPondere);
}

obtenirSpreadActuel():


Contrairement aux trois précédentes fonctions qui étaient des moyennes, le spread est un écart: la différence entre le Bid et le Ask. C'est la marge du broker proportionnelle au capital mis en jeu dans la position ouverte. Il est important d'avoir ce spread, car lorsqu'on ouvre une position, il faut impérativement fermer lorsque le cours a eu une variation supérieure au spread. Eh oui, car le broker prend la valeur du spread quoi qu'il arrive, et donc il nous faut un écart de cours supérieur au spread pour être gagnant (et que le cours ait varié dans le bon sens, ça c'est l'évidence !) Étant donné que l'historique ne contient pas les valeurs Ask, nous ne pouvons le calculer que sur la dernière cotation reçue.
C'est une fonction très simple, sans paramètre, car il n'y a pas de choix à faire: le spread de la dernière cotation reçue est le spread actuel, rien d'autre ! On fait donc la différence entre les variables prédéfinies:  Ask - bid  et on retourne ce résultat !

double obtenirSpreadActuel() {
   double spread = Ask - Bid;
   return(spread);
}

Les constantes de messages:


Pour les besoins de ces fonctions, nous devons écrire quelques constantes de message d'erreur et des noms de fonctions:

string nomObtenirCoursReelActuel = "obtenirCoursReelActuel()";
string nomObtenirCoursMedian = "obtenirCoursMedian()";
string nomObtenirCoursPondere = "obtenirCoursPondere()";
string nomObtenirSpreadActuel = "obtenirSpreadActuel()";

string msgErrDecalageNonValide = " a reçu un argument de
// ligne coupée pour des raisons de largeur du post, ne pas la couper:
   décalage historique négatif ou alors supérieur à Bars: ";

Modification de l'include UtilitairesTests.mqh:


Pour les besoins des tests, nous allons améliorer notre fichier include d'utilitaires des tests et rectifier un petit bug insignifiant. Première chose, nous allons mutualiser des constantes de chaines de texte pour ne pas avoir à les écrire à chaque fois et pour centraliser les données. La première sert à faire un bilan de test par fonction, les deux autres pour servir de séparateurs dans l'affichage du journal pour s'y retrouver plus facilement. Ces constantes sont:

string msgBilanTestFonction = " a terminé ses tests avec un total d'erreurs de ";
// la nouvelle version de l'éditeur qui vient d'arriver cette semaine:
// nous permet d'ouvrir une chaine avec un type de guillemet (simple ou double)
// pour la refermer avec le même et d'insérer l'autre type de guillemet sans
// l'antislash pour échapper la signification de fermeture de chaine.
string SEPARATEUR = "-------------------------------------------------------";
string SEPARATEUR_DOUBLE = "================================================";
Nous rectifions également dans ce fichier une fonction d'analyse de résultat de test: celle du type double. En effet, lors de l'affichage de l'erreur, le dernier paramètre passé à la fonction  Print()  est le nombre décimal tel quel ! Or si la fonction  Print()  l'accepte sans problème, elle tronque tout de même les décimales à la cinquième et arrondi. Vous verriez sans la modification suivante qu'un arrondi et non la valeur exacte de notre constante  ERREUR_DOUBLE  avec 10 décimales. C'est un problème mineur d'affichage qui ne gène pas le fonctionnement du programme. La modification est la suivante: on ne transmet pas la variable resultat directement à la fonction  Print() , mais à une fonction de conversion  DoubleToStr()  qui comme son nom le suggère, va nous prendre un type   double  pour en faire un   string  qui est le type normalement demandé par la fonction  Print() . L'avantage est qu'elle demande le nombre de décimales à fournir en second paramètre. La ligne à modifier est celle du deuxième  Print() :

Print(nomFonction, msgErreur, parametresCas, DoubleToStr(resultatTest, 10));

Les tests de ces fonctions:


Nous réalisons immédiatement le test des fonctions sans attendre dans le fichier d'un expert advisor nommé testsArithmetiqueCotations.mq4. Il n'y aura pas grand chose à tester d'autant plus que la plupart des valeurs utilisées par ces fonctions sont récupérées dans les variables prédéfinies. Mais nous faisons les tests par principe, même si certains sont pour le moment pas très utiles. Du moins on a l'impression que certains seront inutiles, mais à l'avenir, si on modifie une fonction, il y aura déjà le test auquel on ajoute des cas. En plus, même s'il ne teste pas la réaction de la fonction face à un paramètre, il permet d'affirmer qu'il n'y a pas d'erreur de frappe et que le code fonctionne.

obtenirCoursReelActuel()
n°casrésultat attenducommentaires
1(Bid+Ask)/2.0étant donné que le résultat attendu est calculé avec les même valeurs que celles prises par la fonction, ils doit concorder avec celui de la fonction. Ce test est utile pour prouver qu'il n'y a pas d'erreur de syntaxe et que le code fonctionne bien qu'il soit minimaliste.

obtenirCoursMedian()
n°casdecalagerésultat attenducommentaires
10(High[0]+Low[0]) /2.0Période actuelle. Dans tous les cas nous ne pouvons pas prévoir le résultat du calcul, d'où le même calcul que dans la fonction en résultat attendu.
210(High[10]+Low[10]) /2.0Idem.
3Bars-1(High[Bars-1]+Low[Bars-1]) /2.0Décalage valide car strictement inférieur à Bars, c'est la première période chronologique contenue dans l'historique.
4-10ERREUR_DOUBLEDécalage négatif, donc alerte levée et retour d'un code d'erreur.
5BarsERREUR_DOUBLEDécalage égal à Bars, donc alerte levée et retour d'un code d'erreur.
61 000 000ERREUR_DOUBLEDécalage supérieur à Bars, donc alerte levée et retour d'un code d'erreur.

obtenirCoursPondere()
n°casdecalagerésultat attenducommentaires
10(High[0]+Low[0]+ Open[10]+Close[10]) /4.0Période actuelle. Dans tous les cas nous ne pouvons pas prévoir le résultat du calcul d'où le même calcul que dans la fonction en résultat attendu.
210(High[10]+Low[10]+ Open[10]+Close[10]) /4.0Idem.
3Bars-1(High[Bars-1]+Low[Bars-1]+ Open[Bars-1]+Close[Bars-1]) /4.0Décalage valide car strictement inférieur à Bars, c'est la première période chronologique contenue dans l'historique.
4-10ERREUR_DOUBLEDécalage négatif, donc alerte levée et retour d'un code d'erreur.
5BarsERREUR_DOUBLEDécalage égal à Bars, donc alerte levée et retour d'un code d'erreur.
61 000 000ERREUR_DOUBLEDécalage supérieur à Bars, donc alerte levée et retour d'un code d'erreur.

obtenirSpreadActuel()
n°casrésultat attenducommentaires
1Ask-Bidétant donné que le résultat attendu est calculé avec les même valeurs que celles prises par la fonction, il doit concorder avec celui de la fonction. Ce test est utile pour prouver qu'il n'y a pas d'erreur de syntaxe et que le code fonctionne bien qu'il soit minimaliste.

Il y a quelques différences avec le premier module de tests réalisé précédemment. Premièrement les valeurs de résultats sont du type  double  au lieu de  datetime . Il faut les changer tous dans la valeur de retour des fonctions d'exécution et leurs variables resultat à l'intérieur et dans les tableaux des résultats attendus de chaque fonction de test.
Ensuite, la fonction du cours actuel et celle du spread actuel n'ont qu'un seul test, mais surtout elle ne prennent aucun argument. Nous laissons pour elles, le code de test sous forme d'une fonction de test et d'une fonction d'exécution car à l'avenir, il est possible d'ajouter un paramètre optionnel à ces deux fonctions et donc d'avoir besoin de la fonction d'exécution. Le code de la fonction d'exécution pour ces deux-là change: la variable résultat prend une valeur par défaut qui est  ERREUR_DOUBLE . Dans ces deux fonctions d'exécution nous réalisons l'exécution de la fonction testée que si l'argument est la constante  ARG_STRING_EMPTY  (l'argument est du type  string  car s'il venait à y avoir un argument optionnel à la fonction testée, ce serait une chaine de caractères pour désigner la paire de devises).
Il y a aussi les fonctions de tests des deux autres, qui ont un argument mais obligatoire, donc dans ces deux cas l'exécution de la fonction testée ne se fait que si l'argument est différent de  ARG_INT_EMPTY  et il n'y a pas de cas alternatif sans argument, juste une clause  if  comme pour les deux précédentes.
Puis pour les fonctions de cours médian et de cours pondéré, les arguments  Bars  et  Bars-1  ne peuvent être mis dans la déclaration du tableau avec les autres valeurs car le compilateur, malgré sa version nouvelle, n'accepte toujours pas une variable lors de la déclaration d'un tableau. Nous devons donc y mettre une valeur, par exemple  1  et les remplacer desuite après par  Bars  et  Bars-1 .
Enfin nous ajoutons pour une question de lisibilité des résultats de tests, un  Print()  bilan du nombre d'erreurs de la fonction testée à la fin de chaque fonction de test, et encadré par deux  Print()  de lignes de séparation. Pour une question de code clair et non redondant, nous plaçons ces trois lignes de code dans une fonction spéciale dans le fichier UtilitairesTests.mqh. On la nomme afficherBilanTestFonction() et on l'appellera en une ligne en lui passant le nom de la fonction testée et le nombre d'erreurs du test. Voici son code:

void afficherBilanTestFonction(string nomFonction, int nbErreurs) {
   Print(SEPARATEUR);
   Print(nomFonction, msgBilanTestFonction, nbErreurs);
   Print(SEPARATEUR_DOUBLE);
}
Pour rappel les constantes des noms de fonctions sont désormais définies dans les fichiers où les fonctions sont codées, et non plus dans le fichier de tests. Le code correspondant à tout ça est donné en intégralité à la fin du post.

Le tutoriel vidéo et les trois codes complets:




//+------------------------------------------------------------------+
//|                                             UtilitairesTests.mqh |
//|                  Copyright 2014, argent-facile-avec-robots-forex |
//|               http://argent-facile-avec-robots-forex.blogspot.fr |
//+------------------------------------------------------------------+
#property copyright "Copyright 2014, argent-facile-avec-robots-forex"
#property link   "http://argent-facile-avec-robots-forex.blogspot.fr"

//+------------------------------------------------------------------+
//| defines                                                          |
//+------------------------------------------------------------------+

#define  ARG_STRING_EMPTY "-#-EMPTY-#-"
#define  ARG_INT_EMPTY 2147483647
#define  ARG_BOOL_EMPTY -1
#define  ARG_DOUBLE_EMPTY 123456789.123456789
#define  ARG_DATETIME_EMPTY 4294967295
#define  ARG_COLOR_EMPTY 4294967295

#define MESSAGE_ERREUR " a produit une erreur de résultat avec le cas "
#define MESSAGE_SUCCES " a passé le test sans erreurs "

//+------------------------------------------------------------------+
//| constantes de tests                                              |
//+------------------------------------------------------------------+

string msgBilanTestFonction = " a terminé ses tests avec un total d'erreurs de ";
string SEPARATEUR = "-------------------------------------------------------";
string SEPARATEUR_DOUBLE = "================================================";

//+------------------------------------------------------------------+
//| Fonctions d'affichage                                            |
//+------------------------------------------------------------------+

void afficherBilanTestFonction(string nomFonction, int nbErreurs) {
   Print(SEPARATEUR);
   Print(nomFonction, msgBilanTestFonction, nbErreurs);
   Print(SEPARATEUR_DOUBLE);
}

//+------------------------------------------------------------------+
//| Fonctions d'analyse                                              |
//+------------------------------------------------------------------+

int analyserResultatDatetimeDuTest(
         datetime resultatTest, datetime resultatAttendu,
         string nomFonction, string parametresCas,
         string msgErreur = MESSAGE_ERREUR, string msgSucces = MESSAGE_SUCCES) {
   int nbErreurs = 0;
   if(resultatTest == resultatAttendu) {
      Print(nomFonction, msgSucces, resultatTest);
   } else {
      Print(nomFonction, msgErreur, parametresCas, resultatTest);
      nbErreurs++;
   }
   return(nbErreurs);
}

int analyserResultatIntDuTest(
         int resultatTest, int resultatAttendu,
         string nomFonction, string parametresCas,
         string msgErreur = MESSAGE_ERREUR, string msgSucces = MESSAGE_SUCCES) {
   int nbErreurs = 0;
   if(resultatTest == resultatAttendu) {
      Print(nomFonction, msgSucces, resultatTest);
   } else {
      Print(nomFonction, msgErreur, parametresCas, resultatTest);
      nbErreurs++;
   }
   return(nbErreurs);
}

int analyserResultatBoolDuTest(
         bool resultatTest, bool resultatAttendu,
         string nomFonction, string parametresCas,
         string msgErreur = MESSAGE_ERREUR, string msgSucces = MESSAGE_SUCCES) {
   int nbErreurs = 0;
   if(resultatTest == resultatAttendu) {
      Print(nomFonction, msgSucces, resultatTest);
   } else {
      Print(nomFonction, msgErreur, parametresCas, resultatTest);
      nbErreurs++;
   }
   return(nbErreurs);
}

int analyserResultatDoubleDuTest(
         double resultatTest, double resultatAttendu,
         string nomFonction, string parametresCas,
         string msgErreur = MESSAGE_ERREUR, string msgSucces = MESSAGE_SUCCES) {
   int nbErreurs = 0;
   if(resultatTest == resultatAttendu) {
      Print(nomFonction, msgSucces, resultatTest);
   } else {
      Print(nomFonction, msgErreur, parametresCas, DoubleToStr(resultatTest, 10));
      nbErreurs++;
   }
   return(nbErreurs);
}

int analyserResultatStringDuTest(
         string resultatTest, string resultatAttendu,
         string nomFonction, string parametresCas,
         string msgErreur = MESSAGE_ERREUR, string msgSucces = MESSAGE_SUCCES) {
   int nbErreurs = 0;
   if(resultatTest == resultatAttendu) {
      Print(nomFonction, msgSucces, resultatTest);
   } else {
      Print(nomFonction, msgErreur, parametresCas, resultatTest);
      nbErreurs++;
   }
   return(nbErreurs);
}

int analyserResultatColorDuTest(
         color resultatTest, color resultatAttendu,
         string nomFonction, string parametresCas,
         string msgErreur = MESSAGE_ERREUR, string msgSucces = MESSAGE_SUCCES) {
   int nbErreurs = 0;
   if(resultatTest == resultatAttendu) {
      Print(nomFonction, msgSucces, resultatTest);
   } else {
      Print(nomFonction, msgErreur, parametresCas, resultatTest);
      nbErreurs++;
   }
   return(nbErreurs);
}

//+------------------------------------------------------------------+



//+------------------------------------------------------------------+
//|                                        ArithmetiqueCotations.mqh |
//|                  Copyright 2014, argent-facile-avec-robots-forex |
//|               http://argent-facile-avec-robots-forex.blogspot.fr |
//+------------------------------------------------------------------+
#property copyright "Copyright 2014, argent-facile-avec-robots-forex"
#property  link  "http://argent-facile-avec-robots-forex.blogspot.fr"
#property  strict

//+------------------------------------------------------------------+
//| Defines                                                          |
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//| Constantes                                                       |
//+------------------------------------------------------------------+

string nomObtenirCoursReelActuel = "obtenirCoursReelActuel()";
string nomObtenirCoursMedian = "obtenirCoursMedian()";
string nomObtenirCoursPondere = "obtenirCoursPondere()";
string nomObtenirSpreadActuel = "obtenirSpreadActuel()";

string msgErrDecalageNonValide = " a reçu un argument de décalage
ligne coupée pour des raisons de largeur de post ! Recollez-là
 historique négatif ou alors supérieur à Bars: ";

//+------------------------------------------------------------------+
//| Fonctions de contrôle                                            |
//+------------------------------------------------------------------+

bool leDecalageEstIlValide(int decalage, string nomFonction) {
   bool decalageEstValide = (decalage >= 0) && (decalage < Bars);
   if(!decalageEstValide) { Alert(nomFonction, msgErrDecalageNonValide, decalage); }
   return(decalageEstValide);
}

//+------------------------------------------------------------------+
//| Fonctions de Moyennes                                            |
//+------------------------------------------------------------------+

double obtenirCoursReelActuel() {
   double coursReelActuel = (Bid + Ask) / 2.0;
   return(coursReelActuel);
}

double obtenirCoursMedian(int decalage) {
   double coursMedian = ERREUR_DOUBLE;
   bool decalageEstValide = leDecalageEstIlValide(decalage, nomObtenirCoursMedian);
   if(decalageEstValide) { coursMedian = (High[decalage] + Low[decalage]) / 2.0; }
   return(coursMedian);
}

double obtenirCoursPondere(int decalage) {
   double coursPondere = ERREUR_DOUBLE;
   bool decalageEstValide = leDecalageEstIlValide(decalage, nomObtenirCoursPondere);
   if(decalageEstValide) {
      coursPondere = (High[decalage]+Low[decalage]+Open[decalage]+Close[decalage]) /4.0;
   }
   return(coursPondere);
}

//+------------------------------------------------------------------+
//| Fonctions d'écarts                                               |
//+------------------------------------------------------------------+

double obtenirSpreadActuel() {
   double spread = Ask - Bid;
   return(spread);
}

//+------------------------------------------------------------------+


//+------------------------------------------------------------------+
//|                                   testsArithmetiqueCotations.mq4 |
//|                  Copyright 2014, argent-facile-avec-robots-forex |
//|               http://argent-facile-avec-robots-forex.blogspot.fr |
//+------------------------------------------------------------------+
#property copyright "Copyright 2014, argent-facile-avec-robots-forex"
#property  link  "http://argent-facile-avec-robots-forex.blogspot.fr"
#property  version   "1.00"
#property  strict
//+------------------------------------------------------------------+
//| Includes                                                         |
//+------------------------------------------------------------------+

#include <Constantes.mqh>
#include <UtilitairesTests.mqh>
#include <ArithmetiqueCotations.mqh>

//+------------------------------------------------------------------+
//| Defines                                                          |
//+------------------------------------------------------------------+

#define NB_CAS_COURS_REEL_ACTUEL 1
#define NB_CAS_COURS_MEDIAN 6
#define NB_CAS_COURS_PONDERE 6
#define NB_CAS_SPREAD_ACTUEL 1

//+------------------------------------------------------------------+
//| Fonctions de tests                                               |
//+------------------------------------------------------------------+

double execObtenirCoursReelActuel(string argument) {
   double resultat = ERREUR_DOUBLE;
   if(argument == ARG_STRING_EMPTY) { resultat = obtenirCoursReelActuel(); }
   return(resultat);
}

int testObtenirCoursReelActuel() {
   int noCas, nbErreurs = 0;
   double resultat;
   string arguments[NB_CAS_COURS_REEL_ACTUEL] = {ARG_STRING_EMPTY};
   string parametresCas[NB_CAS_COURS_REEL_ACTUEL] = {" => "};
   double resultatsAttendus[NB_CAS_COURS_REEL_ACTUEL];
   resultatsAttendus[0] = (Bid + Ask) / 2.0;
   for(noCas=0 ; noCas<NB_CAS_COURS_REEL_ACTUEL ; noCas++) {
      resultat = execObtenirCoursReelActuel(arguments[noCas]);
      nbErreurs += analyserResultatDoubleDuTest(resultat, resultatsAttendus[noCas],
                                        nomObtenirCoursReelActuel, parametresCas[noCas]);
   }
   afficherBilanTestFonction(nomObtenirCoursReelActuel, nbErreurs);
   return(nbErreurs);
}

double execObtenirCoursMedian(int argument) {
   double resultat = ERREUR_DOUBLE;
   if(argument != ARG_INT_EMPTY) { resultat = obtenirCoursMedian(argument); }
   return(resultat);
}

int testObtenirCoursMedian() {
   int noCas, nbErreurs = 0;
   double resultat;
   int arguments[NB_CAS_COURS_MEDIAN] = {0, 10, 1, -10, 1, 1000000};
   arguments[2] = Bars-1;
   arguments[4] = Bars;
   string parametresCas[NB_CAS_COURS_MEDIAN] = {" 0 => "," 10 => "," Bars-1 => ",
                                                " -10 => "," Bars => "," 1000000 => "};
   double resultatsAttendus[NB_CAS_COURS_MEDIAN];
   resultatsAttendus[0] = (High[0] + Low[0]) / 2.0;
   resultatsAttendus[1] = (High[10] + Low[10]) / 2.0;
   resultatsAttendus[2] = (High[Bars-1] + Low[Bars-1]) / 2.0;
   resultatsAttendus[3] = ERREUR_DOUBLE;
   resultatsAttendus[4] = ERREUR_DOUBLE;
   resultatsAttendus[5] = ERREUR_DOUBLE;
   for(noCas=0 ; noCas<NB_CAS_COURS_MEDIAN ; noCas++) {
      resultat = execObtenirCoursMedian(arguments[noCas]);
      nbErreurs += analyserResultatDoubleDuTest(resultat, resultatsAttendus[noCas],
                                        nomObtenirCoursMedian, parametresCas[noCas]);
   }
   afficherBilanTestFonction(nomObtenirCoursMedian, nbErreurs);
   return(nbErreurs);
}

double execObtenirCoursPondere(int argument) {
   double resultat = ERREUR_DOUBLE;
   if(argument != ARG_INT_EMPTY) { resultat = obtenirCoursPondere(argument); }
   return(resultat);
}

int testObtenirCoursPondere() {
   int noCas, nbErreurs = 0;
   double resultat;
   int arguments[NB_CAS_COURS_PONDERE] = {0, 10, 1, -10, 1, 1000000};
   arguments[2] = Bars-1;
   arguments[4] = Bars;
   string parametresCas[NB_CAS_COURS_PONDERE] = {" 0 => "," 10 => "," Bars-1 => ",
                                                " -10 => "," Bars => "," 1000000 => "};
   double resultatsAttendus[NB_CAS_COURS_MEDIAN];
   resultatsAttendus[0] = (High[0] + Low[0] + Open[0] + Close[0]) / 4.0;
   resultatsAttendus[1] = (High[10] + Low[10] + Open[10] + Close[10]) / 4.0;
   resultatsAttendus[2] = (High[Bars-1] + Low[Bars-1] + Open[Bars-1] + Close[Bars-1]) / 4.0;
   resultatsAttendus[3] = ERREUR_DOUBLE;
   resultatsAttendus[4] = ERREUR_DOUBLE;
   resultatsAttendus[5] = ERREUR_DOUBLE;
   for(noCas=0 ; noCas<NB_CAS_COURS_PONDERE ; noCas++) {
      resultat = execObtenirCoursPondere(arguments[noCas]);
      nbErreurs += analyserResultatDoubleDuTest(resultat, resultatsAttendus[noCas],
                                        nomObtenirCoursPondere, parametresCas[noCas]);
   }
   afficherBilanTestFonction(nomObtenirCoursPondere, nbErreurs);
   return(nbErreurs);
}

double execObtenirSpreadActuel(string argument) {
   double resultat = ERREUR_DOUBLE;
   if(argument == ARG_STRING_EMPTY) { resultat = obtenirSpreadActuel(); }
   return(resultat);
}

int testObtenirSpreadActuel() {
   int noCas, nbErreurs = 0;
   double resultat;
   string arguments[NB_CAS_SPREAD_ACTUEL] = {ARG_STRING_EMPTY};
   string parametresCas[NB_CAS_SPREAD_ACTUEL] = {" => "};
   double resultatsAttendus[NB_CAS_SPREAD_ACTUEL];
   resultatsAttendus[0] = Ask - Bid;
   for(noCas=0 ; noCas<NB_CAS_SPREAD_ACTUEL ; noCas++) {
      resultat = execObtenirSpreadActuel(arguments[noCas]);
      nbErreurs += analyserResultatDoubleDuTest(resultat, resultatsAttendus[noCas],
                                        nomObtenirSpreadActuel, parametresCas[noCas]);
   }
   afficherBilanTestFonction(nomObtenirSpreadActuel, nbErreurs);
   return(nbErreurs);
}

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+

int OnInit() {
   int nbErreurs = 0;
   nbErreurs += testObtenirCoursReelActuel();
   nbErreurs += testObtenirCoursMedian();
   nbErreurs += testObtenirCoursPondere();
   nbErreurs += testObtenirSpreadActuel();
   Print(SEPARATEUR_DOUBLE);
   Print(" TESTS DE ArithmetiqueCotations.mqh TERMINES AVEC ", nbErreurs, " ERREUR(S).");
   Print(SEPARATEUR_DOUBLE);
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+

void OnDeinit(const int reason) {
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+

void OnTick() {
}

//+------------------------------------------------------------------+

Vous pouvez constater qu'il y a des différences avec les fichiers précédemment réalisés. Il y a deux  #property  en plus au début du fichier: un qui donne la version de l'expert qu'on créé et un autre qui dit que nos lignes de code sont en version stricte. (on pourrait utiliser des variantes de code qui s'éloignent du standard!) Il y a un autre changement de taille: le nom des fonctions prédéfinies: elles ne s'appelent plus  init() ,  deinit()  et  start() , mais  OnInit() ,  OnDeinit()  et  OnTick() . Ce nouveau nommage est compatible avec MQL5, et correspond au nommage standard dans tous les langages de programmation pour des fonctions déclenchées sur des évènements (initialisation de l'expert, arrêt de l'expert avec d'ailleurs un paramètre entier fourni par MetaTrader4 lors de l'appel pour indiquer la raison de l'arrêt et l'arrivée d'un tick, d'où un nom plus cohérent avec ce qu'il représente: OnTick = SurUnTick). Il y a aussi les valeurs des retours: celle de la fonction d'initialisation est indiquée par une constante  INIT_SUCCEEDED  qui signifie succès d'initialisation et qui vaut zéro comme la valeur qu'on retournait avant pour dire qu'il n'y a pas eu d'erreur. Pour les deux autres fonctions, apparemment nous ne sommes plus obligés de retourner une valeur, car il n'y a pas d'instruction  return  préparées et surtout leur type de retour est  void , donc ne pas retourner de valeur.
C'est un post marathon qui se termine, ouf ! Ça nous fait quatre nouvelles fonctions de calcul dans un nouveau fichier header (include) et des modfications dans l'include des utilitaires de tests et un expert de tests pour ces quatre fonctions. Comme vous pouvez le constater, nous accélérons le rythme pour la création de code utilitaire pour nos futurs indicateurs et experts advisors. Avec la nouvelle mise à jour de cette semaine, MetaTrader4, MetaEditor et son aide sont traduits en français, la compatibilité avec le MQL5 est améliorée, et l'éditeur nous propose une nouvelle ergonomie. Nous ne pouvons plus taper les noms prédéfinis sans la majuscule et attendre que l'autocomplétion nous propose un nom, maintenant il faut mettre la majuscule pour que l'éditeur le reconnaisse et propose l'autocomplétion (une liste de possibilités). C'est dommage, mais en contrepartie, l'autocomplétion fonctionne désormais avec nos propres noms: toutes nos variables et fonctions, et ça c'est bien mieux ! La raison est qu'on peut maintenant inclure des fonctions du langage C. Par exemple, si vous ne mettez pas la majuscule à Print(), l'éditeur vous propose en autocomplétion printf(), une fonction du langage C, nos formatages de chaines à afficher en seraient facilités. Nous l'utiliserons peut-être à l'avenir. En tout cas, bon courage pour étudier et pratiquer tout ce qu'on vient de voir, et à la prochaine !