vendredi 21 février 2014

Le numéro d'une période (bar) en MQL4 partie 1 / 2

Metatrader nous met à disposition une variable prédéfinie, bien pratique: Bars. Cette variable disponible n'importe où dans le code contient le nombre de périodes actuellement dans l'historique. Si on a besoin de parcourir l'historique, il faut l'utiliser pour connaître le nombre de périodes dans cet historique. Elle pose cependant un problème. J'ai vu des codes utilisant la variable prédéfinie Bars comme indicateur de changement de période. Et je l'ai même, au début, utilisée pour établir un numéro temporel à chaque période comme si c'était un numéro de référence temporelle absolu. J'ai vite déchanté.

Le problème de la variable prédéfinie Bars est qu'elle ne représente que le nombre de périodes (bar) dans l'historique. Et ce nombre varie avec la taille de l'historique, qui elle dépend de la gestion interne de Metatrader. C'est-à-dire que ce nombre ne va pas forcément augmenter. Par expérience personnelle, je peux dire qu'il faut faire attention, et notamment quand on utilise ce nombre pour déterminer un numéro de période où pour déterminer s'il y a une nouvelle période ( Bars n'augmente pas forcément ). Pour déterminer un numéro de période, il faut se baser sur les horodatages, ceux-ci proviennent du serveur et sont associés aux données du cours et indissociables de ces cotations. A chaque tick qu'on reçoit, il s'agit surtout de lire la cotation, mais cette information, le serveur la fournie liée à un horodatage indissociable. La cotation a une position temporelle bien définie, sinon elle ne veux rien dire du tout.

Le principe du calcul:


Prenons par exemple une période qui durerait 5 secondes, cela signifierait que toutes les 5 secondes il y aurait une nouvelle période. C'est-à-dire que les secondes dont l'horodatage serait 0, 1, 2, 3 et 4 appartiennent à la période n°0, que les secondes n° 5, 6, 7, 8 et 9 appartiennent à la période n° 1, et que les secondes n° 10, 11, 12, 13 et 14 appartiennent à la période n° 2, etc. Vous constatez que dans les numéros des secondes de la période n°0, il y a 0 fois le nombre 5. On ne peut pas retirer le nombre 5 de ces numéros. Pour ceux de la période n°1, on peut par contre retirer 1 fois le nombre 5: 5-5=0, 6-5=1, 7-5=2, 8-5=3 et 9-5=4. Et 2 fois le nombre 5 pour ceux de la période n°2: 10-2x5=0, 11-2x5=1, 12-2x5=2, 13-2x5=3 et 14-2x5=4. Arithmétiquement c'est faire une division euclidienne (entière) c'est-à-dire une division dont on ne prend que la partie entière du résultat, soit le nombre de fois qu'on peut retirer le diviseur entier (les résultats des soustractions précédentes sont les restes de ces divisions, c'est ce qu'il reste qui n'est plus assez grand pour y retirer encore le diviseur). Faisons les divisions décimales et voyons:
  • 15=0,2 dont la partie entière est 0.
  • 25=0,4 dont la partie entière est 0.
  • 45=0,8 dont la partie entière est 0.
  • 55=1 dont la partie entière est 1.
  • 75=1,4 dont la partie entière est 1.
  • 95=1,8 dont la partie entière est 1.
  • 105=2 dont la partie entière est 2.
  • 115=2,2 dont la partie entière est 2.
  • 375=5,4 dont la partie entière est 5.
  • 3785=75,6 dont la partie entière est 75.
Les parties entières de ces divisions sont donc les numéros des périodes, et de manière absolue si on utilise les horodatages fournis par le serveur. Et les numéros de périodes seront donc très grands car les horodatages le sont encore plus. Évidemment ces numéros de périodes dépendent de la taille desdites périodes, et c'est normal car depuis le 1er janvier 1970 il y a plus de périodes de 60 secondes que de périodes à 300 secondes (Cinq minutes). Si on divise par 300, nous obtiendrons évidemment des numéros plus petits.

La fonction MathFloor():


Certains langages de programmation proposent d'obtenir directement la partie entière de la division euclidienne et d'autres non. Ici ont peut utiliser deux méthodes:
  1. Approche triviale: division de nombres décimaux avec pour résultat un nombre décimal dont on pourra extraire la partie entière avec la fonction MathFloor().
  2.  Utilisation du casting implicite par une division entre nombres entiers donnant un résultat entier.
On voit donc de suite l'approche que je qualifie de triviale car c'est celle qui vient à l'esprit en premier (la deuxième ne m'est venue que récemment). Quand on fait une division d'un nombre par un autre on s'attend en général à obtenir un nombre avec des décimales. D'où la logique qui vient à l'esprit du programmeur d'utiliser une fonction arithmétique très souvent disponible:  MathFloor() . On passe en paramètre à cette fonction un nombre avec des décimales et elle nous retourne le même nombre sans les décimales. Donc pour notre problème, il suffit de faire la division de l'horodatage du tick, qui est le nombre de secondes depuis le 1er janvier 1970, par le nombre de secondes dans la période. On obtient alors un nombre fractionnaire qu'on passe à la fonction pour avoir en retour ce même nombre débarrassé de ses décimales, et qui se trouve donc être le numéro de la période depuis le 1er janvier 1970. Du coup ce numéro est vraiment une référence qui ne risque pas de varier pour ce tick-là, à la différence de Bars. Les lignes de code pour ceci:

double noPeriodeFractionnaire = horodatage / nbSecondesDansPeriode;
int noPeriode = MathFloor(noPeriodeFractionnaire);


divisions entre entiers:


Quand on a fait le code précédent, on a fait deux erreurs car on a oublié deux choses que le langage MQL4 fait et qui, ici se compensent:
  1. La fonction  MathFloor()  prend un nombre décimal en paramètre du genre 12.3 ou 4.157 mais retourne aussi un nombre décimal, c'est-à-dire que le résultat qu'elle retourne est du genre 12.0 ou 4.0 (type double). Or nous voulons 12 ou 4, des nombres entiers (type int).
  2. Le langage MQL4 réalise un cast implicite. Le cast est un changement de type de données opéré par le compilateur qui rajoute des instructions pour le processeur que nous n'avons pas mises et cela pour convertir le type de donnée. Dans notre cas, quand nous déclarons une variable de type int et que nous lui demandons d'y mettre la valeur 4.0 du type double, le compilateur va se débrouiller pour convertir le type et le 4.0 devient un 4. Mais ! En y réfléchissant bien, la fonction  MathFloor()  ne sert à rien ! On aurait tout aussi bien pu faire la division et demander en même temps de stocker le résultat dans une variable entière ce qui aurait provoqué la suppression des décimales ! Au final le cast implicite nous sauve la mise car nous avons comme résultat un nombre décimal et qu'on ne voulait pas ça, et que le cast convertit dans le bon type. On va continuer dans la réflexion et ceux qui l'ont testé le savent: les opérations aussi dépendent des types. Lors d'une opération arithmétique avec MQL4, s'il n'y a que des nombres entiers le résultat est entier et dès qu'un nombre décimal est en jeu, le résultat est décimal. Il y a un cast implicite aussi pour les opérations arithmétiques. Quand nous divisons deux nombres entiers, le résultat sera entier: 72=3 à la différence de la même division avec au moins un nombre décimal: 72.0=3.5 qui donne un résultat décimal. Or la division qu'on souhaite faire se fait entre deux nombres entiers ! Conclusion la fonction d'extraction de partie entière ne sert à rien ! La ligne de code que nous devons mettre est simplement
    
    int noPeriode = horodatage / nbSecondesDansPeriode;
    
    
Je me suis sérieusement fait avoir par ce genre de choses: j'avais un code assez copieux, qu'il a fallu tout décortiquer pour trouver le problème. Et c'était une division qui me donnait toujours zéro au lieu d'un nombre fractionnaire entre 0 et 1. =D Vous comprenez l'origine du bug ! Prévu initialement en un seul post, ça commence à faire long, je vais donc vous proposer la suite au post suivant. Prenez le temps d'assimiler ce qu'on vient de voir. A chaque jour suffit sa peine.

mercredi 19 février 2014

Gestion des valeurs erronées des arguments en MQL4

Nous avons vu avec les tests des trois premières fonctions du fichier include d'arithmétique temporelle qu'il pouvait y avoir des valeurs reçues en argument auxquelles aucune signification n'est attachée. Par exemple, pour obtenir le nombre de secondes dans une période, nous recevons en argument un nombre entier qui donne le nombre de minutes de la période. Mais ce nombre entier fourni pourrait être négatif ou nul et la fonction fournirait quand même un résultat. Mais un nombre de minutes dans une période négatif ou nul n'a aucune signification et il s'agit clairement d'une valeur erronée. Et le résultat fourni par la fonction n'a pas plus de sens. Que faire dans ce cas-là, parce que ces valeurs peuvent arriver, ne serait-ce que par un bug de l'expert advisor qui appelle la fonction. Nous allons réfléchir à ce problème dans ce post, et proposer une modification de code.

Contrôler la validité des arguments:


Nous ne pouvons clairement pas laisser les fonctions donner un résultat qui n'a aucun sens et qui induirait des bugs dans le programme. Il faut constamment contrôler les valeurs pour détecter les problèmes. Donc quand une fonction reçoit une valeur pour faire un calcul ou un traitement quelconque, nous devons vérifier que nous n'allons pas provoquer un problème ou propager une erreur. Une chose est sûre, il ne faut pas retourner un résultat qui n'a pas de sens et qui provoquerait des erreurs. Imaginez par exemple un nombre de secondes dans la période qui serait négatif, nous ferions des calculs ensuite avec et nous aurons forcément un résultat faux et notre programme pourrait envoyer un ordre de position sur le marché alors qu'il ne faudrait surtout pas, et nous faire perdre du capital. Nous allons donc introduire une condition dans la fonction: nous ferons le calcul que si l'argument est valide et nous déclencherons une alerte avec un message d'erreur dans le cas contraire. Nous plaçons ce test de validité après le contrôle de la valeur vide, car il peut y avoir interférence par recouvrement entre la valeur vide (valeur par défaut) et les valeurs erronées.

Retourner un code d'erreur:


Une fois que nous avons vérifié que l'argument est non valide et que nous avons déclenché une alerte, nous ne pouvons pas retourner un résultat comme si de rien n'était. Parce que le code qui a appelé la fonction va recevoir ce résultat et l'utiliser. Il faut un moyen pour l'avertir qu'il n'y a pas de résultat et qu'il y a un problème et qu'il ne doit pas continuer à faire son traitement normal. Nous n'avons qu'un seul moyen de l'avertir, c'est la valeur de retour. Il faut donc trouver une valeur dans chaque cas qui n'est pas utilisée, pour signifier qu'il y a eu une erreur. Souvent le résultat de retour est toujours positif, on peut alors utiliser les valeurs négatives, ou selon le cas une valeur nulle. Si la valeur de retour est du type string nous pouvons utiliser une chaîne de caractères très spécifique. Nous pouvons utiliser la valeur -1 pour les booléens, car elle est acceptée et compatible, et surtout non utilisée (faux=0 et vrai=1). Pour le type color nous pouvons utiliser une valeur sur quatre octets (-1 où tous les bits sont à 1) car seuls trois octets sont utilisés (un par composante primaire rouge, vert et bleu) et les valeurs entières sont acceptées. La valeur 0 peut signifier pour un horodatage qu'il y a une erreur, car elle ne sera pas utilisée parce qu'il n'y avait pas le forex actuel le 1er janvier 1970. C'est seulement à partir de 1973, que les monnaies ont été flottantes. Pour les valeurs décimales, il va falloir trouver un nombre très particulier qui ne sera pas utilisé. Pour les double il y a un problème, ce sont les possibilités d'écriture d'un nombre décimal avec MQL4 qui ne permet que les chiffres et le point décimal. Les valeurs possibles peuvent être très grandes et impossible à écrire de cette manière puisqu'il nous faudrait écrire un nombre de 308 chiffres pour écrire le nombre maximum. Il nous faudrait à la place une notation exponentielle qui n'existe pas en MQL4. Nous allons mettre -9876543210.0123456789 car il y a peu de chances de l'utiliser et la probabilité de tomber pile sur celle-la dans un calcul est extrêmement faible. Pour le type int il y a deux cas à distinguer: si la valeur retournée n'est jamais négative, on peut utiliser -1. Si la valeur retournée peut être négative (signée, car affectée d'un signe) -1 ne convient pas, alors on va utiliser une autre valeur: la plus grande possible pour un nombre entier positif qui a très peu de probabilités d'être utilisée. Cette valeur, pour un nombre sur 4 octets, est précisément 2 147 483 647.

Un fichier include des constantes d'erreurs


Nous allons utiliser ces constantes d'erreurs dans tous les codes que nous allons faire. Nous mettrons donc ces constantes dans un fichier header .mqh à inclure en premier dans les Experts Advisors et les indicateurs. Nommé «Contantes.mqh», en voici le code complet:

//+------------------------------------------------------------------+
//|                                                   Constantes.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"

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

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

int      ERREUR_INT_POSITIF   = -1;
int      ERREUR_INT_SIGNE     = 2147483647;
bool     ERREUR_BOOL          = -1;
datetime ERREUR_DATETIME      = 0;
double   ERREUR_DOUBLE        = -9876543210.0123456789;
color    ERREUR_COLOR         = -1;
string   ERREUR_STRING        = "-#-ERREUR-#-";

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

Ce fichier header est bien évidemment à inclure, mais au début du fichier principal de notre programme, et il sera valable pour tous les includes qui utilisent ces constantes mais qui ne l'incluent pas. On ne peut pas l'inclure dans chaque fichier qui l'utilise, car avec les inclusions multiples, ce qu'il contient serait alors répété plusieurs fois dans le code, ce que le compilateur n'accepterait pas forcément.

Ajout du test de validité dans les fonctions:


La valeur  EMPTY  est représentée par la constante  -1 , or les nombres négatifs sont non valides. Donc notre valeur par défaut provoquera une erreur. Il faut donc vérifier notre valeur par défaut avant, et la changer pour la valeur de la période courante avant de tester la validité. Puis nous testons la validité et exécutons le calcul seulement si elle est valide. Dans le cas contraire nous affichons une alerte avec la valeur reçue. Le test de validité est fait hors du  if  et est stocké dans une variable booléenne juste avant pour une question de lisibilité de l'algorithme. Le nommage des variables nous permet dévoiler nos intentions dans l'algorithme. J'aurais pu placer ce test directement dans le  if . Nous mettons la valeur d'erreur  ERREUR_INT_POSITIF  par défaut à l'initialisation de la variable qui contient la valeur retournée, comme ça lorsque le calcul n'est pas fait, le code d'erreur à retourner est déjà présent dans la variable de retour. Et si le calcul est fait, son résultat écrasera le code d'erreur. Les horodatages, n'ont pas de valeurs invalides car toutes positives et on toutes une signification, mais quelques valeurs ont très peu de probabilité d'être utilisées. On ne teste pas la validité des horodatages reçus en argument. On remarque tout de même qu'il y a un défaut à cette manière de faire, c'est le recouvrement des valeurs. Si le code produit une valeur erronée qui est exactement -1 et la passe en argument à une de nos fonction, cette valeur erronée sera considérée comme une demande d'utiliser la période courante. Il faut garder ceci à l'esprit, et il n'est pas bon de laisser un bug potentiel ! Il faut y remédier. Dans l'immédiat on déplace le problème en mettant la constante  NULL  en valeur par défaut et dans le test correspondant. Le problème n'est pas réglé mais la valeur de recouvrement est située dans la partie positive et n'est pas utilisée. Le code appelant la fonction peut toujours produire une erreur en mettant en argument la valeur 0 qui sera prise comme une demande d'utiliser la période courante. Pour régler ce problème il faudrait séparer les demandes: une fonction pour la période courante sans argument et une fonction généraliste qui reçoit un argument. Je fais le choix de garder une unique fonction, bien que j'ai sûrement tord :( Voici le code des deux fonctions modifiées avec la définition des constantes chaînes des messages d'erreur dans la section constantes globales à ne pas oublier:


string msgErrNbMinsNonValide = " a reçu un nombre de minutes non valide: ";
string msgErrNbSecsNonValide = " a reçu un nombre de secondes non valide: ";

int obtenirNbSecsDsPeriode(int nbMinsDansPeriode=NULL) {
   int nbSecsDsPeriode = ERREUR_INT_POSITIF;
   if(nbMinsDansPeriode == NULL) { nbMinsDansPeriode = Period(); }
   bool nbMinsEstValide = (nbMinsDansPeriode > 0);
   if(nbMinsEstValide) { nbSecsDsPeriode = 60 * nbMinsDansPeriode; }
   else { Alert("ObtenirNbSecsDsPeriode() ", msgErrNbMinsNonValide, nbMinsDansPeriode); } 
   return(nbSecsDsPeriode);
}

int obtenirNoSecDsPeriode(datetime horodatageSec, int nbSecsDansPeriode=NULL) {
   int noSecDansPeriode = ERREUR_INT_POSITIF;
   if(nbSecsDansPeriode == NULL) { nbSecsDansPeriode = obtenirNbSecsDsPeriode(); }
   bool nbSecsEstValide = (nbSecsDansPeriode > 0);
   if(nbSecsEstValide) { noSecDansPeriode = horodatageSec % nbSecsDansPeriode; }
   else { Alert("ObtenirNoSecDsPeriode() ", msgErrNbSecsNonValide, nbSecsDansPeriode); }
   return(noSecDansPeriode);
}


Modification des cas de tests:


Nous avons changé le comportement des fonctions vis à vis de valeurs reçues, il faut changer les cas de tests en conséquence. Pour la fonction obtenirNoDerniereSecConnue(), il n'y a pas de problèmes, d'ailleurs elle n'a pas été modifiée. Pour la fonction obtenirNbSecsDsPeriode() on modifie:
  • le résultat attendu pour l'argument -2 qui est désormais ERREUR_INT_POSITIF
  • le résultat attendu pour l'argument 0 qui est la même valeur que le premier cas.

   resultatsAttendus[3] = ERREUR_INT_POSITIF;
   resultatsAttendus[4] = 60 * Period();

Et Pour la fonction obtenirNoSecDsPeriode(), nous ajoutons ces deux cas:
  • arguments 127 et 0 pour un résultat attendu identique au premier cas
  • arguments 127 et -4 pour un résultat attendu à ERREUR_INT_POSITIF.


                                                        127,
                                                        127};


                                                                 0,
                                                                 -4};


                                                     " 127, 0 => ",
                                                     " 127, -4 => "};


   resultatsAttendus[5] = 127 % (60 * Period());
   resultatsAttendus[6] = ERREUR_INT_POSITIF;

Et on n'oublie surtout pas d'ajouter l'include des constantes en haut du fichier ! Et de changer le nombre de cas de tests de 5 à 7 dans la définition de la constante ! =D

#include <Constantes.mqh>

#define NB_CAS_NO_SEC_IN_PERIOD 7


Le tutoriel vidéo et le code des deux fichiers modifiés :



Le fichier include des constantes créé dans ce post est déjà donné plus haut en intégralité. Ici, seulement les deux fichiers modifiés. J'ai ajouté un affichage d'une ligne de séparation pour bien distinguer l'annonce du total des erreurs à la fin des tests.

//+------------------------------------------------------------------+
//|                                       ArithmetiqueTemporelle.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                                                          |
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//| DLL imports                                                      |
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//| EX4 imports                                                      |
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//| includes                                                         |
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//| variables globales                                               |
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//| constantes globales                                              |
//+------------------------------------------------------------------+

string msgErrNbMinsNonValide = " a reçu un nombre de minutes non valide: ";
string msgErrNbSecsNonValide = " a reçu un nombre de secondes non valide: ";

//+------------------------------------------------------------------+
//| fonctions comptage                                               |
//+------------------------------------------------------------------+

datetime obtenirNoDerniereSecConnue(string paire="NULL") {
   datetime horodatage;
   if(paire=="NULL") { horodatage = TimeCurrent(); }
   else { horodatage = MarketInfo(paire, MODE_TIME); }
   return(horodatage);
}

int obtenirNbSecsDsPeriode(int nbMinsDansPeriode=NULL) {
   int nbSecsDsPeriode = ERREUR_INT_POSITIF;
   if(nbMinsDansPeriode == NULL) { nbMinsDansPeriode = Period(); }
   bool nbMinsEstValide = (nbMinsDansPeriode > 0);
   if(nbMinsEstValide) { nbSecsDsPeriode = 60 * nbMinsDansPeriode; }
   else { Alert("ObtenirNbSecsDsPeriode() ", msgErrNbMinsNonValide, nbMinsDansPeriode); } 
   return(nbSecsDsPeriode);
}

int obtenirNoSecDsPeriode(datetime horodatageSec, int nbSecsDansPeriode=NULL) {
   int noSecDansPeriode = ERREUR_INT_POSITIF;
   if(nbSecsDansPeriode == NULL) { nbSecsDansPeriode = obtenirNbSecsDsPeriode(); }
   bool nbSecsEstValide = (nbSecsDansPeriode > 0);
   if(nbSecsEstValide) { noSecDansPeriode = horodatageSec % nbSecsDansPeriode; }
   else { Alert("ObtenirNoSecDsPeriode() ", msgErrNbSecsNonValide, nbSecsDansPeriode); }
   return(noSecDansPeriode);
}

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



//+------------------------------------------------------------------+
//|                                  testsArithmetiqueTemporelle.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"

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

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

#define NB_CAS_LAST_KNOWN_SEC_NO 5
#define NB_CAS_NB_SECS_IN_PERIOD 5
#define NB_CAS_NO_SEC_IN_PERIOD 7

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

string nomObtenirNoDerniereSecConnue = "obtenirNoDerniereSecConnue()";
string nomObtenirNbSecsDsPeriode = "obtenirNbSecsDsPeriode()"
string nomObtenirNoSecDsPeriode = "obtenirNoSecDsPeriode()"

//+------------------------------------------------------------------+
//| fonctions de tests                                               |
//+------------------------------------------------------------------+

datetime execObtenirNoDerniereSecConnue(string argument) {
   datetime resultat;
   if(argument == ARG_STRING_EMPTY) { resultat = obtenirNoDerniereSecConnue(); }
   else { resultat = obtenirNoDerniereSecConnue(argument); }
   return(resultat);
}

int testObtenirNoDerniereSecConnue() {
   int noCas, nbErreurs = 0;
   datetime resultat;
   string arguments[NB_CAS_LAST_KNOWN_SEC_NO] = {ARG_STRING_EMPTY,
                                                 "EURUSD",
                                                 "GBPJPY",
                                                 "EUR",
                                                 ""};
   string parametresCas[NB_CAS_LAST_KNOWN_SEC_NO] = {" sans paramètre => ",
                                                     " \"EURUSD\" => ",
                                                     " \"GBPJPY\" => ",
                                                     " \"EUR\" => ",
                                                     " \"\" => "};
   datetime resultatsAttendus[NB_CAS_LAST_KNOWN_SEC_NO];
   resultatsAttendus[0] = TimeCurrent();
   resultatsAttendus[1] = MarketInfo("EURUSD", MODE_TIME);
   resultatsAttendus[2] = MarketInfo("GBPJPY", MODE_TIME);
   resultatsAttendus[3] = 0;
   resultatsAttendus[4] = 0;
   for(noCas=0 ; noCas<NB_CAS_LAST_KNOWN_SEC_NO ; noCas++) {
      resultat = execObtenirNoDerniereSecConnue(arguments[noCas]);
      nbErreurs += analyserResultatDatetimeDuTest(resultat,
                                                   resultatsAttendus[noCas],
                                                   nomObtenirNoDerniereSecConnue,
                                                   parametresCas[noCas]);
   }
   return(nbErreurs);
}

int execObtenirNbSecsDsPeriode(int argument) {
   int resultat;
   if(argument == ARG_INT_EMPTY) { resultat = obtenirNbSecsDsPeriode(); }
   else { resultat = obtenirNbSecsDsPeriode(argument); }
   return(resultat);
}

int testObtenirNbSecsDsPeriode() {
   int noCas, nbErreurs = 0;
   int resultat;
   int arguments[NB_CAS_NB_SECS_IN_PERIOD] = {ARG_INT_EMPTY, 5, 240, -2, 0};
   string parametresCas[NB_CAS_NB_SECS_IN_PERIOD] = {" sans paramètres => ",
                                                     " 5 => ",
                                                     " 240 => ",
                                                     " -2 => ",
                                                     " 0 => "};
   int resultatsAttendus[NB_CAS_NB_SECS_IN_PERIOD];
   resultatsAttendus[0] = 60 * Period();
   resultatsAttendus[1] = 300;
   resultatsAttendus[2] = 14400;
   resultatsAttendus[3] = ERREUR_INT_POSITIF;
   resultatsAttendus[4] = 60 * Period();
   for(noCas=0 ; noCas<NB_CAS_NB_SECS_IN_PERIOD ; noCas++) {
      resultat = execObtenirNbSecsDsPeriode(arguments[noCas]);
      nbErreurs += analyserResultatIntDuTest(resultat,
                                             resultatsAttendus[noCas],
                                             nomObtenirNbSecsDsPeriode,
                                             parametresCas[noCas]);
   }
   return(nbErreurs);
}

int execObtenirNoSecDsPeriode(datetime argumentHorodatage, int argumentNbSecsDsPeriode) {
   int resultat;
   if(argumentNbSecsDsPeriode == ARG_INT_EMPTY) {
      resultat = obtenirNoSecDsPeriode(argumentHorodatage); }
   else { resultat = obtenirNoSecDsPeriode(argumentHorodatage, argumentNbSecsDsPeriode); }
   return(resultat);
}

int testObtenirNoSecDsPeriode() {
   int noCas, nbErreurs = 0;
   int resultat;
   datetime argumentsHorodatages[NB_CAS_NO_SEC_IN_PERIOD] = {127,
                                                        123456789,
                                                        123456789,
                                                        123456789,
                                                        127,
                                                        127,
                                                        127};
   int argumentsNbSecsDsPeriode[NB_CAS_NO_SEC_IN_PERIOD] = {ARG_INT_EMPTY,
                                                                 ARG_INT_EMPTY,
                                                                 240,
                                                                 900,
                                                                 60,
                                                                 0,
                                                                 -4};
   string parametresCas[NB_CAS_NO_SEC_IN_PERIOD] = {" 127, EMPTY => ",
                                                     " 123456789, EMPTY => ",
                                                     " 123456789, 240 => ",
                                                     " 123456789, 900 => ",
                                                     " 127, 60 => ",
                                                     " 127, 0 => ",
                                                     " 127, -4 => "};
   int resultatsAttendus[NB_CAS_NO_SEC_IN_PERIOD];
   resultatsAttendus[0] = 127 % (60 * Period());
   resultatsAttendus[1] = 123456789 % (60 * Period());
   resultatsAttendus[2] = 69;
   resultatsAttendus[3] = 189;
   resultatsAttendus[4] = 7;
   resultatsAttendus[5] = 127 % (60 * Period());
   resultatsAttendus[6] = ERREUR_INT_POSITIF;
   for(noCas=0 ; noCas<NO_CAS_NB_SEC_IN_PERIOD ; noCas++) {
      resultat = execObtenirNoSecsDsPeriode(argumentsHorodatages[noCas],
                                            argumentsNbSecsDsPeriode[noCas]);
      nbErreurs += analyserResultatIntDuTest(resultat,
                                             resultatsAttendus[noCas],
                                             nomObtenirNoSecDsPeriode,
                                             parametresCas[noCas]);
   }
   return(nbErreurs);
}

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

int init()
  {
   int nbErreurs = 0;
   nbErreurs += testObtenirNoDerniereSecConnue();
   nbErreurs += testObtenirNbSecsDsPeriode();
   nbErreurs += testObtenirNoSecDsPeriode();
   Print("-------------------------------------------------------------------");
   Print("TESTS DE ArithmetiqueTemporelle.mqh TERMINES AVEC ", nbErreurs, " ERREUR(S).");
   return(0);
  }

//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+

int deinit()
  {
   return(0);
  }

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+

int start()
  {
   return(0);
  }

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


vendredi 31 janvier 2014

Code de tests des fonctions de nombre et numéro de secondes dans les périodes

Je l'avais dit: «il faut tester impérativement le code des fonctions des bibliothèques avant de les utiliser dans des projets.» et nous le faisons dès à présent.

Factorisation des messages par défaut avec deux directives #define:


La première fonction que nous avons réalisé pour le type datetime contient des messages par défaut, il faut les mutualiser pour ne pas les répéter dans le code. Mais nous ne pouvons pas employer de constantes, car le compilateur les refuse pour une déclaration d'argument de fonction. Nous allons alors utiliser deux directives préprocesseur #define. Nous ajoutons donc ces deux lignes dans la section define du fichier UtilitairesTests.mqh:

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

Il faut ensuite mettre ces étiquettes aux endroits où il faut que ces chaines de caractères se trouvent. Donc dans la liste des arguments de la fonction analyserResultatDatetimeDuTest() du même fichier nous remplaçons les chaines de caractères et leurs guillemets par les étiquettes MESSAGE_ERREUR et MESSAGE_SUCCES. Il faut bien comprendre que cela ne change rien pour le compilateur qui lui verra le code comme si on n'avait fait aucune modification. En effet le préprocesseur passe avant le compilateur et remplace l'étiquette par les caractères qu'on a mis après l'étiquette sur la ligne du #define. Donc le compilateur verra à la place des étiquettes dans le code les chaines que nous avons voulu mutualiser. Et les lignes de directives #define ne seront plus là. Voici le code de la fonction:

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


Une fonction d'analyse de résultat de test pour le type int:


Maintenant occupons-nous de l'analyse du résultat de test, s'il est du type entier (int). En effet, nous avons une fonction qui analyse le résultat d'un test seulement si c'est un horodatage (datetime) et ne peut en analyser de types différents car on doit passer ce résultat en argument de la fonction, et que cet argument doit avoir un type bien précis. On va donc créer la même fonction mais pour le type entier où datetime est remplacé par int y compris dans le nom et avec une majuscule pour la lisibilité (sinon il y aurait deux fonctions du même nom ce que le compilateur refuse et on ne pourrait pas les distinguer). Voici le code de cette fonction:

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

Et pourquoi attendre ? Dans la foulée, on écrit (ou plutôt copie et colle, avec trois modifications pour chacune) les mêmes fonctions pour les types bool, double, string et color. Je vous donne le code complet du fichier à la fin du post. (Ces codes complets sont juste là pour vous donner une référence, un moyen de contrôle sur vos écritures, il faudrait pour bien apprendre que vous fassiez l'effort d'écrire vous même le code.)

Le test de ObtenirNbSecsDsPeriode():


On peut donc maintenant écrire du code pour tester notre deuxième fonction du fichier include, celle qui donne le nombre de secondes dans une période. Il y a peu de cas à tester, ce sera assez rapide, car cette fonction prend un entier positif qu'on lui fournit et le multiplie par 60. Si on ne lui donne pas, elle le récupère sur la période courante. On va donc tester deux cas positifs, un cas négatif, un cas nul et un cas sans argument. Ce sera largement suffisant. Normalement, aucun nombre négatif n'est sensé être passé à cette fonction, mais ça ne l'empêchera pas de donner un résultat. Nous n'avons pas fait de code pour lever une erreur si l'argument venait à être négatif, ou bien fournir une valeur par défaut si l'argument n'est pas bon. C'est à réfléchir. Nous verrons ça plus après. Il n'y a qu'un seul argument à la fonction à tester, donc un code d'exécution binaire simple: avec ou sans l'argument. On va donc coder nos deux fonctions: une pour l'exécution avec ou sans argument et l'autre pour faire tous les cas de test et faire analyser les résultat. On n'oublie pas non plus d'ajouter un #define pour le nombre de cas de test et la constante qui contient le nom de la fonction. On change évidemment les types des résultats et des arguments. Et on ajoute une ligne exécutant la fonction de test dans la fonction init(), sinon, pas de test ! Et bien sûr, on change les valeurs des arguments et résultats pour les différents cas. Voici les cas traités:
  1. Sans argument, le résultat attendu est le nombre de secondes de la période courante, soit  60 * Period() .
  2. Argument 5 (timeframe M5), le résultat attendu est 5*60=300.
  3. Argument 240 (timeframe H4), le résultat attendu est 240*60=14 400.
  4. Argument -2 (ne doit normalement pas arriver, sauf bug), résultat attendu -2*60=-120.
  5. Argument 0 (no doit normalement pas arriver, sauf bug), résultat attendu 0*60=0.
Voici les lignes de codes à ajouter à l'expert de test de l'include (testArithmetiqueTemporelle.mq4):

#define NB_CAS_NB_SECS_IN_PERIOD 5

string nomObtenirNbSecsDsPeriode = "obtenirNbSecsDsPeriode()"

int execObtenirNbSecsDsPeriode(int argument) {
   int resultat;
   if(argument == ARG_INT_EMPTY) { resultat = obtenirNbSecsDsPeriode(); }
   else { resultat = obtenirNbSecsDsPeriode(argument); }
   return(resultat);
}

int testObtenirNbSecsDsPeriode() {
   int noCas, nbErreurs = 0;
   int resultat;
   int arguments[NB_CAS_NB_SECS_IN_PERIOD] = {ARG_INT_EMPTY, 5, 240, -2, 0};
   string parametresCas[NB_CAS_NB_SECS_IN_PERIOD] = {" sans paramètres => ",
                                                     " 5 => ",
                                                     " 240 => ",
                                                     " -2 => ",
                                                     " 0 => "};
   int resultatsAttendus[NB_CAS_NB_SECS_IN_PERIOD];
   resultatsAttendus[0] = 60 * Period();
   resultatsAttendus[1] = 300;
   resultatsAttendus[2] = 14400;
   resultatsAttendus[3] = -120;
   resultatsAttendus[4] = 0;
   for(noCas=0 ; noCas<NB_CAS_NB_SECS_IN_PERIOD ; noCas++) {
      resultat = execObtenirNbSecsDsPeriode(arguments[noCas]);
      nbErreurs += analyserResultatIntDuTest(resultat,
                                             resultatsAttendus[noCas],
                                             nomObtenirNbSecsDsPeriode,
                                             parametresCas[noCas]);
   }
   return(nbErreurs);
}

Et ne pas oublier la ligne à ajouter dans la fonction init():

   nbErreurs += testObtenirNbSecsDsPeriode();


Le test de obtenirNoSecDsPeriode():


Même chose pour la fonction qui calcule le numéro de seconde à l'intérieur d'une période. La différence est qu'elle reçoit deux arguments au lieu d'un seul, et que l'un des deux est optionnel. Nous prévoyons donc cinq cas de tests aussi, deux sans l'argument optionnel et trois avec le deuxième argument:
  1. Sans l'argument optionnel, et avec un petit horodatage: 127.
    Résultat attendu:  127 % (60 * Period()).
  2. Sans l'argument optionnel, et avec un horodatage plus conséquent: 123456789.
    Résultat attendu:  123456789 % (60 * Period()).
  3. Avec un nombre de secondes par période de 240 et un horodatage de 123456789.
    Résultat attendu: 69.
  4. Avec un nombre de secondes par période de 900 et un horodatage de 123456789.
    Résultat attendu: 189.
  5. Avec un nombre de secondes par période de 60 et un horodatage de 127.
    Résultat attendu: 7.

Pour le code, on ajoute la directive #define pour déclarer le nombre de cas de test, la constante string pour définir le nom de la fonction pour les affichages, on écrit la fonction qui exécute la fonction à tester selon le cas avec ou sans argument optionnel. On écrit aussi la fonction de test qui définit les valeurs des cas et fait les tests dans une boucle. Pour les valeurs des cas de test, il faut ajouter un autre tableau d'arguments puisqu'elle en demande deux. Et on n'oublie pas d'ajouter la ligne d'exécution du test de cette fonction dans la fonction init(). Voici le code à ajouter à l'expert advisor:

#define NB_CAS_NO_SEC_IN_PERIOD 5

string nomObtenirNoSecDsPeriode = "obtenirNoSecDsPeriode()"

int execObtenirNoSecDsPeriode(datetime argumentHorodatage, int argumentNbSecsDsPeriode) {
   int resultat;
   if(argumentNbSecsDsPeriode == ARG_INT_EMPTY) {
      resultat = obtenirNoSecDsPeriode(argumentHorodatage); }
   else { resultat = obtenirNoSecDsPeriode(argumentHorodatage, argumentNbSecsDsPeriode); }
   return(resultat);
}

int testObtenirNoSecDsPeriode() {
   int noCas, nbErreurs = 0;
   int resultat;
   datetime argumentsHorodatages[NB_CAS_NO_SEC_IN_PERIOD] = {127,
                                                        123456789,
                                                        123456789,
                                                        123456789,
                                                        127};
   int argumentsNbSecsDsPeriode[NB_CAS_NO_SEC_IN_PERIOD] = {ARG_INT_EMPTY,
                                                                 ARG_INT_EMPTY,
                                                                 240,
                                                                 900,
                                                                 60};
   string parametresCas[NB_CAS_NO_SEC_IN_PERIOD] = {" 127, EMPTY => ",
                                                     " 123456789, EMPTY => ",
                                                     " 123456789, 240 => ",
                                                     " 123456789, 900 => ",
                                                     " 127, 60 => "};
   int resultatsAttendus[NB_CAS_NO_SEC_IN_PERIOD];
   resultatsAttendus[0] = 127 % (60 * Period());
   resultatsAttendus[1] = 123456789 % (60 * Period());
   resultatsAttendus[2] = 69;
   resultatsAttendus[3] = 189;
   resultatsAttendus[4] = 7;
   for(noCas=0 ; noCas<NO_CAS_NB_SEC_IN_PERIOD ; noCas++) {
      resultat = execObtenirNbSecsDsPeriode(argumentsHorodatages[noCas],
                                            argumentsNbSecsDsPeriode[noCas]);
      nbErreurs += analyserResultatIntDuTest(resultat,
                                             resultatsAttendus[noCas],
                                             nomObtenirNoSecDsPeriode,
                                             parametresCas[noCas]);
   }
   return(nbErreurs);
}

Le code est un peu décousu car des lignes sont tronquées. Visionnez la vidéo et vous aurez une meilleure présentation. Et dans la fonction init():

   nbErreurs += testObtenirNoSecDsPeriode();


Le tutoriel vidéo et les deux 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                                              |
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//| fonctions                                                        |
//+------------------------------------------------------------------+

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, resultatTest);
      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);
}

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



//+------------------------------------------------------------------+
//|                                  testsArithmetiqueTemporelle.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"

#include <UtilitairesTests.mqh>
#include <ArithmetiqueTemporelle.mqh>

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

#define NB_CAS_LAST_KNOWN_SEC_NO 5
#define NB_CAS_NB_SECS_IN_PERIOD 5
#define NB_CAS_NO_SEC_IN_PERIOD 5

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

string nomObtenirNoDerniereSecConnue = "obtenirNoDerniereSecConnue()";
string nomObtenirNbSecsDsPeriode = "obtenirNbSecsDsPeriode()"
string nomObtenirNoSecDsPeriode = "obtenirNoSecDsPeriode()"

//+------------------------------------------------------------------+
//| fonctions de tests                                               |
//+------------------------------------------------------------------+

datetime execObtenirNoDerniereSecConnue(string argument) {
   datetime resultat;
   if(argument == ARG_STRING_EMPTY) { resultat = obtenirNoDerniereSecConnue(); }
   else { resultat = obtenirNoDerniereSecConnue(argument); }
   return(resultat);
}

int testObtenirNoDerniereSecConnue() {
   int noCas, nbErreurs = 0;
   datetime resultat;
   string arguments[NB_CAS_LAST_KNOWN_SEC_NO] = {ARG_STRING_EMPTY,
                                                 "EURUSD",
                                                 "GBPJPY",
                                                 "EUR",
                                                 ""};
   string parametresCas[NB_CAS_LAST_KNOWN_SEC_NO] = {" sans paramètre => ",
                                                     " \"EURUSD\" => ",
                                                     " \"GBPJPY\" => ",
                                                     " \"EUR\" => ",
                                                     " \"\" => "};
   datetime resultatsAttendus[NB_CAS_LAST_KNOWN_SEC_NO];
   resultatsAttendus[0] = TimeCurrent();
   resultatsAttendus[1] = MarketInfo("EURUSD", MODE_TIME);
   resultatsAttendus[2] = MarketInfo("GBPJPY", MODE_TIME);
   resultatsAttendus[3] = 0;
   resultatsAttendus[4] = 0;
   for(noCas=0 ; noCas<NB_CAS_LAST_KNOWN_SEC_NO ; noCas++) {
      resultat = execObtenirNoDerniereSecConnue(arguments[noCas]);
      nbErreurs += analyserResultatDatetimeDuTest(resultat,
                                                   resultatsAttendus[noCas],
                                                   nomObtenirNoDerniereSecConnue,
                                                   parametresCas[noCas]);
   }
   return(nbErreurs);
}

int execObtenirNbSecsDsPeriode(int argument) {
   int resultat;
   if(argument == ARG_INT_EMPTY) { resultat = obtenirNbSecsDsPeriode(); }
   else { resultat = obtenirNbSecsDsPeriode(argument); }
   return(resultat);
}

int testObtenirNbSecsDsPeriode() {
   int noCas, nbErreurs = 0;
   int resultat;
   int arguments[NB_CAS_NB_SECS_IN_PERIOD] = {ARG_INT_EMPTY, 5, 240, -2, 0};
   string parametresCas[NB_CAS_NB_SECS_IN_PERIOD] = {" sans paramètres => ",
                                                     " 5 => ",
                                                     " 240 => ",
                                                     " -2 => ",
                                                     " 0 => "};
   int resultatsAttendus[NB_CAS_NB_SECS_IN_PERIOD];
   resultatsAttendus[0] = 60 * Period();
   resultatsAttendus[1] = 300;
   resultatsAttendus[2] = 14400;
   resultatsAttendus[3] = -120;
   resultatsAttendus[4] = 0;
   for(noCas=0 ; noCas<NB_CAS_NB_SECS_IN_PERIOD ; noCas++) {
      resultat = execObtenirNbSecsDsPeriode(arguments[noCas]);
      nbErreurs += analyserResultatIntDuTest(resultat,
                                             resultatsAttendus[noCas],
                                             nomObtenirNbSecsDsPeriode,
                                             parametresCas[noCas]);
   }
   return(nbErreurs);
}

int execObtenirNoSecDsPeriode(datetime argumentHorodatage, int argumentNbSecsDsPeriode) {
   int resultat;
   if(argumentNbSecsDsPeriode == ARG_INT_EMPTY) {
      resultat = obtenirNoSecDsPeriode(argumentHorodatage); }
   else { resultat = obtenirNoSecDsPeriode(argumentHorodatage, argumentNbSecsDsPeriode); }
   return(resultat);
}

int testObtenirNoSecDsPeriode() {
   int noCas, nbErreurs = 0;
   int resultat;
   datetime argumentsHorodatages[NB_CAS_NO_SEC_IN_PERIOD] = {127,
                                                        123456789,
                                                        123456789,
                                                        123456789,
                                                        127};
   int argumentsNbSecsDsPeriode[NB_CAS_NO_SEC_IN_PERIOD] = {ARG_INT_EMPTY,
                                                                 ARG_INT_EMPTY,
                                                                 240,
                                                                 900,
                                                                 60};
   string parametresCas[NB_CAS_NO_SEC_IN_PERIOD] = {" 127, EMPTY => ",
                                                     " 123456789, EMPTY => ",
                                                     " 123456789, 240 => ",
                                                     " 123456789, 900 => ",
                                                     " 127, 60 => "};
   int resultatsAttendus[NB_CAS_NO_SEC_IN_PERIOD];
   resultatsAttendus[0] = 127 % (60 * Period());
   resultatsAttendus[1] = 123456789 % (60 * Period());
   resultatsAttendus[2] = 69;
   resultatsAttendus[3] = 189;
   resultatsAttendus[4] = 7;
   for(noCas=0 ; noCas<NO_CAS_NB_SEC_IN_PERIOD ; noCas++) {
      resultat = execObtenirNoSecsDsPeriode(argumentsHorodatages[noCas],
                                            argumentsNbSecsDsPeriode[noCas]);
      nbErreurs += analyserResultatIntDuTest(resultat,
                                             resultatsAttendus[noCas],
                                             nomObtenirNoSecDsPeriode,
                                             parametresCas[noCas]);
   }
   return(nbErreurs);
}

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

int init()
  {
   int nbErreurs = 0;
   nbErreurs += testObtenirNoDerniereSecConnue();
   nbErreurs += testObtenirNbSecsDsPeriode();
   nbErreurs += testObtenirNoSecDsPeriode();
   Print("TESTS DE ArithmetiqueTemporelle.mqh TERMINES AVEC ", nbErreurs, " ERREUR(S).");
   return(0);
  }

//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+

int deinit()
  {
   return(0);
  }

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+

int start()
  {
   return(0);
  }

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


Ouf =D Ça commence à en faire du code ! C'est assez répétitif, à cause des limitations de MQL4: on fait ce qu'on peut ! Si quelqu'un voit une amélioration, qu'il la propose je suis preneur. Faites attention aux modifications à apporter à chaque copier coller de fonction. Il ne faut pas en oublier car le compilateur ne vous dira pas forcément qu'il y a un problème. Vous pouvez laissez par exemple un datetime sans le changer en int, et ça passe, le compilateur ne vous dit rien, mais vous aurez des bugs potentiels car il y a des valeurs qui changent en faisant les conversions d'un int à un datetime et inversement.

mercredi 29 janvier 2014

Traitement du cas de test sans argument

A faire des fonctions utilisant la valeur par défaut  EMPTY  je réfléchissais aux tests des fonctions et du traitement du cas sans argument. Je ne peux pas laisser ce problème de côté, il faut le régler, je fais donc ce post pour améliorer le code de tests de fonctions une seconde fois. Le problème est que lorsque je mets une valeur par défaut, il faudrait tester le cas ou je ne passe pas l'argument avec valeur par défaut pour voir si le code qui doit gérer ce cas dans la fonction testée fonctionne bien. Or en passant la valeur par défaut, je ne teste pas forcément la portion de code qui y est dédiée surtout si j'emploie la valeur  EMPTY  que je ne peux stocker dans une variable, ni même passer en argument car le compilateur refuse. Nous allons donc régler ce problème: traiter le cas sans argument et pouvoir mettre la constante  EMPTY .

Utilisation de la constante EMPTY dans la fonction à tester:


Attention ! La constante  EMPTY  est à utiliser avec précautions. Je m'explique: elle possède une valeur entière valide qui est -1. Première conclusion, on ne peut pas l'utiliser avec les types  double  ou  string  (les autres types acceptant les nombres entiers). Deuxième remarque, c'est une valeur et il faut faire attention qu'elle ne soit pas utilisée parce que dans ce cas, quand on utilise la valeur -1 sans vouloir signifier une valeur vide, on se retrouve à traiter le cas de la valeur vide. Donc extrême vigilance. On ne touche donc pas au code de la première fonction obtenirNoDerniereSecConnue() qui a une valeur par défaut à  "NULL"  Cette valeur convient pour le cas, et  EMPTY  ne serait pas acceptée par le compilateur pour cause d'incompatibilité de type.

Création de directives #define pour des valeurs vides selon les types:


C'est bien beau de mettre la valeur vide par défaut, mais il va falloir trouver un moyen de signaler à la fonction de test qu'il n'y a pas d'argument car le compilateur ne va pas accepter une valeur vide dans une variable, ni dans un tableau. En informant la fonction de test qu'il n'y a pas de valeur, donc pas d'argument à mettre, elle peut faire une alternative d'appel de la fonction sans cet argument. Il faut alors trouver le moyen de lui indiquer. Ce moyen va être la définition de directives #define pour mettre dans les variables ou tableaux des constantes littérales. Ces constantes auront une valeur particulière dont on est certain (ou presque) qu'elles ne seront pas utilisées pour désigner le fait qu'il ne faut pas passer l'argument. Cette valeur est destinée à être stockée dans une variable ou un tableau, elle devra donc avoir le même type que l'argument. Il faut alors créer une directive #define par argument. Bien obligé d'utiliser une directive préprocesseur au lieu de variables constantes, car le compilateur ne les accepte pas dans les tableaux. Elles devront être utilisées dans tous les tests, ont les définit donc dans l'include UtilitairesTests.mqh. On ajoute le code suivant dans ce fichier:


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


Je propose ces valeurs car il y a une chaine de caractères très particulière dont il est très improbable de se servir dans les tests, la valeur positive maximum d'un entier, une valeur entière négative acceptée pour les booléens mais qui ne correspond ni à true (1) ni à false (0), une valeur décimale très grande et très particulière dont il est très improbable de se servir dans un test, une valeur entière pour datetime qui est la valeur maximum et la même valeur pour une couleur qui n'est pas utilisée car elle dépasse trois octets en mémoire (quatre octets tous remplis).

Prise en compte des cas de test où un argument est absent:


Maintenant que nous avons de quoi informer la fonction de test qu'il y a un argument à laisser vide, nous pouvons ajouter le code pour qu'elle fasse une alternative à l'exécution de la fonction à tester: avec ou sans l'argument. Pour ne pas modifier la boucle de test des différents cas, nous ajoutons juste un préfixe à la fonction appelée pour placer notre code supplémentaire dans une autre fonction. Ce préfixe est «exec» car la nouvelle fonction qu'on crée est là pour exécuter la fonction à tester de différentes manières: avec ou sans les arguments optionnels. Dans cette nouvelle fonction nous testons l'égalité de l'argument avec la constante correspondante précédemment définie pour excéuter comme il faut la fonction à tester: c'est-à-dire que si l'argument est égal à la constante désignant une valeur vide, alors la fonction est exécutée sans cet argument, sinon avec l'argument. Nous changeons aussi la valeur des arguments absents en mettant aussi cette valeur précédemment définie dans le tableau des arguments.

datetime execObtenirNoDerniereSecConnue(string argument) {
   datetime resultat;
   if(argument == ARG_STRING_EMPTY) { resultat = obtenirNoDerniereSecConnue(); }
   else { resultat = obtenirNoDerniereSecConnue(argument); }
   return(resultat);
}

int testObtenirNoDerniereSecConnue() {
   int noCas, nbErreurs = 0;
   datetime resultat;
   string arguments[NB_CAS_LAST_KNOWN_SEC_NO] = {ARG_STRING_EMPTY,
                                                 "EURUSD",
                                                 "GBPJPY",
                                                 "EUR",
                                                 ""};
   string parametresCas[NB_CAS_LAST_KNOWN_SEC_NO] = {" sans paramètre => ",
                                                     " \"EURUSD\" => ",
                                                     " \"GBPJPY\" => ",
                                                     " \"EUR\" => ",
                                                     " \"\" => "};
   datetime resultatsAttendus[NB_CAS_LAST_KNOWN_SEC_NO];
   resultatsAttendus[0] = TimeCurrent();
   resultatsAttendus[1] = MarketInfo("EURUSD", MODE_TIME);
   resultatsAttendus[2] = MarketInfo("GBPJPY", MODE_TIME);
   resultatsAttendus[3] = 0;
   resultatsAttendus[4] = 0;
   for(noCas=0 ; noCas<NB_CAS_LAST_KNOWN_SEC_NO ; noCas++) {
      resultat = execObtenirNoDerniereSecConnue(arguments[noCas]);
      nbErreurs += analyserResultatDatetimeDuTest(resultat,
                                                   resultatsAttendus[noCas],
                                                   nomObtenirNoDerniereSecConnue,
                                                   parametresCas[noCas]);
   }
   return(nbErreurs);
}



Le tutoriel vidéo et les deux fichiers de code 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

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

//+------------------------------------------------------------------+
//| fonctions                                                        |
//+------------------------------------------------------------------+

int analyserResultatDatetimeDuTest(
         datetime resultatTest,
         datetime resultatAttendu,
         string nomFonction,
         string parametresCas,
         string msgErreur = " a produit une erreur de résultat avec le cas ",
         string msgSucces = " a passé le test sans erreurs ") {
   int nbErreurs = 0;
   if(resultatTest == resultatAttendu) {
      Print(nomFonction, msgSucces, resultatTest);
   } else {
      Print(nomFonction, msgErreur, parametresCas, resultatTest);
      nbErreurs++;
   }
   return(nbErreurs);
}



//+------------------------------------------------------------------+
//|                                  testsArithmetiqueTemporelle.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"

#include <UtilitairesTests.mqh>
#include <ArithmetiqueTemporelle.mqh>

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

#define NB_CAS_LAST_KNOWN_SEC_NO 5

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

string nomObtenirNoDerniereSecConnue = "obtenirNoDerniereSecConnue()";

//+------------------------------------------------------------------+
//| fonctions de tests                                               |
//+------------------------------------------------------------------+

datetime execObtenirNoDerniereSecConnue(string argument) {
   datetime resultat;
   if(argument == ARG_STRING_EMPTY) { resultat = obtenirNoDerniereSecConnue(); }
   else { resultat = obtenirNoDerniereSecConnue(argument); }
   return(resultat);
}

int testObtenirNoDerniereSecConnue() {
   int noCas, nbErreurs = 0;
   datetime resultat;
   string arguments[NB_CAS_LAST_KNOWN_SEC_NO] = {ARG_STRING_EMPTY,
                                                 "EURUSD",
                                                 "GBPJPY",
                                                 "EUR",
                                                 ""};
   string parametresCas[NB_CAS_LAST_KNOWN_SEC_NO] = {" sans paramètre => ",
                                                     " \"EURUSD\" => ",
                                                     " \"GBPJPY\" => ",
                                                     " \"EUR\" => ",
                                                     " \"\" => "};
   datetime resultatsAttendus[NB_CAS_LAST_KNOWN_SEC_NO];
   resultatsAttendus[0] = TimeCurrent();
   resultatsAttendus[1] = MarketInfo("EURUSD", MODE_TIME);
   resultatsAttendus[2] = MarketInfo("GBPJPY", MODE_TIME);
   resultatsAttendus[3] = 0;
   resultatsAttendus[4] = 0;
   for(noCas=0 ; noCas<NB_CAS_LAST_KNOWN_SEC_NO ; noCas++) {
      resultat = execObtenirNoDerniereSecConnue(arguments[noCas]);
      nbErreurs += analyserResultatDatetimeDuTest(resultat,
                                                   resultatsAttendus[noCas],
                                                   nomObtenirNoDerniereSecConnue,
                                                   parametresCas[noCas]);
   }
   return(nbErreurs);
}

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

int init()
  {
   int nbErreurs = 0;
   nbErreurs += testObtenirNoDerniereSecConnue();
   Print("TESTS DE ArithmetiqueTemporelle.mqh TERMINES AVEC ", nbErreurs, " ERREUR(S).");
   return(0);
  }

//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+

int deinit()
  {
   return(0);
  }

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+

int start()
  {
   return(0);
  }

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