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

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


lundi 27 janvier 2014

Le nombre et le numéro de secondes dans les périodes (bars) avec MQL4


Nous allons utiliser les techniques arithmétiques de code vues dans la première partie de ce sujet pour réaliser des fonctions supplémentaires de calculs temporels, à placer dans le fichier include d'arithmétique temporelle que nous avons créé. Ces fonctions seront très simple pour les premières.

La fonction Period():


On commence par utiliser une fonction prédéfinie par Metatrader qui donne une caractéristique de la période courante: le nombre de minutes qu'elle contient. La période courante étant celle sur laquelle on a lancé le programme (expert advisor ou indicateur). Cette fonction est  Period() . Si elle nous donne le nombre 5, c'est que nous sommes sur une période de 5 minutes ! c'est aussi simple que cela. Nous allons l'utiliser pour obtenir une information qui en découle comme le nombre de secondes de la période.

Le nombre de secondes dans la période courante:


Une fonction très simple à faire: calculer le nombre de secondes en ayant le nombre de minutes ! Tout ça pour la période utilisée dans le graphique sur lequel le programme a été lancé. Je suis sûr que vous avez déjà trouvé comment faire. La fonction  Period()  nous donne les minutes et je vous donne le code de la fonction très minimaliste:

int obtenirNbSecsDsPeriodeCourante() {
   return(60*Period());
}


Le nombre de secondes dans une période quelconque:


Sur le même modèle, on calcule le nombre de secondes d'une période en recevant en argument le nombre de minutes dans la période. Le code complet et minimaliste également de la fonction:

int obtenirNbSecsDsPeriode(int nbMinsDansPeriode) {
   return(60*nbMinsDansPeriode);
}

Mais plutôt que de faire deux fonctions pour la même chose, on va en faire une synthèse. Je m'explique: on ne va pas créer la première mais modifier celle-ci pour qu'elle prenne en compte le cas particulier de la première. L'argument aura une valeur par défaut:  EMPTY  comme ça lorsque dans le corps de la fonction la variable est vide, on lui met alors la valeur utilisée dans la première fonction issue de  Period() . Ainsi nous n'avons pas besoin de préciser le nombre de secondes dans une période si on veut la période courante. Voici le code:

int obtenirNbSecsDsPeriode(int nbMinsDansPeriode=EMPTY) {
   int nbSecsDsPeriode;
   if(nbMinsDansPeriode == EMPTY) { nbMinsDansPeriode = Period(); }
   nbSecsDsPeriode = 60 * nbMinsDansPeriode;
   return(nbSecsDsPeriode);
}


Le numéro d'une seconde dans une période:


On va maintenant faire un calcul un peu différent, puisqu'on veut non pas un numéro depuis l'epoch (1er janvier 1970 à 00:00:00) mais le numéro d'une seconde depuis le début de la période. Par exemple, si les périodes sont des M1, c'est-à-dire de 1 min = 60 sec, et que la seconde porte l'horodatage 132 (je sais c'est très vieux!) alors on en déduit que de 0 à 59, c'est la période n°0, de 60 à 119 c'est la période n°1, et que 132 est dans la période n°2. C'est la seconde n°132-120=n°12 de la période n°2. Il faut donc retirer de l'horodatage toutes les périodes entières qu'il contient et prendre le reste. Cette opération porte un nom en arithmétique: modulo. L'opérateur modulo (souvent représenté avec le signe pourcent: %) indique de faire la division euclidienne (entière) et de prendre le reste. En résumé: 132%60=12 ou encore 12%5=2. Mentalement il faut faire comme on vient de voir, retirer le diviseur autant de fois que possible et voir ce qui reste. 12=10+2=5x2+2, donc 12%5=2. Je vous propose une série d'exercices pour vous exercer si vous en avez besoin:

Exercices sur l'opérateur modulo
Soit l'opération 24%5 (prononcez «vingt-quatre modulo cinq»)
Combien de 5 peut-on retirer de 24:
La décomposition de 24 à choisir est:
24%5 est égal à:
Pour 24%11 la décomposition est:
Pour 8%3 la décomposition est:
8%3 est égal à:
Pour 9%3 la décomposition est:
9%3 est égal à:

Il faut retenir qu'il y a un nombre limité de restes possibles, par exemple: un nombre modulo 5, ne peut avoir que 5 résultats possibles qui sont 0, 1, 2, 3 et 4. On ne peut évidemment pas avoir 5 ou plus car on pourrait y retirer encore le diviseur qui est 5. Quand un nombre augmente, le résultat de son modulo est cyclique:
  • 0%3=0
  • 1%3=1
  • 2%3=2
  • 3%3=0
  • 4%3=1
  • 5%3=2
  • 6%3=0
  • 7%3=1
  • etc.

Nous allons donc utiliser l'opérateur modulo pour extraire le numéro d'une seconde dans la période à laquelle il appartient à partir de son horodatage. Notre fonction va donc recevoir en argument un horodatage et le nombre de secondes dans la période. Nous mettrons ce deuxième argument avec une valeur par défaut  EMPTY  comme ça lorsque nous le préciserons pas, il s'agira du nombre de secondes dans la période courante. Et lorsque le deuxième argument sera vide il faudra y mettre la valeur qu'on fera calculer par la fonction que nous avons créée précédemment, sans argument évidemment comme ça nous l'aurons pour la période courante:

int obtenirNoSecDsPeriode(datetime horodatageSec, int nbSecsDansPeriode=EMPTY) {
   int noSecDansPeriode;
   if(nbSecsDansPeriode == EMPTY) { nbSecsDansPeriode = obtenirNbSecsDsPeriode(); }
   noSecDansPeriode = horodatageSec % nbSecsDansPeriode;
   return(noSecDansPeriode);
}


Le tutoriel vidéo et le code du fichier include augmenté:




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

//+------------------------------------------------------------------+
//| 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=EMPTY) {
   int nbSecsDsPeriode;
   if(nbMinsDansPeriode == EMPTY) { nbMinsDansPeriode = Period(); }
   nbSecsDsPeriode = 60 * nbMinsDansPeriode;
   return(nbSecsDsPeriode);
}

int obtenirNoSecDsPeriode(datetime horodatageSec, int nbSecsDansPeriode=EMPTY) {
   int noSecDansPeriode;
   if(nbSecsDansPeriode == EMPTY) { nbSecsDansPeriode = obtenirNbSecsDsPeriode(); }
   noSecDansPeriode = horodatageSec % nbSecsDansPeriode;
   return(noSecDansPeriode);
}

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


Dans le prochain post nous ferons sans attendre un code de test pour ces deux fonctions, à ajouter à celui déjà existant. Il est impératif de le faire le plus tôt possible, car nous ne voyons pas le code fonctionner pour l'instant, et il ne s'agit pas de modifier ces fonctions quand nous voudrons les inclure dans un gros projet. Ou même d'avoir un bug dans un gros projet à cause de petites fonctions de base, on perdrait ainsi un temps précieux à chercher l'origine du problème et de la motivation au passage.

vendredi 24 janvier 2014

Factorisation du code de test avec un fichier include pour les tests

Le code précédent pour tester notre fonction, est tout à fait du «bad code». Il faut impérativement corriger cela, car il ne sert à rien d'essayer de faire du code utile propre et laisser un code de tests dégueulasse. Cela induirait les mêmes effets que si le code utile était mauvais.

Factorisation de l'analyse du résultat du cas:


Je vous propose donc de créer une fonction d'analyse du résultat du test de cas qui sera appelée plusieurs fois pour tester une fonction. Le problème est que nous ne pouvons pas mettre le test de la fonction proprement dit à l'intérieur car avec le langage MQL4 on ne peut pas passer de fonction en paramètre. On est donc obligé de tester le cas puis d'appeler cette fonction d'analyse qui comparera les résultats et fera les affichages et retournera une valeur booléenne pour dire si c'est ok ou bien s'il y a une erreur. Non on va plutôt retourner un entier qui est le nombre d'erreur (0 ou 1) comme ça on pourra l'additionner directement au total des erreurs lors de l'appel de la fonction d'analyse, ça fera un code plus concis. Cette fonction d'analyse du résultat du cas sera placée dans un include, car elle ne sera pas la seule sans doute (pour le moment oui) et qu'elle sera utilisée dans de nombreux experts advisors de tests de code. Cette fonction doit donc recevoir en arguments: le résultat du test, la valeur attendue, le nom de la fonction, le texte représentant les paramètres du cas, le message de succès avec une valeur par défaut et le message d'erreur avec une valeur par défaut. Ça fait beaucoup trop de paramètres, et donc pas «clean code» du tout. Mais hélas, trois fois hélas, avec le MQL4, on est obligé de faire des entorses aux règles du code propre et des compromis. C'est ça ou bien un code très long et très redondant. Autre détail facheux: le type du résultat va varier d'une fonction à tester à l'autre. Or on doit passer ce résultat à la fonction d'analyse, et préciser un type, et il n'y a pas de type général en MQL4 et on ne peut pas se servir du type void pour dire qu'on ne connait pas le type à la compilation et ainsi pouvoir recevoir n'importe quel type de données comme dans le langage C. Ça ne fonctionne pas ! Le compilateur réclame un type précis. Nous allons donc créer une fonction d'analyse par type de valeur de retour, et par «chance» avec MQL4, ça sera en nombre limité. Nous ferons les autres fonctions quand nous en aurons besoin. Je place l'argument du message d'erreur avant celui du succès car le message d'erreur est plus susceptible d'être spécifié que le message de succès, puisqu'il faut placer les valeurs par défaut et non précisées lors de l'appel en dernier. Voici le code complet du fichier include en première version:

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

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


Factorisation des tests des cas:


Nous avons donc maintenant deux lignes par test de cas au lieu de 7: un appel de la fonction à tester avec récupération du résultat et un appel de la fonction qu'on vient de créer pour analyser avec une addition au total des erreurs en même temps. Ces deux étapes sont à faire pour chaque cas. Nous aurons encore du code redondant: ces deux lignes à répéter pour chaque cas. On va factoriser tout ça avec une boucle for, mais dans ce cas il va falloir mettre les données des différents cas dans des tableaux, qu'on mettra en constantes locales à la fonction de test. Nous aurons besoin d'une variable entière noCas (int) pour parcourir la boucle, du nombre d'erreurs (int) comme précédemment, d'une variable résultat (datetime), d'un tableau de string des arguments, un autre tableau de string des paramètres des cas et un tableau de datetime des resultats attendus. Nous pouvons remplir ces tableaux de deux manières:
  1. à la déclaration, avec un égal et la liste des valeurs littérales (nombres et textes dans le code) séparées par des virgules, entre accolades.
  2. en affectant une valeur case par case après la déclaration, et nous y serons obligés bien que ce soit moins pratique pour les valeurs issues de fonctions sur le moment de l'affectation.
Il faut également factoriser ce nombre de cas qui est répété plusieurs fois: dans la déclaration des tableaux et dans la condition de limite de la boucle. Mais lors de déclaration des tableaux on ne peut pas indiquer la taille avec une variable, ni même une constante (c'est la même chose). Par contre il y a un autre moyen qui est d'utiliser les directives #define qui nous permettent de définir des constantes pour le préprocesseur. Celui-ci va remplacer toutes les constantes qu'on lui aura définit par leur valeur associée avant la compilation. Le compilateur ne verra dans le code que le nombre 5 que le préprocesseur aura mis. Pour ne pas faire de noms de constantes #define trop longs je vais donner parfois des noms en anglais quand ils sont plus courts. Voici le tutoriel vidéo et le nouveau code de l'expert advisor de test de l'include ArithmetiqueTemporelle.mqh (certaines lignes entières dans le fichier ont été tronquées pour tenir dans le post, celles qui ont des colonnades de trop nombreux arguments pas «clean code» du tout ! Ahhh ... le MQL4 !):



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

int testObtenirNoDerniereSecConnue() {
   int noCas, nbErreurs = 0;
   datetime resultat;
   string arguments[NB_CAS_LAST_KNOWN_SEC_NO] = {"NULL",
                                                 "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 = obtenirNoDerniereSecConnue(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);
  }

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

PS: La fonction testée, a un paramètre optionnel, donc avec une valeur par défaut. J'avais commencé par mettre la constante NULL à cette valeur, comme dans le langage C, mais le compilateur MQL4 ne l'accepte pas et j'ai donc mis la chaine de caractères "NULL". Le MQL4 a une constante pour les paramètres vides, c'est la constante EMPTY, et NULL ne concerne que les variables string vides (différent de chaine vide: ""). Mais j'ai laissé la chaine "NULL" car je peux la mettre dans le tableau des arguments et la passer pour le premier cas bien que ce soit un cas sans argument. Cet argument passé est celui par défaut. Ça n'aurait pas pu être le cas avec EMPTY, puisque que le compilateur ne l'accepte pas en tant que valeur dans le tableau.
Je ne peux pas améliorer encore plus le code, je n'ai pas trouvé comment faire pour l'instant, car dans l'idéal il faudrait sortir les déclarations des valeurs des tests et en plus certaines d'entre elles doivent être affectées au moment de l'exécution de la fonction de test (les appels aux fonctions prédéfinies). Nous sommes un peu limités par le langage MQL4. Le code actuel est tout de même bien meilleur que le précédent, y a pas photo ! =D

jeudi 23 janvier 2014

Un fichier de tests pour l'include d'arithmétique temporelle


La programmation est très rigoureuse, et nous humains, nous le somme beaucoup moins. Il est difficile de penser à tout, et nous oublions de nombreuses choses. Surtout nous ne pouvons pas tout connaitre et c'est l'expérience qui nous fait apprendre de nouvelles choses. C'est pour cela qu'on ne peut pas espérer coder ne serait-ce qu'un petit programme sans aucun bug voire défaut de conception algorithmique. Dès qu'on fait du code, il faut s'attendre à ce que ça ne fonctionne pas, et qu'il faille tester pour trouver les problèmes de conception et bugs cachés. Depuis les années 90, le monde du développement informatique applique ce qu'on appelle le Test Driven Developpment: le développement conduit par les tests. Nous n'appliquerons pas à la lettre les recommandations qui disent par exemple de commencer à coder le programme de test avant même le code qu'il est sensé tester. Et nous avons tord, mais pour des raisons pédagogiques, je ne commence pas par faire les tests, ni ne fait des tests absolument pour tout. Pour les petits experts advisors ou indicateurs de démonstration le simple fait de les voir fonctionner de suite après nous confirme si la fonction principale est assurée sur le moment (on en sait rien dans le temps, ni des bugs cachés). Donc pour eux, pas de tests rigoureux. En général le test minimal est le test dit unitaire. Il consiste à juste tester que le code fonctionne dans un cas banal. Il n'y a pas de tests étendus, de torture avec un fonctionnement marathon ou avec le test du débile où des données farfelues sont fournies au code pour voir s'il réagit bien. Pas de tests avec tous les différents cas possibles, etc. Dans notre cas nous allons faire un peu plus que le test unitaire sans pour autant tester tous les cas. La stabilité dans le temps pourra être testé avec le testeur de stratégies de metaTrader, mais ça ne concerne que les indicateurs et experts advisors. Une librairie de fonction doit juste donner des résultats de traitement cohérents. Nous testerons donc plusieurs cas, et, plus nous en mettrons des différents, plus nous seront sûr de la qualité du code et de l'algorithme.

Tester la fonction obtenirNoDerniereSecConnue():


Nous allons tester cette fonction en l'appelant en lui passant ou non un paramètre, et même différents paramètres. Sans paramètre pour tester si la valeur par défaut est bien gérée, puis avec différents textes pour tester différents cas, et même des cas ou le texte n'est pas valide. Dans ce dernier cas, nous supposons déjà que la fonction va planter et nous verrons comment l'erreur est gérée par Metatrader. Puis nous modifierons la fonction pour qu'elle gère elle-même les erreurs qui peuvent survenir. Il faut le faire car si un bug est caché dans un indicateur ou expert advisor, et qu'il provoque le fait qu'un paramètre non valide est fourni à cette fonction, il faut pouvoir réagir en conséquence et ne pas faire perdre du capital par des erreurs en cascade:
  • Un cas sans paramètre et on doit obtenir la même chose que ce que fournit TimeCurrent().
  • Avec "EURUSD" et obtenir la même chose que ce que nous fournit  MarketInfo() .
  • Avec "GBPJPY" et obtenir la même chose que ce que nous fournit  MarketInfo() .
  • Avec une chaîne vide "" et voir la gestion de l'erreur par MetaTrader4 pour commencer puis obtenir le résultat par défaut lors d'une erreur que nous aurons décidé.
  • Avec "EUR" et obtenir le résultat par défaut pour une erreur.
Comme nous allons faire les tests dans un expert advisor, qui aura pour but de tester toutes les fonctions de ce fichier include, nous allons mettre tous ces cas de test dans une fonction nommée pour l'occasion TestObtenirNoDerniereSecConnue() (ouf ! 30 caractères, ça a faillit dépasser 31 caractères). Et à l'intérieur nous allons y tester les différents cas et comptabiliser les échecs aux tests. En cas d'échec on affiche un message avec les valeurs et en cas de succès aussi. Pour chaque cas on va:
  • obtenir le résultat du cas
  • tester si le résultat est égal à la valeur attendue
  • si oui, afficher que c'est ok et le résultat
  • si non afficher qu'il y a eu une erreur et le résultat, et augmenter le nombre d'erreurs de 1.
le code pour tester chaque cas sera donc:

resultat = obtenirNoDerniereSecConnue(//paramètre);
if(resultat == //resultat attendu) {
   Print(//nom de la fonction, //message ok, resultat);
} else {
   nbErreurs++;
   Print(//nom de la fonction, //message erreur, //paramètre fourni, resultat);
}

Et il faut répéter ce code avec les bonnes valeurs pour chaque cas de test, ce qui n'est pas très "clean code". J'ai réfléchi pour factoriser ce code, mais cela rallonge énormément la liste des déclarations de constantes et au final le code est très long. je ferai un post plus tard pour essayer de factoriser ces cas de tests, mais ce n'est pas facile d'obtenir quelque chose de convenable avec un langage comme MQL4. Pour l'instant on se contente de ce code linéaire et redondant qui malgré tout fonctionne.

Les résultats du test de la fonction obtenirNoDerniereSecConnue():


J'ai écrit le code et testé la fonction, et je constate que MetaTrader4 ne m'indique aucune erreur et que les quatre tests sont effectués, dont les deux derniers en erreur signalée. Ce qui signifie une chose: la fonction MarketInfo() réagit bien aux paramètres erronés. Elle les accepte et retourne quand même un résultat qui est zéro. J'avais l'intention de modifier le code pour qu'il réagisse à une erreur retournée par la fonction MarketInfo() et de renvoyer la valeur 0 car jamais utilisée en tant qu'horodatage ce qui aurait signifié au code appelant qu'il y a eu une erreur, mais MarketInfo() retourne déjà zéro ! C'est parfait ! On a donc déjà zéro dans la variable horodatage et c'est donc la valeur qui sera retournée. Nous n'avons pas besoin de modifier le code. Nous modifions juste les valeurs de résultat attendu qui doivent être zéro dans les cas où le paramètre fourni est mauvais.

Le tutoriel vidéo et le code complet:


Le code sera donc placé dans un expert advisor, on doit évidemment inclure le fichier ArithmétiqueTemporelle avec la directive préprocesseur #include suivie du nom du fichier entre deux chevrons ouvrant et fermant: #include <ArithmetiqueTemporelle.mqh> au début du fichier après le copyright. Nous créons alors une fonction testObtenirNoDerniereSecConnue() qui contient les tests de tous les cas et qui retourne le nombre d'erreur. Puis dans la fonction init(), on fait exécuter toutes les fonctions de tests (pour l'instant une seule) en récupérant le nombre d'erreurs à chaque fois et en les accumulant. A la fin de la fonction init() on affiche le bilan du test de l'include entier.

Voici le tutoriel vidéo:


Et le code complet:

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

#include <ArithmetiqueTemporelle.mqh>

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

string msgTestOk = " a passé le test sans erreurs. Valeur retournée: ";
string msgErreurTest = " a produit une erreur de résultat avec le cas ";

string nomObtenirNoDerniereSecConnue = "obtenirNoDerniereSecConnue()";

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

int testObtenirNoDerniereSecConnue() {
   int nbErreurs = 0;
   datetime resultat;
   resultat = obtenirNoDerniereSecConnue();
   if(resultat == TimeCurrent()) {
      Print(nomObtenirNoDerniereSecConnue, msgTestOk, resultat);
   } else {
      nbErreurs++;
      Print(nomObtenirNoDerniereSecConnue, msgTestOk, " sans paramètre => ", resultat);
   }
   resultat = obtenirNoDerniereSecConnue("EURUSD");
   if(resultat == MarketInfo("EURUSD", MODE_TIME)) {
      Print(nomObtenirNoDerniereSecConnue, msgTestOk, resultat);
   } else {
      nbErreurs++;
      Print(nomObtenirNoDerniereSecConnue, msgTestOk, "\"EURUSD\" => ", resultat);
   }
   resultat = obtenirNoDerniereSecConnue("GBPJPY");
   if(resultat == MarketInfo("GBPJPY", MODE_TIME)) {
      Print(nomObtenirNoDerniereSecConnue, msgTestOk, resultat);
   } else {
      nbErreurs++;
      Print(nomObtenirNoDerniereSecConnue, msgTestOk, "\"GBPJPY\" => ", resultat);
   }
   resultat = obtenirNoDerniereSecConnue("EUR");
   if(resultat == 0) {
      Print(nomObtenirNoDerniereSecConnue, msgTestOk, resultat);
   } else {
      nbErreurs++;
      Print(nomObtenirNoDerniereSecConnue, msgTestOk, "\"EUR\" => ", resultat);
   }
   resultat = obtenirNoDerniereSecConnue("");
   if(resultat == 0) {
      Print(nomObtenirNoDerniereSecConnue, msgTestOk, resultat);
   } else {
      nbErreurs++;
      Print(nomObtenirNoDerniereSecConnue, msgTestOk, "\"\" => ", resultat);
   }
   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);
  }

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

Je ne peux définitivement pas laisser un tel code, redondant à ce point ! On est loin du «clean code», c'est même du «bad code». Surtout qu'on va ajouter d'autres fonctions dans l'include d'arithmétique temporelle, et donc multiplier ce code de test. Il est important, très important de maintenir un code de test clean, tout comme le code utile. Je m'y attèle dès le prochain post, nous factoriserons ce code. Pour l'instant l'objectif est rempli: notre première fonction de l'include est testée et on sait qu'on peut avoir confiance en elle. Nous n'avons pas pourtant de garantie à 100%, on va dire que nous sommes à 99%. Mais sans ces tests, c'était du 1%. (la première version de la fonction avait échouée non pas aux tests, mais carrément à la compilation !  =D)