Plusieurs indicateurs sur un même graphique (Partie 06) : Transformer MetaTrader 5 en un système RAD (II)

Daniel Jose | 19 août, 2022

Introduction

Dans mon précédent article, je vous ai montré comment créer un Chart Trade en utilisant les objets MetaTrader 5 et transformer ainsi la plateforme en un système RAD. Le système fonctionne très bien. Il est certain que de nombreux lecteurs auraient créé une bibliothèque, ce qui permettrait d'avoir des fonctionnalités étendues dans le système proposé. A partir de là, il serait possible de développer un Expert Advisor plus intuitif, avec une interface plus agréable et plus facile à utiliser.

L'idée est si bonne qu'elle m'a incité à vous montrer étape par étape comment ajouter des fonctionnalités. Je vais donc implémenter ici deux nouvelles fonctionnalités majeures (ces informations serviront de base pour implémenter d'autres fonctionnalités). La créativité et les éléments existants sont vos seules limites.


Plan

Les changements de notre IDE seront affichés comme dans les images suivantes :

         

Comme vous pouvez le voir sur l’image ci-dessus, il y a quelques changements mineurs au design lui-même. Deux nouvelles zones ont été ajoutées : l’une affichera le nom de l’actif, et l’autre affichera la valeur cumulée de la journée. Ce sont des choses mineures qui n’affecteront pas nos décisions. Mais elles sont quand même intéressantes. Je vais vous montrer la façon correcte la plus simple pour ajouter une fonctionnalité à notre IDE. Ouvrez la liste des objets dans la nouvelle interface. Elle apparaît comme ceci :


Les deux objets entourés n’ont pas d’évènements associés, ce qui signifie qu’ils ne fonctionneront pas dans l’IDE. Tous les autres objets sont correctement associés à des évènements, et MetaTrader 5 exécute ces évènements lorsqu’ils se produisent dans l’EA. Cela signifie que nous pouvons modifier l’interface de l’IDE comme nous voulons. Mais tant que la fonctionnalité n’est pas implémentée, MetaTrader 5 ne fera rien d’autre qu’afficher l’objet sur le graphique. Nous avons besoin que l’objet EDIT 00 affiche le nom de l’actif en cours, de façon centrée. L’objet EDIT 01 affichera la valeur cumulée pour une certaine période. Nous utiliserons la période Daily pour savoir si nous sommes en profit ou en perte sur la journée. Si la valeur est négative, elle sera affichée dans une couleur. Si elle est positive, une autre couleur sera utilisée.

Les 2 valeurs ne peuvent bien sûr pas être changées par l’utilisateur, nous laisserons donc la propriété en lecture seule, comme affichée dans la figure ci-dessous.


Gardez cependant en tête qu’il est impossible de spécifier comment l’information sera affichée, c’est à dire que nous ne pouvons pas aligner le texte pour qu’il apparaisse centré au milieu de l’objet. Si nous le désirons, cela peut être réalisé avec le code puisqu’il existe une propriété qui centre le texte. Consultez "Propriété d’un objet" pour plus de détails. L’énumération ENUM_ALIGN_MODE contient les objets sur lesquels vous pouvez justifier du texte

Quelque soit la modification que nous allons incorporer, la première chose à faire est de préparer un plan : définir la nouvelle fonctionnalité, la façon dont elle sera présentée et la façon dont l’utilisateur pourra l’utiliser. Ceci permettra de sélectionner le bon objet et la bonne configuration autant que possible en utilisant la propre interface de MetaTrader 5. Nous aurons ainsi un IDE tout prêt, et nous n’aurons qu’à l’ajuster avec du code MQL5 pour que notre IDE soit opérationnel à 100%. Effectuons maintenant les modifications.


Modifications

Pour ne pas que le code devienne un véritable monstre, nous devons nous organiser autant que possible pour vérifier quelles fonctionnalités existent déjà et celles que nous devons implémenter. Dans de nombreux cas, il suffit d’apporter quelques changements mineurs au code existant, en ajouter et le tester. Ce nouveau code sera réutilisé dans celui que nous allons implémenter. La seule chose qu’il nous restera à faire sera de tester les fonctions de contrôle que nous ajouterons pour créer une toute nouvelle fonctionnalité. Les bons programmeurs font toujours cela : ils réutilisent du code existant d’une certaine façon, en y ajoutant des points de contrôle.


Modification 1 : Ajout du nom de l’actif

Pour implémenter cette partie, nous n’avons pas besoin de changements complexes, mais ils doivent toutefois être mis aux bons endroits. Ajoutons d’abord une nouvelle valeur à l’énumération, le code source est affiché ci-dessous :

enum eObjectsIDE {eRESULT, eBTN_BUY, eBTN_SELL, eCHECK_DAYTRADE, eBTN_CANCEL, eEDIT_LEVERAGE, eEDIT_TAKE, eEDIT_STOP};



Voici le nouveau code. La partie surlignée correspond à notre ajout. Notez que je n’ajoute la nouvelle valeur ni au début ni à la fin de l’énumération. Cela permet d’éviter d’avoir à modifier d’autres parties du code existant qui fonctionnent déjà.

enum eObjectsIDE {eRESULT, eLABEL_SYMBOL, eBTN_BUY, eBTN_SELL, eCHECK_DAYTRADE, eBTN_CANCEL, eEDIT_LEVERAGE, eEDIT_TAKE, eEDIT_STOP};



Si vous ajoutez la nouvelle valeur au début ou à la fin de l’énumération, vous aurez à modifier tous les endroits où ces deux limites sont utilisées. Dans de nombreux cas, ces oublis malencontreux peuvent générer des erreurs difficiles à trouver. Vous pourriez penser que ces erreurs apparaissent à cause des ajouts, alors que ce n’est pas le cas. C’est pourquoi il est préférable d’effectuer ces changements entre les valeurs extrêmes.

Immédiatement après, nous devons ajouter un message au système pour qu’il ne retourne pas une erreur de type RunTime. Ajoutons donc la ligne suivante dans notre code source :

static const string C_Chart_IDE::szMsgIDE[] = {
                                                "MSG_RESULT",
                                                "MSG_NAME_SYMBOL",
                                                "MSG_BUY_MARKET",
                                                "MSG_SELL_MARKET",
                                                "MSG_DAY_TRADE",
                                                "MSG_CLOSE_POSITION",
                                                "MSG_LEVERAGE_VALUE",
                                                "MSG_TAKE_VALUE",
                                                "MSG_STOP_VALUE"
                                              };



Comme vous pouvez le constater, nous l’avons ajouter au même endroit pour conserver le même ordre. Mais pour le moment, la constante peut être ajoutée n’importe où. Cela ne fera pas de différence puisqu’elle n’est utilisée que pour vérifier quel objet recevra l’évènement. Pour des raisons de structure, nous l’avons ajoutée comme deuxième message.

Retournons maintenant à MetaTrader 5 et faisons les changements affichés ci-dessous :

         

MetaTrader 5 reconnaît maintenant l’objet dans notre IDE comme un objet recevant un message. Il ne reste plus qu’à créer une procédure pour envoyer l’évènement. Le texte du message ne devrait être ajouté qu’une seule fois, et il peut être envoyé dès que MetaTrader 5 place notre IDE sur le graphique. Cela peut être fait en ajoutant simplement le code nécessaire à la fin de la fonction Create de notre classe. Encore une fois, pour que le code ne se transforme pas en un monstre plein de corrections, nous ajouterons le nouveau code dans la fonction DispatchMessage. La fonction d’origine ressemble à ceci :

void DispatchMessage(int iMsg, string szArg, double dValue = 0.0)
{
        if (m_CountObject < eEDIT_STOP) return;
        switch (iMsg)
        {
                case CHARTEVENT_CHART_CHANGE:
                        if (szArg == szMsgIDE[eRESULT])
                        {
                                ObjectSetInteger(Terminal.Get_ID(), m_ArrObject[eRESULT].szName, OBJPROP_BGCOLOR, (dValue < 0 ? clrLightCoral : clrLightGreen));
                                ObjectSetString(Terminal.Get_ID(), m_ArrObject[eRESULT].szName, OBJPROP_TEXT, DoubleToString(dValue, 2));
                        }
                        break;
                case CHARTEVENT_OBJECT_CLICK:

// ... The rest of the code...

        }
}



Voici maintenant le code après les modifications :

void DispatchMessage(int iMsg, string szArg, double dValue = 0.0)
{
        if (m_CountObject < eEDIT_STOP) return;
        switch (iMsg)
        {
                case CHARTEVENT_CHART_CHANGE:
                        if (szArg == szMsgIDE[eRESULT])
                        {
                                ObjectSetInteger(Terminal.Get_ID(), m_ArrObject[eRESULT].szName, OBJPROP_BGCOLOR, (dValue < 0 ? clrLightCoral : clrLightGreen));
                                ObjectSetString(Terminal.Get_ID(), m_ArrObject[eRESULT].szName, OBJPROP_TEXT, DoubleToString(dValue, 2));
                        }else if (szArg == szMsgIDE[eLABEL_SYMBOL])
                        {
                                ObjectSetString(Terminal.Get_ID(), m_ArrObject[eLABEL_SYMBOL].szName, OBJPROP_TEXT, Terminal.GetSymbol());
                                ObjectSetInteger(Terminal.Get_ID(), m_ArrObject[eLABEL_SYMBOL].szName, OBJPROP_ALIGN, ALIGN_CENTER);
                        }
                        break;
                case CHARTEVENT_OBJECT_CLICK:

// ... The rest of the code

        }
}



Après avoir créé la fonction d’envoi, nous pouvons choisir l’emplacement à partir duquel le message sera envoyé. Le meilleur endroit est à la fin de la fonction Create de notre classe. Le code final ressemblera donc à ceci :

bool Create(int nSub)
{
        m_CountObject = 0;
        if ((m_fp = FileOpen("Chart Trade\\IDE.tpl", FILE_BIN | FILE_READ)) == INVALID_HANDLE) return false;
        FileReadInteger(m_fp, SHORT_VALUE);
                                
        for (m_CountObject = eRESULT; m_CountObject <= eEDIT_STOP; m_CountObject++) m_ArrObject[m_CountObject].szName = "";
        m_SubWindow = nSub;
        m_szLine = "";
        while (m_szLine != "</chart>")
        {
                if (!FileReadLine()) return false;
                if (m_szLine == "<object>")
                {
                        if (!FileReadLine()) return false;
                        if (m_szLine == "type")
                        {
                                if (m_szValue == "102") if (!LoopCreating(OBJ_LABEL)) return false;
                                if (m_szValue == "103") if (!LoopCreating(OBJ_BUTTON)) return false;
                                if (m_szValue == "106") if (!LoopCreating(OBJ_BITMAP_LABEL)) return false;
                                if (m_szValue == "107") if (!LoopCreating(OBJ_EDIT)) return false;
                                if (m_szValue == "110") if (!LoopCreating(OBJ_RECTANGLE_LABEL)) return false;
                        }
                }
        }
        FileClose(m_fp);
        DispatchMessage(CHARTEVENT_CHART_CHANGE, szMsgIDE[eLABEL_SYMBOL]);
        return true;
}


Le code ajouté est surligné en vert. Notez que sans n’avoir presque pas implémenté de changements, nous avons déjà un flux d’évènements qui marche à 100%. Nous pouvons maintenant passer au message à implémenter suivant.


Modification 2 : Ajout de la valeur cumulée de la journée

Nous suivrons encore la même logique que lorsque nous avons ajouter le nom de l’actif. Le nouveau code ressemblera donc à ceci :

enum eObjectsIDE {eRESULT, eLABEL_SYMBOL, eROOF_DIARY, eBTN_BUY, eBTN_SELL, eCHECK_DAYTRADE, eBTN_CANCEL, eEDIT_LEVERAGE, eEDIT_TAKE, eEDIT_STOP};

// ... Rest of the code

static const string C_Chart_IDE::szMsgIDE[] = {
                                                "MSG_RESULT",
                                                "MSG_NAME_SYMBOL",
                                                "MSG_ROOF_DIARY",
                                                "MSG_BUY_MARKET",
                                                "MSG_SELL_MARKET",
                                                "MSG_DAY_TRADE",
                                                "MSG_CLOSE_POSITION",
                                                "MSG_LEVERAGE_VALUE",
                                                "MSG_TAKE_VALUE",
                                                "MSG_STOP_VALUE"
                                              };



Après cela, changeons l’IDE avec un nouvel évènement :

         

Notre nouvel IDE est prêt. Nous implémenterons maintenant le code qui crée le message contenant la valeur cumulée de la journée. Nous devons d’abord décider dans quelle classe nous allons implémenter cette fonctionnalité. De nombreuses personnes créeraient probablement cette fonction ici, dans la classe C_Chart_IDE. Mais pour des raisons d’organisation, il est préférable de la mettre avec les fonctions sur les ordres. Le code sera donc implémenté dans la classe C_OrderView. Son code est affiché ci-dessous :

double UpdateRoof(void)
{
        ulong   ticket;
        int     max;
        string  szSymbol = Terminal.GetSymbol();
        double  Accumulated = 0;
                                
        HistorySelect(macroGetDate(TimeLocal()), TimeLocal());
        max = HistoryDealsTotal();
        for (int c0 = 0; c0 < max; c0++) if ((ticket = HistoryDealGetTicket(c0)) > 0)
                if (HistoryDealGetString(ticket, DEAL_SYMBOL) == szSymbol)
                        Accumulated += HistoryDealGetDouble(ticket, DEAL_PROFIT);
                                                
        return Accumulated;
}



Maintenant que le code a été implémenté, nous devons ajouter le message au système. Pour faciliter son utilisation, j’ai déjà ajouté le code pour retourner les résultats finalisés. Voici le code :

void DispatchMessage(int iMsg, string szArg, double dValue = 0.0)
{
        static double AccumulatedRoof = 0.0;
        bool    b0;
        double  d0;

        if (m_CountObject < eEDIT_STOP) return;
        switch (iMsg)
        {
                case CHARTEVENT_CHART_CHANGE:
                        if ((b0 = (szArg == szMsgIDE[eRESULT])) || (szArg == szMsgIDE[eROOF_DIARY]))
                        {
                                if (b0)
                                {
                                        ObjectSetInteger(Terminal.Get_ID(), m_ArrObject[eRESULT].szName, OBJPROP_BGCOLOR, (dValue < 0 ? clrLightCoral : clrLightGreen));
                                        ObjectSetString(Terminal.Get_ID(), m_ArrObject[eRESULT].szName, OBJPROP_TEXT, DoubleToString(dValue, 2));
                                }else
                                {
                                        AccumulatedRoof = dValue;
                                        dValue = 0;
                                }
                                d0 = AccumulatedRoof + dValue;
                                ObjectSetString(Terminal.Get_ID(), m_ArrObject[eROOF_DIARY].szName, OBJPROP_TEXT, DoubleToString(MathAbs(d0), 2));
                                ObjectSetInteger(Terminal.Get_ID(), m_ArrObject[eROOF_DIARY].szName, OBJPROP_BGCOLOR, (d0 >= 0 ? clrForestGreen : clrFireBrick));
                        }else   if (szArg == szMsgIDE[eLABEL_SYMBOL])
                        {
                                ObjectSetString(Terminal.Get_ID(), m_ArrObject[eLABEL_SYMBOL].szName, OBJPROP_TEXT, Terminal.GetSymbol());
                                ObjectSetInteger(Terminal.Get_ID(), m_ArrObject[eLABEL_SYMBOL].szName, OBJPROP_ALIGN, ALIGN_CENTER);
                        }
                        break;
                case CHARTEVENT_OBJECT_CLICK:

// .... The rest of the code....

        }
}



Les parties surlignées supportent le système comme nous l’avons décrit. Si le code n’était pas implémenté de cette façon, nous devrions envoyer 2 messages au système pour mettre à jour l’information correctement. Mais avec cette façon, nous pouvons suivre le résultat des positions ouvertes et du résultat du jour en un seul message.

Une autre modification de l’EA concerne la fonction OnTrade. Elle ressemble à ceci :

void OnTrade()
{
        SubWin.DispatchMessage(CHARTEVENT_CHART_CHANGE, C_Chart_IDE::szMsgIDE[C_Chart_IDE::eROOF_DIARY], NanoEA.UpdateRoof());
        NanoEA.UpdatePosition();
}



Bien que le système fonctionne, vous devez faire attention avec le temps d’exécution de la fonction OnTrade, qui peut dégrader les performances de l’EA avec la fonction OnTick. Le code de la fonction OnTick n’est pas très bon et son optimisation devient critique. Mais il est plus facile avec la fonction OnTrade puisque la fonction n’est appelée qu’en cas de changement dans la position. Sachant cela, nous avons 2 alternatives : La première est de modifier la position UpdateRoof pour limiter son temps d’exécution. La deuxième alternative est de modifier la fonction OnTrade elle-même. Pour des raisons pratiques, nous modifierons la fonction Update Roof, et nous améliorerons donc un peu le temps d’exécution de la fonction lorsque nous aurons une position ouverte. La nouvelle fonction correspond à ceci :

double UpdateRoof(void)
{
        ulong           ticket;
        string  szSymbol = Terminal.GetSymbol();
        int             max;
        static int      memMax = 0;
        static double   Accumulated = 0;
                
        HistorySelect(macroGetDate(TimeLocal()), TimeLocal());
        max = HistoryDealsTotal();
        if (memMax == max) return Accumulated; else memMax = max;
        for (int c0 = 0; c0 < max; c0++) if ((ticket = HistoryDealGetTicket(c0)) > 0)
                if (HistoryDealGetString(ticket, DEAL_SYMBOL) == szSymbol)
                        Accumulated += HistoryDealGetDouble(ticket, DEAL_PROFIT);
                                                
        return Accumulated;
}


Les lignes surlignées montrent le code ajouté à la fonction originale. Même si cela ne paraît pas être un gros changement, cela fait toute la différence. Voyons pourquoi. Lorsque le code est référencé pour la première fois, les deux variables statiques memMax et Accumulated seront initialisées à 0 s’il n’y a pas de valeur dans l’historique pour la période spécifiée. Le test reflétera ceci et on sortira de la fonction s’il n’y a pas de données. Dans le cas contraire, le code sera testé, et memMax et Accumulated retourneront toutes les deux les nouvelles valeurs. Le fait que ces variables soient statiques signifie que leurs valeurs sont conservées entre 2 appels. Lorsque la valeur d’une position change en raison du mouvement naturel du prix de l’actif, MetaTrader 5 génère un évènement qui appellera la fonction OnTrade. A ce moment, nous avons un nouvel appel à la fonction UpdateRoof, et si la position n’a pas été fermée, la fonction retournera le point de contrôle, qui accélérera le retour.


Conclusion

Dans cet article, nous avons vu comment ajouter une nouvelle fonctionnalité au système RAD permettant la création d’une bibliothèque qui rend le système idéal pour créer l’interface d’un IDE avec beaucoup plus de simplicité et moins d’erreurs lors de la construction des interactions et de l’interface de contrôle. Maintenant, la seule limite sera votre créativité puisque nous n’avons parlé ici que de l’utilisation de MQL5. Mais vous pouvez intégrer la même idée avec des bibliothèques externes, et étendre ainsi les possibilités de création d’un IDE.