Organiser l'Accès aux Données

Dans cette section, les questions liées à l'obtention, au stockage et à la demande des données des prix (timeseries) sont considérées.

Recevoir les Données du Serveur de Trades

Avant que les données des prix soient disponibles dans le terminal MetaTrader 5, ils doivent être obtenus et traités. Pour obtenir les données, la connexion au serveur de trades MetaTrader 5 doit être établie. Les données sont obtenues sous la forme de blocs compressés de barres 1 minute depuis le serveur suivant la demande d'un terminal.

Le mécanisme de référence du serveur pour les données ne dépend pas de la façon dont la demande a été initiée - par un utilisateur naviguant dans un graphique ou de façon programmatique dans le langage MQL5.

Stocker les Données Intermédiaires

Les données obtenues du serveur sont automatiquement décompréssées et sauvegardées dans le format intermédiaire HCC. Les données de chaque symbole sont écrites dans un dossier séparé : répertoire_du_terminal\bases\nom_du_serveur\history\nom_du_symbole. Par exemple, les données de l'EURUSD reçues du serveur MetaQuotes-Demo seront stockées dans répertoire_du_terminal\bases\MetaQuotes-Demo\history\EURUSD\.

Les données sont écrites dans des fichiers avec l'extension .hcc. Chaque fichier stocke les données des barres 1 minute pour un an. Par exemple, le fichier nommé 2009.hcc dans le dossier EURUSD contient les barres 1 minutes de l'EURUSD pour l'année 2009. Ces fichiers sont utilisés pour préparer les données des prix pour toutes les périodes et ne sont pas prévus pour être accéder directement.

Obtenir les Données sur une Période Désirée à partir des Données Intermédiaires

Les fichiers HCC intermédiaires sont utilisés comme source de données pour construire les données des prix pour les périodes demandées dans le format HC. Les données au format HC sont des timeseries qui sont préparées au maximum pour un accès rapide. Elles sont créées à la demande d'un graphique ou d'un programme MQL5. Le volume des données ne doit pas excéder la valeur du paramètre "Nb max de barres dans les graphiques". Les données sont stockées pour une utilisation ultérieure dans des fichiers avec l'extension hcc.

Pour économiser des ressources, les données d'une période sont stockées et sauvegardées en RAM uniquement si nécessaire. Si elles ne sont pas appelées pendant un long moment, elles sont libérées de la RAM et sauvegardées dans un fichier. Pour chaque période, les données sont préparées indépendamment du fait qu'il y ait des données prêtes pour les autres périodes ou pas. Les règles de formation et d'accès aux données sont les mêmes pour toutes les périodes. C'est à dire qu'en dépit du fait que les données unitaires stockées au format HCC sont les données 1 minute (M1), la disponibilité des données HCC ne signifie pas la disponibilité des données de la période M1 puisque HC est dans le même volume.

La réception des nouvelles données du serveur appelle automatiquement la mise à jour des données des prix au format HC de toutes les périodes. Cela mène aussi au recalcul de tous les indicateurs qui les utilisent implicitement comme données d'entrée pour les calculs.

Paramètre "Nb max de barres dans le graphique"

Le paramètre "Nb max de barres dans le graphique" restreint le nombre de barres au format HC disponibles dans les graphiques, dans les indicateurs et dans les programmes mql5. Ceci est valide pour toutes les périodes disponibles et sert en tout premier lieu à économiser les ressources de l'ordinateur.

Lorsqu'une grande valeur est mise pour ce paramètre, il faut se souvenir que si un historique profond des données des prix est disponible pour les petites périodes, la mémoire utilisée pour stocker les timeseries et les buffers de l'indicateur peut atteindre des centaines de mega-octets et atteindre la limite de RAM pour le programme du terminal client (2 Go pour les applications 32 bits de MS Windows).

Un changement du paramètre "Nb max de barres dans les graphiques" n'est pris en compte qu'après le redémarrage du terminal client. Le changement de ce paramètre ne cause ni la vérification automatique du serveur pour des données supplémentaires, ni la formation de barres supplémentaires des timeseries. Les données supplémentaires des prix sont demandées au serveur, et les timeseries sont mises à jour en prenant en compte la nouvelle limite, dans le cas du défilement de la zone du graphique où il n'y a pas de données, ou lorsque les données sont demandées par un programme mql5.

Le volume des données demandées au serveur correspond au nombre demandé de barres de cette période avec le paramètre "Nb max de barres dans les graphiques" pris en compte. La restriction donnée par ce paramètre n'est pas stricte, et dans certains cas, le nombre de barres disponibles pour une période peut être supérieur à la valeur actuelle du paramètre.

Disponibilité des Données

La présence de données au format HCC ou même préparée pour être utilisées au format HC ne signifie pas toujours la disponibilité absolue de ces données pour être affichées dans un graphique ou pour être utilisées dans des programmes MQL5.

Lors de l'accès aux données des prix ou aux valeurs d'un indicateur depuis un programme mql5, il faut se souvenir que leurs disponibilités à un certain moment ou à partir d'un certain moment ne sont pas garanties. Ceci est dû au fait que dans le but d'économiser des ressources, la copie complète des données nécessaires pour un programme mql5 n'est pas stocké dans MetaTrader 5 ; seul un accès direct à la base de données du terminal est donné.

L'historique des prix pour toutes les périodes est construit à partir des données communes au format HCC, and toute mise à jour des données du serveur conduit à la mise à jour des données pour toutes les périodes et au recalcul des indicateurs. A cause de cela, l'accès aux aux données peut être fermé, même si ces données n'étaient pas disponibles auparavant.

Synchronisation des Données du Terminal et des Données du Serveur #

Puisqu'un programme mql5 peut appeler des données d'un autre symbole et d'une autre période, il y a la possibilité que les données des timeseries nécessaires ne soient pas encore formées dans le terminal ou que les données nécessaires des prix ne sont pas synchronisées avec le serveur de trades. Dans ce cas, prédire le temps de latence est difficile.

Les algorithmes utilisant des boucles "ne-fait-rien" ne sont pas la meilleure solution. La seule exception dans ce cas sont les scripts, car il n'existe pas d'autre choix d'algorithme car ils ne gérent pas d'évènements. Pour les indicateurs personnalisés, de tels algorithmes, ainsi que n'importe quelles autres boucles "ne-fait-rien", sont fortement à éviter, car ils mènent à la fin du calcul de tous les indicateurs et tout autre traitement des données des prix du symbole.

Pour les Expert Advisors et les indicateurs, il est préférable d'utiliser le modèle de gestion d'évènements. Si pendant le traitement de l'évènement OnTick() ou OnCalculate(), la réception des données pour les timeseries requises échoue, vous devriez sortir de la fonction de gestion de l'évènement, en vous basant sur la disponibilité de l'accès à l'appel suivant de la fonction.

Exemple de Script d'Ajout dans l'Historique

Considérons l'exemple d'un script qui exécute une demande d'obtention de l'historique pour le symbole sélectionné auprès du serveur de trades. Le script est prévu pour être exécuté dans le graphique d'un symbole sélectionné ; la période n'importe pas car, comme il a été mentionné auparavant, les données des prix sont reçues du serveur de trades sous la forme de données 1 minutes compréssées à partir desquelles les timeseries prédéfinies sont construites.

Il écrit toutes les actions concernant l'obtention des données dans une fonction CheckLoadHistory(symbol, timeframe, start_date) séparée :

int CheckLoadHistory(string symbol,ENUM_TIMEFRAMES period,datetime start_date)
  {
  }

La fonction CheckLoadHistory() est conçue comme fonction universelle qui peut être appelée depuis n'importe quel programme (Expert Advisor, script ou indicateur). Il nécessite donc 3 paramètres d'entrée : nom du symbole, période et date de début pour indiquer le début de l'historique des prix dont vous avez besoin.

Il insère les vérifications nécessaires dans le code de la fonction avant de demander l'historique manquant. Tout d'abord, nous devons nous assurer que le nom du symbole et la période sont corrects :

   if(symbol==NULL || symbol==""symbol=Symbol();
   if(period==PERIOD_CURRENT)     period=Period();

Assurons-nous ensuite que le symbole est disponible dans la fenêtre du MarketWatch, c'est à dire que l'historique pour le symbole sera disponible lors de l'envoi d'une demande au serveur de trades. Si un tel symbole n'existe pas dans le MarketWatch, ajoutez-le avec la fonction SymbolSelect().

   if(!SymbolInfoInteger(symbol,SYMBOL_SELECT))
    {
      if(GetLastError()==ERR_MARKET_UNKNOWN_SYMBOLreturn(-1);
      SymbolSelect(symbol,true);
     }

Nous devons maintenant récupérer la date de départ de l'historique disponible pour la paire symbole/période donnée. Peut être que la valeur du paramètre d'entrée startdate, passé à CheckLoadHistory(), est dans l'historique disponible ; la demande au serveur de trades n'est donc pas nécessaire. Pour obtenir la toute première date de la paire symbole-période à ce moment, la fonction SeriesInfoInteger() est utilisée avec le modificateur SERIES_FIRSTDATE.

   SeriesInfoInteger(symbol,period,SERIES_FIRSTDATE,first_date);
   if(first_date>0 && first_date<=start_date) return(1);

La prochaine vérification importante est le type du programme à partir duquel la fonction est appelée. Notez qu'il n'est pas souhaitable d'envoyer une demande pour mettre à jour les timeseries depuis l'indicateur dans la même période. Le caractère indésirable de la demande de données sur la même paire symbole-période que celle de l'indicateur est conditionné par le fait que la mise à jour des données de l'historique est effectuée dans le même thread que celui où l'indicateur fonctionne. La possibilité de deadlock est donc élevée. Pour le vérifier, utilisez la fonction MQL5InfoInteger() avec le modificateur MQL5_PROGRAM_TYPE.

   if(MQL5InfoInteger(MQL5_PROGRAM_TYPE)==PROGRAM_INDICATOR && Period()==period && Symbol()==symbol)
      return(-4);

Si toutes les vérifications ont été réalisées avec succès, effectuez la dernière tentative sans vous référer au serveur de trades. Trouvons d'abord la date de dpart pour laquelle les données 1 minute au format HCC sont disponibles. Récupérez cette valeur en utilisation la fonction SeriesInfoInteger() avec le modificateur SERIES_TERMINAL_FIRSTDATE et comparez-la à nouveau avec la valeur du paramètre start_date.

   if(SeriesInfoInteger(symbol,PERIOD_M1,SERIES_TERMINAL_FIRSTDATE,first_date))
     {
      //--- il y a des données chargées pour construire les timeseries
      if(first_date>0)
        {
         //--- force la construction des timeseries
         CopyTime(symbol,period,first_date+PeriodSeconds(period),1,times);
         //--- vérifie la date
         if(SeriesInfoInteger(symbol,period,SERIES_FIRSTDATE,first_date))
            if(first_date>0 && first_date<=start_date) return(2);
        }
     }

Si après toutes les vérifications, le thread d'exécution est toujours dans le corps de la fonction CheckLoadHistory(), cela signifie qu'il est nécessaire de demander les données manquantes des prix au serveur de trades. Retourne d'abord la valeur du paramètre "Nb max de barres dans le graphique" avec la fonction TerminalInfoInteger() :

  int max_bars=TerminalInfoInteger(TERMINAL_MAXBARS);

Nous en aurons besoin pour éviter de demander des données supplémentaires. Trouvons ensuite la toute première date de l'historique du symbole sur le serveur de trades (indépendamment de la période) en utilisant la fonction SeriesInfoInteger() avec le modificateur SERIES_SERVER_FIRSTDATE.

   datetime first_server_date=0;
   while(!SeriesInfoInteger(symbol,PERIOD_M1,SERIES_SERVER_FIRSTDATE,first_server_date) && !IsStopped())
      Sleep(5);

Puisque la demande est une opération asynchrone, la fonction est appelée dans la boucle avec un court délai de 5 millisecondes jusqu'à ce que la variable first_server_date reçoive une valeur, ou que la boucle d'exécution soit stoppée par l'utilisateur (IsStopped() retournera true dans ce cas). Indiquons une valeur correcte de date de départ, à partir de laquelle nous demandons les données des prix au serveur de trades.

   if(first_server_date>start_date) start_date=first_server_date;
   if(first_date>0 && first_date<first_server_date)
      Print("Attention : la première date du serveur ",first_server_date," pour ",
symbol," ne correspond pas à la date des premières séries ",first_date);

Si la date de départ first_server_date du serveur est inférieure à la date de départ first_date du symbole au format HCC, l'entrée correspondante sera écrite dans le journal.

Nous sommes maintenant prêt à effectuer une demande au serveur de trades pour demandes les données manquantes des prix. Effectuons la demande sous la forme d'une boucle et commençons à remplir son corps :

   while(!IsStopped())
     {
      //1. attendons la synchronisation entre les timeseries reconstruites et l'historique intermédiaire en HCC
      //2. récupérons le nombre courant de barres dans cette timeserie
      //    si la barre est supérieure à Max_bars_in_chart, nous pouvons sortir, le travail est terminé
      //3. récupérons la date de départ first_date dans les timeseries reconstruites et comparons-la à start_date
      //    si first_date est inférieure à start_date, nous pouvons sortir, le travail est terminé
      //4. demandons au serveur une nouvelle partie de l'historique - 100 barres à partir de la dernière barre numérotée 'bars'
     }

Les 3 premiers points sont implémentés par des moyens déjà connus.

   while(!IsStopped())
     {
      //--- 1. attends que le process de reconstruction des timeseries soit fini
      while(!SeriesInfoInteger(symbol,period,SERIES_SYNCHRONIZED) && !IsStopped())
         Sleep(5);
      //--- 2. demande combien de barres nous avons
      int bars=Bars(symbol,period);
      if(bars>0)
        {
         //--- barres supérieures à celles qui peuvent être dessinées sur le graphique, on sort
         if(bars>=max_bars) return(-2); 
         //--- 3. retourne la date de départ actuelle dans les timeseries
         if(SeriesInfoInteger(symbol,period,SERIES_FIRSTDATE,first_date))
            // la date de départ est plus récente que celle demandée, la tâche est terminée
            if(first_date>0 && first_date<=start_date) return(0);
        }
      //4. Demandons au serveur une nouvelle partie de l'historique - 100 barres à partir de la dernière barre numérotée 'bars'
     }

Il reste le quatrième et dernier point à faire - demander l'historique. Nous ne pouvons pas nous référer à un serveur directement, mais n'importe quelle fonction Copy inities automatiquement l'envoi de la demande à un serveur si l'historique au format HCC n'est pas suffisant. Puisque l'heure de la toute première date dans la variable first_date est le critère simple et naturel pour évaluer le degré d'exécution de la demande, la façon la plus simple est donc d'utiliser la fonction CopyTime().

Lors de l'appel aux fonctions de copie des données à partir des timeseries, il est à noter que le paramètre start (numéro de la barre à partir de laquelle les données des prix sont à copier) doit toujours être dans l'historique disponible du terminal. Si vous n'avez que 100 barres, il est inutile d'essayer de copier 300 barres à partir de la barre d'indice 500. Un telle demande sera considérée comme invalide et ne sera pas traitée, c'est à dire qu'aucun historique supplémentaire ne sera chargé du serveur de trades.

c'est pourquoi nous copierons les barres en groupes de 100 en commençant par la barre avec l'indice bars . Cela permettra un chargement lissé de l'historique manquant depuis le serveur de trades. En fait, un peu plus de 100 barres seront chargées, car le serveur renvoie un historique plus grand.

   int copied=CopyTime(symbol,period,bars,100,times);

Après la copie, nous devons analyser le nombre d'éléments copiés. Si la tentative échoue, alors la valeur de copied sera égale à 0 et la valeur du compteur fail_cnt sera incrémentée de 1. Après 100 échecs, l'exécution de la fonction sera stoppée.

int fail_cnt=0;
...
   int copied=CopyTime(symbol,period,bars,100,times);
   if(copied>0)
     {
      //--- vérification des données
      if(times[0]<=start_date)  return(0);  // la valeur copiée est plus petite, tout est prêt
      if(bars+copied>=max_bars) return(-2); // il y a plus de barres que ce qui peut être dessiné dans le graphique, tout est prêt
      fail_cnt=0;
     }
   else
     {
      //--- pas plus de 100 échecs à la suite
      fail_cnt++;
      if(fail_cnt>=100) return(-5);
      Sleep(10);
     }
 

Donc, non seulement la gestion correcte de la situation correcte à tout moment de l'exécution est implémentée dans la fonction, mais également le code de fin est retourné. Il peut être géré après l'appel à la fonction CheckLoadHistory() pour obtenir des informations supplémentaires. Par exemple, de cette façon :

   int res=CheckLoadHistory(InpLoadedSymbol,InpLoadedPeriod,InpStartDate);
   switch(res)
     {
      case -1 : Print("Symbole inconnu ",InpLoadedSymbol);                                           break;
      case -2 : Print("Plus de barres que ce qui peut être dessiné dans le graphique");              break;
      case -3 : Print("Exécution stoppée par l'utilisateur");                                        break;
      case -4 : Print("L'indicateur ne doit pas charger ses propres données");                       break;
      case -5 : Print("Echec du chargement");                                                        break;
      case  0 : Print("Toutes les données ont été chargées");                                        break;
      case  1 : Print("Les données déjà disponibles dans les timeseries sont suffisantes");          break;
      case  2 : Print("La timeserie est construite à partir des données disponibles du terminal");   break;
      default : Print("Résultat de l'exécution non défini");
     }

Le code complet de la fonction est disponible dans l'exemple du script qui montre l'organisation correcte de l'accès à n'importe quelle donnée dans la gestion des résultats de la demande.

Code :

//+------------------------------------------------------------------+
//|                                              TestLoadHistory.mq5 |
//|                        Copyright 2009, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "2009, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.02"
#property script_show_inputs
//--- paramètres d'entrée
input string          InpLoadedSymbol="NZDUSD";   // symbole à charger
input ENUM_TIMEFRAMES InpLoadedPeriod=PERIOD_H1;  // période à charger
input datetime        InpStartDate=D'2006.01.01'; // date de début
//+------------------------------------------------------------------+
//| Fonction de démarrage du script        |
//+------------------------------------------------------------------+
void OnStart()
  {
   Print("Démarre le chargement",InpLoadedSymbol+","+GetPeriodName(InpLoadedPeriod),"from",InpStartDate);
//---
   int res=CheckLoadHistory(InpLoadedSymbol,InpLoadedPeriod,InpStartDate);
   switch(res)
     {
      case -1 : Print("Symbole inconnu ",InpLoadedSymbol);                                                break;
      case -2 : Print("Nombre de barres demandées supérieure au nombre max de barres dans le graphique"); break;
      case -3 : Print("Le programme a été stoppé");                                                       break;
      case -4 : Print("L'indicateur ne doit pas charger ses propres données");                            break;
      case -5 : Print("Echec du chargement");                                                             break;
      case  0 : Print("Chargement réussi");                                                               break;
      case  1 : Print("Chargé précédemment");                                                             break;
      case  2 : Print("Chargé précédemment et construit");                                                break;
      default : Print("Résultat inconnu");
     }
//---
   datetime first_date;
   SeriesInfoInteger(InpLoadedSymbol,InpLoadedPeriod,SERIES_FIRSTDATE,first_date);
   int bars=Bars(InpLoadedSymbol,InpLoadedPeriod);
   Print("Première date ",first_date," - ",bars," barres");
//---
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
int CheckLoadHistory(string symbol,ENUM_TIMEFRAMES period,datetime start_date)
  {
   datetime first_date=0;
   datetime times[100];
//--- vérifie le symbole et la période
   if(symbol==NULL || symbol==""symbol=Symbol();
   if(period==PERIOD_CURRENT)     period=Period();
//--- vérifie si le symbole est sélectionné dans le Market Watch
   if(!SymbolInfoInteger(symbol,SYMBOL_SELECT))
     {
      if(GetLastError()==ERR_MARKET_UNKNOWN_SYMBOLreturn(-1);
      SymbolSelect(symbol,true);
     }
//--- vérifie si les données sont présentes
   SeriesInfoInteger(symbol,period,SERIES_FIRSTDATE,first_date);
   if(first_date>0 && first_date<=start_date) return(1);
//--- ne demande pas le chargement de ses propres données si c'est un indicateur
   if(MQL5InfoInteger(MQL5_PROGRAM_TYPE)==PROGRAM_INDICATOR && Period()==period && Symbol()==symbol)
      return(-4);
//--- deuxième tentative
   if(SeriesInfoInteger(symbol,PERIOD_M1,SERIES_TERMINAL_FIRSTDATE,first_date))
     {
      //--- il y a des données chargées pour construire les timeseries
      if(first_date>0)
        {
         //--- force la construction des timeseries
         CopyTime(symbol,period,first_date+PeriodSeconds(period),1,times);
         //--- vérifie la date
         if(SeriesInfoInteger(symbol,period,SERIES_FIRSTDATE,first_date))
            if(first_date>0 && first_date<=start_date) return(2);
        }
     }
//--- nb max de barres dans le graphique depuis les options du terminal
   int max_bars=TerminalInfoInteger(TERMINAL_MAXBARS);
//--- charge les informations de l'historique du symbole
   datetime first_server_date=0;
   while(!SeriesInfoInteger(symbol,PERIOD_M1,SERIES_SERVER_FIRSTDATE,first_server_date) && !IsStopped())
      Sleep(5);
//--- fixe la date de début du chargement
   if(first_server_date>start_date) start_date=first_server_date;
   if(first_date>0 && first_date<first_server_date)
      Print("Attention : la première date du serveur ",first_server_date," pour ",symbol,
            " ne correspond pas ) la date des premières séries",first_date);
//--- chargement des données pas à pas
   int fail_cnt=0;
   while(!IsStopped())
     {
      //--- attend la construction des timeseries
      while(!SeriesInfoInteger(symbol,period,SERIES_SYNCHRONIZED) && !IsStopped())
         Sleep(5);
      //--- demande la construction des barres
      int bars=Bars(symbol,period);
      if(bars>0)
        {
         if(bars>=max_bars) return(-2);
         //--- demande la première date
         if(SeriesInfoInteger(symbol,period,SERIES_FIRSTDATE,first_date))
            if(first_date>0 && first_date<=start_date) return(0);
        }
      //--- la copie de la partie suivante force le chargement des données
      int copied=CopyTime(symbol,period,bars,100,times);
      if(copied>0)
        {
         //--- vérifie les données
         if(times[0]<=start_date)  return(0);
         if(bars+copied>=max_bars) return(-2);
         fail_cnt=0;
        }
      else
        {
         //--- pas plus de 100 échecs
         fail_cnt++;
         if(fail_cnt>=100) return(-5);
         Sleep(10);
        }
     }
//--- stoppé
   return(-3);
  }
//+------------------------------------------------------------------+
//| Retourne une chaîne de caractères correspondant à la période |
//+------------------------------------------------------------------+
string GetPeriodName(ENUM_TIMEFRAMES period)
  {
   if(period==PERIOD_CURRENTperiod=Period();
//---
   switch(period)
     {
      case PERIOD_M1:  return("M1");
      case PERIOD_M2:  return("M2");
      case PERIOD_M3:  return("M3");
      case PERIOD_M4:  return("M4");
      case PERIOD_M5:  return("M5");
      case PERIOD_M6:  return("M6");
      case PERIOD_M10return("M10");
      case PERIOD_M12return("M12");
      case PERIOD_M15return("M15");
      case PERIOD_M20return("M20");
      case PERIOD_M30return("M30");
      case PERIOD_H1:  return("H1");
      case PERIOD_H2:  return("H2");
      case PERIOD_H3:  return("H3");
      case PERIOD_H4:  return("H4");
      case PERIOD_H6:  return("H6");
      case PERIOD_H8:  return("H8");
      case PERIOD_H12return("H12");
      case PERIOD_D1:  return("Daily");
      case PERIOD_W1:  return("Weekly");
      case PERIOD_MN1return("Monthly");
     }
//---
   return("période inconnue");
  }