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

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


Aucun commentaire:

Enregistrer un commentaire