English Русский 中文 Español Deutsch 日本語 Português 한국어 Italiano Türkçe
preview
Développer un Expert Advisor de trading à partir de zéro (Partie 24) : Assurer la robustesse du système (I)

Développer un Expert Advisor de trading à partir de zéro (Partie 24) : Assurer la robustesse du système (I)

MetaTrader 5Trading | 12 février 2024, 11:30
312 0
Daniel Jose
Daniel Jose

Introduction

Certaines choses ne sont pas si simples, même si certaines personnes le pensent. Le système d’ordres est l'un de ces éléments. Vous pouvez même créer un système plus modeste qui vous convient parfaitement, comme nous l'avons fait dans l'article Développer un Expert Advisor de trading à partir de zéro, dans lequel nous avons créé un système de base pouvant être utile pour de nombreuses personnes et insuffisant pour d'autres. Le moment est donc venu où tout a commencé à changer : c'est à ce moment-là qu'est née la première partie de cette série sur le nouveau système d'ordre. Ceci peut être vu dans l'article Développer un Expert Advisor de trading à partir de zéro (Partie 18). C'est là que nous avons commencé à développer un système qui peut être géré par l'EA tout en étant pris en charge par MetaTrader 5. L'idée du système était de ne pas avoir de limites sur les ordres sur le graphique. Au début, le système semblait plutôt audacieux, et je dois admettre que le fait même de créer un système dans lequel les objets seraient maintenus non pas par l'EA mais par MetaTrader 5 me semblait plutôt inutile et inefficace.

Mais le système était en cours de développement et dans l'article Développer un Expert Advisor de trading à partir de zéro (Partie 23), nous avons développé un système fantôme pour faciliter la gestion des ordres, des positions ou des niveaux de stop (Take Profit et Stop Loss). C'était très intéressant à développer, mais il y avait un problème. Si vous regardez le nombre d'objets utilisés et visibles par rapport au nombre d'objets supportés par MetaTrader 5, vous serez certainement surpris, car le nombre d'objets supportés sera toujours plus élevé.

Dans de nombreux cas, le problème n'est pas si grave, vous pouvez même vivre avec quelques moments. Mais il y a deux problèmes qui font que le système n'est pas très stable pendant les périodes de forte volatilité du marché. Dans certaines situations, ils ont forcé l'utilisateur à agir de manière incorrecte. En effet, lorsque l'opérateur ajoute un ordre en attente, le système l'envoie au serveur, et ce dernier a parfois besoin de plus de temps que d'habitude pour répondre. Le système indique à certains moments qu'il existe un ordre, et à d'autres moments qu'il n'y en a pas. Et lorsqu'il s'agit de positions (voir la documentation pour la différence entre les ordres et les positions) cela s'est avéré encore plus fastidieux car on ne savait pas si le serveur avait exécuté l’ordre comme prévu.

Il existe plusieurs façons de résoudre ce problème. Certains sont plus simples, et d'autres plus complexes. Quoi qu'il en soit, nous devons faire confiance à l'EA. Sinon il ne faut l’utiliser en aucun cas.


1.0. Plan

Le grand problème ici est de concevoir un système qui possède 2 qualités : la rapidité et la fiabilité. Dans certains types de systèmes, il est très difficile, voire impossible, de réaliser les deux. Nous essayons donc d'équilibrer les choses dans de nombreux cas. Mais comme il s'agit d'argent, de NOTRE argent, nous ne voulons pas le risquer en achetant un système qui ne présente pas ces qualités. Il ne faut pas oublier qu'il s'agit d'un système qui fonctionne en TEMPS RÉEL. C'est le scénario le plus difficile dans lequel un développeur peut s'engager, car nous devons toujours essayer d'avoir un système extrêmement rapide : il doit réagir instantanément aux événements, tout en montrant suffisamment de fiabilité pour ne pas s'effondrer lorsque nous essayons de l'améliorer. Il est donc clair que la tâche est assez difficile.

La rapidité peut être obtenue en veillant à ce que les fonctions soient appelées et exécutées de la meilleure façon, en évitant les appels inutiles à des moments encore plus inutiles. Cela permettra au système d'être aussi rapide que possible. Mais si nous voulons quelque chose d'encore plus rapide, nous devons descendre au niveau du langage machine, ce qui signifie en Assembleur. Mais c'est souvent inutile, on peut utiliser le langage C et obtenir d'aussi bons résultats.

L'un des moyens d'obtenir la robustesse souhaitée est d'essayer de réutiliser le code autant que possible afin qu'il soit constamment testé dans différents cas. Mais ce n'est qu'un moyen. Une autre méthode consiste à utiliser la Programmation Orientée Objet (POO). Si cela est fait correctement et de manière à ce que chaque classe d'objets ne manipule pas directement les données de la classe d'objets (sauf dans le cas de l'héritage) alors il suffira d'avoir un système très robuste. Cela réduit parfois la vitesse d'exécution. Mais cette réduction est si faible qu'elle peut être ignorée en raison de l'incrément exponentiel généré par l'encapsulation fournie par la classe. Cette encapsulation apporte la robustesse dont nous avons besoin.

Comme vous pouvez le constater, il n'est pas si simple d'atteindre à la fois la vitesse et la robustesse. Mais ce qui est formidable, c'est que nous n'avons pas besoin de faire tant de sacrifices que cela, comme on pourrait le penser à première vue. Nous pouvons simplement vérifier la documentation du système et voir ce qui peut être modifié pour améliorer les choses. Le simple fait que nous n'essayons pas de réinventer la roue est déjà un bon début. Mais n'oubliez pas que les programmes et les systèmes s'améliorent constamment. Nous devrions donc toujours essayer d'utiliser les éléments disponibles autant que possible. Et seulement ensuite, dans le dernier cas, réinventer vraiment la roue.

Avant que certains ne trouvent inutile de présenter les changements effectués dans cet article ou ne pensent que je modifie beaucoup le code sans vraiment le déplacer, laissez-moi m'expliquer : Lorsque nous codons quelque chose, nous n'avons aucune idée de la manière dont le code final fonctionnera. Tout ce dont nous disposons, ce sont les objectifs à atteindre. Une fois cet objectif atteint, nous commençons à examiner comment nous l'avons atteint et nous essayons d'améliorer les choses pour les rendre meilleures.

Dans le cas d'un système commercial, qu'il s'agisse d'un exécutable ou d'une bibliothèque de code, nous effectuons les modifications et les publions sous forme de mises à jour. L'utilisateur n'a pas vraiment besoin de connaître les chemins à suivre pour atteindre l'objectif, puisqu'il s'agit d'un système commercial. Et il est bon qu'il ne le sache pas. Mais puisqu’il s'agit d'un système ouvert, je ne veux pas vous faire croire que vous pouvez développer un système extrêmement efficace tout de suite, dès le départ. Penser ainsi n'est pas adéquat. C'est même une insulte, car quel que soit le niveau de connaissance d'un programmeur ou d'un développeur sur le langage à utiliser, il y aura toujours des choses à améliorer au fil du temps.

Ne considérez donc pas cette séquence comme quelque chose qui pourrait être résumé en 3 ou 4 articles. Si c'était le cas, il serait préférable de simplement créer le code, en restant dans la manière que je pensais être la plus appropriée et de le vendre. Ce n'est pas mon intention. J'ai appris à programmer en observant le code d'autres programmeurs plus expérimentés. Je connais la valeur de cette méthode. Il est beaucoup plus important de savoir comment la chose se développe au fil du temps que de simplement prendre la solution finie et d'essayer de comprendre comment elle fonctionne.

Après ces observations, passons au développement.


2.0. Implémentation

2.0.1. Nouvelle modélisation des indicateurs de position

La première chose à noter dans le nouveau format de code est le changement d'une fonction, devenue une macro.

inline string MountName(ulong ticket, eIndicatorTrade it, eEventType ev, bool isGhost = false)
{
        return StringFormat("%s%c%c%c%llu%c%c%c%s", def_NameObjectsTrade, def_SeparatorInfo, (char)it, def_SeparatorInfo, ticket, def_SeparatorInfo, (char)(isGhost ? ev + 32 : ev), def_SeparatorInfo, (isGhost ? def_IndicatorGhost : def_IndicatorReal));
}

Même si le compilateur utilise ce code à chaque fois qu'il est référencé grâce au mot réservé "inline", vous ne devez pas le considérer comme acquis, car cette fonction est appelée de nombreuses fois dans le code. Nous devons nous assurer qu'elle s'exécute aussi rapidement que possible. Notre nouveau code ressemblera donc à ceci :

#define macroMountName(ticket, it, ev, Ghost) 								 \
		StringFormat("%s%c%llu%c%c%c%c%c%c%c", def_NameObjectsTrade, def_SeparatorInfo,          \                                                                                                                                                                                                                                                                                                
                                                       ticket, def_SeparatorInfo,                        \                                                                                                                                                                                                                                        
                                                       (char)it, def_SeparatorInfo,                      \ 
                                                       (char)(Ghost ? ev + 32 : ev), def_SeparatorInfo,  \ 
                                                       (Ghost ? def_IndicatorGhost : def_IndicatorReal))

Notez que les données de l'ancienne version de la macro et celles de cette version sont différentes. Il y a une raison à ce changement, que nous aborderons plus loin dans cet article.

Mais en raison de cette modification, nous devons également apporter un petit changement au code d'une autre fonction.

inline bool GetIndicatorInfos(const string sparam, ulong &ticket, eIndicatorTrade &it, eEventType &ev)
                        {
                                string szRet[];
                                char szInfo[];
                                
                                if (StringSplit(sparam, def_SeparatorInfo, szRet) < 2) return false;
                                if (szRet[0] != def_NameObjectsTrade) return false;
                                ticket = (ulong) StringToInteger(szRet[1]);
                                StringToCharArray(szRet[2], szInfo);
                                it = (eIndicatorTrade)szInfo[0];
                                StringToCharArray(szRet[3], szInfo);
                                ev = (eEventType)szInfo[0];

                                return true;
                        }

Le changement ne concerne que l'indice qui sera utilisé pour indiquer ce qui est un ticket et ce qui est un indicateur. Rien de bien compliqué. Il s'agit d'un simple détail qui doit être fait. Sinon nous risquerions d’avoir des données incohérentes en utilisant cette fonction.

Vous pouvez vous interroger : "Pourquoi avons-nous besoin de ces changements ? Le système ne fonctionnait-il pas parfaitement ? Oui, c'est le cas. Mais il y a des choses que nous ne pouvons pas contrôler. Par exemple, lorsque le développeur de MetaTrader 5 améliore certaines fonctions qui ne sont pas utilisées dans l'EA et ne peuvent donc pas nous être utiles. La règle est d'éviter de réinventer la roue et d'utiliser les ressources disponibles. Nous devrions donc toujours essayer d'utiliser les fonctions fournies par le langage, qui dans notre cas est MQL5, et éviter de créer nos propres fonctions. Cela peut sembler absurde, mais en fait, si vous vous arrêtez et réfléchissez, vous verrez que de temps en temps la plateforme apporte des améliorations à certaines fonctions. Si vous utilisez ces mêmes fonctions, vous aurez de meilleures performances et une sécurité accrue dans vos programmes sans avoir à faire d'effort supplémentaire.

La fin justifie donc les moyens. Mais les changements apportés ci-dessus permettront-ils à l'EA de bénéficier des améliorations apportées à la bibliothèque MQL5 ? La réponse à cette question est NON. Les changements ci-dessus sont nécessaires pour s'assurer que la modélisation des noms d'objets est correcte afin que nous puissions utiliser efficacement les améliorations futures possibles apportées par les développeurs de MQL5 et de MetaTrader 5. Vous trouverez ci-dessous l'un des éléments pouvant être utiles :

inline void RemoveIndicator(ulong ticket, eIndicatorTrade it = IT_NULL)
{
        ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false);
        if ((it == IT_NULL) || (it == IT_PENDING) || (it == IT_RESULT))
                ObjectsDeleteAll(Terminal.Get_ID(), StringFormat("%s%c%llu%c", def_NameObjectsTrade, def_SeparatorInfo, ticket, (ticket > 1 ? '*' : def_SeparatorInfo)));
        else ObjectsDeleteAll(Terminal.Get_ID(), StringFormat("%s%c%llu%c%c", def_NameObjectsTrade, def_SeparatorInfo, ticket, def_SeparatorInfo, (char)it));
        ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true);
        m_InfoSelection.bIsMovingSelect = false;
        ChartRedraw();
}

La version précédente du même code est indiquée ci-dessous pour ceux qui ne s'en souviennent pas ou qui ne l'ont jamais rencontrée auparavant. Le code se présente comme suit :

inline void RemoveIndicator(ulong ticket, eIndicatorTrade it = IT_NULL)
{
#define macroDestroy(A, B)      {                                                                               \
                ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_GROUND, B));                            \
                ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_LINE, B));                              \
                ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_CLOSE, B));                             \
                ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_EDIT, B));                              \
                if (A != IT_RESULT)     ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_MOVE, B));      \
                else ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_PROFIT, B));                       \
                                }

        ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false);
        if ((it == IT_NULL) || (it == IT_PENDING) || (it == IT_RESULT))
        {
                macroDestroy(IT_RESULT, true);
                macroDestroy(IT_RESULT, false);
                macroDestroy(IT_PENDING, true);
                macroDestroy(IT_PENDING, false);
                macroDestroy(IT_TAKE, true);
                macroDestroy(IT_TAKE, false);
                macroDestroy(IT_STOP, true);
                macroDestroy(IT_STOP, false);
        } else
        {
                macroDestroy(it, true);
                macroDestroy(it, false);
        }
        ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true);
#undef macroDestroy
}

Le code semble être devenu plus compact. Mais ce n'est pas tout. La réduction du code est une chose évidente, mais la vérité est bien plus profonde. L'ancien code a été remplacé par un nouveau qui utilise mieux les ressources de la plateforme. Mais comme le modèle de noms d'objets utilisé précédemment ne permettait pas cette amélioration, nous modifions la modélisation pour pouvoir espérer bénéficier des fonctions de MQL5. Si cette fonction est améliorée pour quelque raison que ce soit, l'EA bénéficiera de cette modification sans que nous ayons à modifier sa structure. Je parle de la fonction ObjectsDeleteAll. Si nous l'utilisons correctement, MetaTrader 5 fera le nettoyage. Nous n'avons pas besoin de spécifier trop de détails, nous spécifions juste le nom des objets et laissons MetaTrader 5 faire le reste. Les points où cette fonction est utilisée sont mis en évidence dans le nouveau code. Remarquez la modélisation pour informer du préfixe qui sera utilisé. Cela n'était pas possible avant la modification de la modélisation des noms d'objets.

J'aimerais attirer votre attention sur un détail du nouveau fragment de code, mis en évidence ci-dessous :

if ((it == IT_NULL) || (it == IT_PENDING) || (it == IT_RESULT))
        ObjectsDeleteAll(Terminal.Get_ID(), StringFormat("%s%c%llu%c", def_NameObjectsTrade, def_SeparatorInfo, ticket, (ticket > 1 ? '*' : def_SeparatorInfo)));

Pourquoi pensez-vous que j'ai ajouté la partie surlignée ?

C’est parce que si le système crée un ticket avec une valeur égale à 1, tous les objets seront supprimés de l'écran dès que l’ordre en attente sera passé. Ce n’est pas clair ? L'entrée utilisée pour placer un ordre en attente a une valeur de 1, c'est-à-dire que l'indicateur 0 a en fait une valeur de 1 (et non de 0), puisque 0 est utilisé pour effectuer d'autres tests dans l'EA. Pour cette raison, la valeur initiale est 1. Un problème se pose à présent : supposons que le système de trading crée le ticket 1221766803. L'objet qui représente ce ticket aura alors pour préfixe la valeur suivante : SMD_OT#1221766803. Lorsque l'EA exécute la fonction ObjectsDeleteAll pour supprimer l'indicateur 0, le nom de l'objet est le suivant SMD_OT#1. Cela supprimera tous les objets commençant par cette valeur, y compris le système nouvellement créé. Pour résoudre ce problème, nous allons légèrement modifier le nom pour passer l’information à la fonction ObjectsDeleteAll. Nous allons ajouter un caractère supplémentaire à la fin du nom pour que la fonction sache si nous supprimons l'indicateur 0 ou un autre.

Ainsi, si l'indicateur 0 doit être supprimé, la fonction recevra la valeur SMD_OT#1#. Cela permettra d'éviter le problème. En même temps, dans le cas de l'exemple ci-dessus, la fonction recevra le nom SMD_OT#1221766803*. Il s'agit apparemment de quelque chose de simple. Mais cela peut vous laisser perplexe quant à la raison pour laquelle l'EA supprime sans cesse les objets d'un ordre nouvellement placé.

Parlons maintenant d'un détail curieux. À la fin de la fonction, il y a un appel à la fonction ChartRedraw. A quoi sert-il ici ? MetaTrader 5 n'actualise-t-il pas le graphique lui-même ? C'est le cas. Mais nous ne savons pas exactement quand cela se produira. Un autre problème se pose : tous les appels visant à placer ou à supprimer des objets sur le graphique sont synchrones, c'est-à-dire qu'ils sont exécutés à un certain moment, qui n'est pas nécessairement le meilleur ni celui auquel nous nous attendons. Mais notre système d’ordres utilisera des objets pour afficher ou gérer les ordres. Nous devons donc nous assurer que l'objet se trouve sur le graphique. Nous ne pouvons pas nous permettre de penser que MetaTrader 5 a déjà placé ou supprimé des objets du graphique, nous devons en être sûrs. C'est pourquoi nous forçons la plateforme à effectuer ce rafraîchissement.

Lorsque nous appelons ChartRedraw, nous forçons donc la plateforme à rafraîchir la liste des objets présents sur le graphique. Nous pouvons donc être sûrs qu'un certain objet est présent ou non sur le graphique. Si cela n'est toujours pas clair, passons au sujet suivant.


2.0.2. Moins d'objets = plus de vitesse

La fonction d'initialisation de la version précédente était lourde. Elle comportait de nombreux contrôles répétitifs et certains éléments étaient dupliqués. Outre quelques problèmes mineurs, le système n'a réutilisé que très peu de la capacité déjà existante. Pour tirer parti de la nouvelle modélisation, j'ai donc décidé de réduire le nombre d'objets créés lors de l'initialisation. Le système se présente maintenant comme suit :

void Initilize(void)
{
        ChartSetInteger(Terminal.Get_ID(), CHART_SHOW_OBJECT_DESCR, false);
        ChartSetInteger(Terminal.Get_ID(), CHART_SHOW_TRADE_LEVELS, false);
        ChartSetInteger(Terminal.Get_ID(), CHART_DRAG_TRADE_LEVELS, false);
        for (int c0 = OrdersTotal(); c0 >= 0; c0--) IndicatorInfosAdd(OrderGetTicket(c0));
        for (int c0 = PositionsTotal(); c0 >= 0; c0--) IndicatorInfosAdd(PositionGetTicket(c0));
}

Il semblerait que tout est différent. Et en fait, c'est le cas. Nous réutilisons maintenant une fonction qui n'a pas été suffisamment utilisée : la fonction qui ajoute des indicateurs au graphique. Jetons un coup d'œil à cette caractéristique spéciale.

inline void IndicatorAdd(ulong ticket)
{
        char ret;
                                
        if (ticket == def_IndicatorTicket0) ret = -1; else
        {
                if (ObjectGetDouble(Terminal.Get_ID(), macroMountName(ticket, IT_PENDING, EV_LINE, false), OBJPROP_PRICE) != 0) return;
                if (ObjectGetDouble(Terminal.Get_ID(), macroMountName(ticket, IT_RESULT, EV_LINE, false), OBJPROP_PRICE) != 0) return;
                if ((ret = GetInfosTradeServer(ticket)) == 0) return;
        }
        switch (ret)
        {
                case  1:
                        CreateIndicatorTrade(ticket, IT_RESULT);
                        PositionAxlePrice(ticket, IT_RESULT, m_InfoSelection.pr);
                        break;
                case -1:
                        CreateIndicatorTrade(ticket, IT_PENDING);
                        PositionAxlePrice(ticket, IT_PENDING, m_InfoSelection.pr);
                        break;
        }
        ChartRedraw();
        UpdateIndicators(ticket, m_InfoSelection.tp, m_InfoSelection.sl, m_InfoSelection.vol, m_InfoSelection.bIsBuy);
}

Examinez attentivement le code ci-dessus. Il peut sembler que le code contienne des tests inutiles. Mais ils existent pour une raison très simple. Cette fonction est le seul moyen de créer un ordre en attente ou un indicateur de position. Les deux lignes en surbrillance vérifient si l'indicateur existe. Pour cela, on vérifie si une valeur est stockée dans l'objet utilisé comme ligne. Il s'agit ici de la valeur du prix auquel se trouve l'objet. Cette valeur doit être différente de 0 si l'objet se trouve sur le graphique. Dans tous les autres cas, elle sera égale à 0, soit parce que l'objet n'existe pas, soit pour toute autre raison, qui n'a pas d'importance. La raison pour laquelle nous devons forcer l'actualisation du graphique est-elle maintenant claire ? Si ce n'était pas le cas, l'EA ajouterait des objets inutilement. Nous ne pouvons donc pas attendre que la plateforme prenne cette mesure à un moment inconnu. Il faut s'assurer que le graphique a été mis à jour. Sinon, lorsque ces contrôles sont effectués, ils signalent des éléments qui ne correspondent pas à l'état actuel des objets, ce qui rend le système moins fiable.

Bien qu'il semble que ces contrôles ralentissent la vitesse de l'EA, il s'agit d'une erreur conceptuelle. Lorsque nous procédons à ces vérifications et que nous n'essayons pas de forcer la plateforme à créer un objet qui pourrait déjà se trouver dans la file d'attente de création, nous disons à la plateforme "METTRE À JOUR MAINTENANT". Lorsque nous en avons besoin, nous vérifions ensuite si l'objet a déjà été créé. Si c'est le cas, nous l'utilisons selon nos besoins. C'est ce qu'on appelle "programmer de la bonne manière". De cette manière, nous réduisons le travail de la plateforme et évitons les vérifications inutiles pour savoir si l'objet est créé ou non. Ceci rend l'EA plus fiable, car nous savons que nous disposons des données avec lesquelles nous voulons travailler.

Comme les vérifications montrent qu'il n'y a pas d'objet correspondant au ticket spécifié, l'objet sera créé. Faites attention au fait qu'il y a une autre vérification au début pour savoir si nous créons l'indicateur 0 ou tout autre indicateur. Cela permet de s'assurer que nous n'avons pas d'objets inutiles pris en charge par MetaTrader 5 : nous n'avons que les objets que nous utilisons réellement sur le graphique. Si nous créons l'indicateur 0, aucun autre test n'est nécessaire, car nous le créerons dans des conditions très particulières et spécifiques. L'objet 0 est utilisé pour positionner les ordres en utilisant les touches SHIFT ou CTRL + la souris. Ne vous inquiétez pas, nous verrons bientôt comment cela fonctionne.

Il y a un détail important dans le code ci-dessus : pourquoi mettons-nous à jour le graphique avant d'appeler la fonction Update ? C'est inutile. Pour le comprendre, examinons la fonction UpdateIndicators ci-dessous :

void UpdateIndicators(ulong ticket, double tp, double sl, double vol, bool isBuy)
{
        double pr;
        bool b0 = false;
                                
        pr = macroGetLinePrice(ticket, IT_RESULT);
        pr = (pr > 0 ? pr : macroGetLinePrice(ticket, IT_PENDING));
        SetTextValue(ticket, IT_PENDING, vol);
        if (tp > 0)
        {
                if (b0 = (ObjectGetDouble(Terminal.Get_ID(), macroMountName(ticket, IT_TAKE, EV_LINE, false), OBJPROP_PRICE) == 0 ? true : b0))
                        CreateIndicatorTrade(ticket, IT_TAKE);
                PositionAxlePrice(ticket, IT_TAKE, tp);
                SetTextValue(ticket, IT_TAKE, vol, (isBuy ? tp - pr : pr - tp));
        }
        if (sl > 0)
        {
                if (b0 = (ObjectGetDouble(Terminal.Get_ID(), macroMountName(ticket, IT_STOP, EV_LINE, false), OBJPROP_PRICE) == 0 ? true : b0))
                        CreateIndicatorTrade(ticket, IT_STOP);
                PositionAxlePrice(ticket, IT_STOP, sl);
                SetTextValue(ticket, IT_STOP, vol, (isBuy ? sl - pr : pr - sl));
        }
        if (b0) ChartRedraw();
}

Cette fonction s'occupera essentiellement des indicateurs indiquant les limites. Regardez maintenant les 2 lignes en surbrillance : si le graphique n'est pas mis à jour, ces lignes ne se déclencheront pas et renverront une valeur de 0. Si c'est le cas, le reste du code ne fonctionnera pas et les indicateurs de limite ne s'afficheront pas correctement à l'écran.

Mais avant de créer les indicateurs de limites, nous devons procéder à quelques vérifications pour savoir s'il est vraiment nécessaire de les créer, ou s'il suffit juste de les ajuster. Cette opération s'effectue de la même manière que lors de la création de l'objet de base. Et même ici, lors de la création des objets, nous forcerons également la mise à jour du graphique afin qu'il soit toujours à jour.

Vous pouvez vous demander : "Pourquoi tant de mises à jour forcées, sont-elles vraiment nécessaires ? La réponse à cette question est un GRAND et SONORE OUI... La raison en est la fonction ci-dessous :

inline double SecureChannelPosition(void)
{
        double Res = 0, sl, profit, bid, ask;
        ulong ticket;
                                
        bid = SymbolInfoDouble(Terminal.GetSymbol(), SYMBOL_BID);
        ask = SymbolInfoDouble(Terminal.GetSymbol(), SYMBOL_ASK);
        for (int i0 = PositionsTotal() - 1; i0 >= 0; i0--) if (PositionGetSymbol(i0) == Terminal.GetSymbol())
        {
                IndicatorAdd(ticket = PositionGetInteger(POSITION_TICKET));
                SetTextValue(ticket, IT_RESULT, PositionGetDouble(POSITION_VOLUME), profit = PositionGetDouble(POSITION_PROFIT), PositionGetDouble(POSITION_PRICE_OPEN));
                sl = PositionGetDouble(POSITION_SL);
                if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
                {
                        if (ask < sl) ClosePosition(ticket);
                }else
                {
                        if ((bid > sl) && (sl > 0)) ClosePosition(ticket);
                }
                Res += profit;
        }
        return Res;
};

Vous pourriez penser que cette fonction n'a rien de spécial. Êtes-vous sûr ? FAUX ! Cette fonction contient un point clé : nous devons nous assurer que l'objet est sur le graphique. Sinon tout le code pour le créer sera appelé plusieurs fois, créant une grande file d'attente à gérer par MetaTrader 5, et certaines données peuvent être perdues ou devenir obsolètes. Tout cela rendra le système instable, moins sûr et donc peu fiable. L'appel de la fonction qui crée l'objet est mis en évidence. Si nous ne forçons pas MetaTrader 5 à mettre à jour le graphique à des moments stratégiques, nous pourrions avoir des problèmes, car la fonction ci-dessus est appelée par l'événement OnTick. Pendant les périodes de forte volatilité, le nombre d'appels provenant de OnTick est assez important, ce qui peut donner lieu à un excès d'objets dans la file d'attente, ce qui n'est pas bon du tout. Les données sont ainsi forcées à être rafraîchies via l'appel à ChartRedraw et validées via ObjectGetDouble, ce qui réduit le risque d'avoir trop d'objets dans la file d'attente.

Même sans regarder comment le système fonctionne, vous pourriez penser : "Il serait bien maintenant, en cas de suppression accidentelle de l'objet TradeLine, que l'EA le remarque, et si la vérification par ObjectGetDouble échoue et que l'indicateur échoue, que l'indicateur soit recréé". C’est l'idée. Mais il n'est pas recommandé à l'utilisateur de supprimer des objets présents dans la fenêtre de la liste des objets sans vraiment savoir de quoi il s'agit. Si vous supprimez un objet (à l'exception de TradeLine), l'EA peut ne pas remarquer qu'il n'y a plus d'indicateur, restant sans moyen d'y accéder, puisqu'il n'a tout simplement pas d'autre moyen d'accès que les boutons présents sur l'indicateur.

Le script ci-dessus serait un véritable cauchemar s'il n'y avait pas la fonction qui vient juste après et qui est responsable du maintien de l'ensemble du flux de messages au sein de la classe. Mais ce n'est pas le seul point d'entrée. Je parle de la fonction DispatchMessage. Jetons-y un coup d'œil :

void DispatchMessage(int id, long lparam, double dparam, string sparam)
{
        ulong   ticket;
        double  price;
        bool    bKeyBuy,
                bKeySell,
                bEClick;
        datetime        dt;
        uint            mKeys;
        char            cRet;
        eIndicatorTrade it;
        eEventType      ev;
                                
        static bool bMounting = false, bIsDT = false;
        static double valueTp = 0, valueSl = 0, memLocal = 0;
                                
        switch (id)
        {
                case CHARTEVENT_MOUSE_MOVE:
                        Mouse.GetPositionDP(dt, price);
                        mKeys   = Mouse.GetButtonStatus();
                        bEClick  = (mKeys & 0x01) == 0x01;    //Left mouse click
                        bKeyBuy  = (mKeys & 0x04) == 0x04;    //SHIFT pressed
                        bKeySell = (mKeys & 0x08) == 0x08;    //CTRL pressed
                        if (bKeyBuy != bKeySell)
                        {
                                if (!bMounting)
                                {
                                        Mouse.Hide();
                                        bIsDT = Chart.GetBaseFinance(m_InfoSelection.vol, valueTp, valueSl);
                                        valueTp = Terminal.AdjustPrice(valueTp * Terminal.GetAdjustToTrade() / m_InfoSelection.vol);
                                        valueSl = Terminal.AdjustPrice(valueSl * Terminal.GetAdjustToTrade() / m_InfoSelection.vol);
                                        m_InfoSelection.it = IT_PENDING;
                                        m_InfoSelection.pr = price;
                                }
                                m_InfoSelection.tp = m_InfoSelection.pr + (bKeyBuy ? valueTp : (-valueTp));
                                m_InfoSelection.sl = m_InfoSelection.pr + (bKeyBuy ? (-valueSl) : valueSl);
                                m_InfoSelection.bIsBuy = bKeyBuy;
                                if (!bMounting)
                                {
                                        IndicatorAdd(m_InfoSelection.ticket = def_IndicatorTicket0);
                                        m_TradeLine.SpotLight(macroMountName(def_IndicatorTicket0, IT_PENDING, EV_LINE, false));
                                        m_InfoSelection.bIsMovingSelect = bMounting = true;
                                }
                                MoveSelection(price);
                                if ((bEClick) && (memLocal == 0))
                                {
                                        RemoveIndicator(def_IndicatorTicket0);
                                        CreateOrderPendent(m_InfoSelection.vol, bKeyBuy, memLocal = price,  price + m_InfoSelection.tp - m_InfoSelection.pr, price + m_InfoSelection.sl - m_InfoSelection.pr, bIsDT);
                                }
                        }else if (bMounting)
                        {
                                RemoveIndicator(def_IndicatorTicket0);
                                Mouse.Show();
                                memLocal = 0;
                                bMounting = false;
                        }else if ((!bMounting) && (bKeyBuy == bKeySell))
                        {
                                if (bEClick) SetPriceSelection(price); else MoveSelection(price);
                        }
                        break;
                case CHARTEVENT_OBJECT_DELETE:
                        if (GetIndicatorInfos(sparam, ticket, it, ev))
                        {
                                if (GetInfosTradeServer(ticket) == 0) break;
                                CreateIndicatorTrade(ticket, it);
                                if ((it == IT_PENDING) || (it == IT_RESULT))
                                        PositionAxlePrice(ticket, it, m_InfoSelection.pr);
                                ChartRedraw();
				m_TradeLine.SpotLight();
                                m_InfoSelection.bIsMovingSelect = false;
                                UpdateIndicators(ticket, m_InfoSelection.tp, m_InfoSelection.sl, m_InfoSelection.vol, m_InfoSelection.bIsBuy);
                        }
                        break;
                case CHARTEVENT_CHART_CHANGE:
                        ReDrawAllsIndicator();
                        break;
                case CHARTEVENT_OBJECT_CLICK:
                        if (GetIndicatorInfos(sparam, ticket, it, ev)) switch (ev)
                        {
                                case EV_CLOSE:
                                        if ((cRet = GetInfosTradeServer(ticket)) != 0) switch (it)
                                        {
                                                case IT_PENDING:
                                                case IT_RESULT:
                                                        if (cRet < 0) RemoveOrderPendent(ticket); else ClosePosition(ticket);
                                                        break;
                                                case IT_TAKE:
                                                case IT_STOP:
							m_InfoSelection.ticket = ticket;
							m_InfoSelection.it = it;
                                                        m_InfoSelection.bIsMovingSelect = true;
                                                        SetPriceSelection(0);
                                                        break;
                                        }
                                        break;
                                case EV_MOVE:
                                        if (m_InfoSelection.bIsMovingSelect)
                                        {
                                                m_TradeLine.SpotLight();
                                                m_InfoSelection.bIsMovingSelect = false;
                                        }else
                                        {
                                                m_InfoSelection.ticket = ticket;
                                                m_InfoSelection.it = it;
                                                if (m_InfoSelection.bIsMovingSelect = (GetInfosTradeServer(ticket) != 0))
                                                m_TradeLine.SpotLight(macroMountName(ticket, it, EV_LINE, false));
                                        }
                                        break;
                        }
                        break;
        }
}

Cette fonction a subi tellement de changements que je vais devoir la décomposer en petites parties pour expliquer ce qui se passe à l'intérieur. Si vous avez déjà une expérience de la programmation, il ne vous sera pas difficile de comprendre ce qu'elle fait. Mais si vous n'êtes qu'un enthousiaste ou un programmeur MQL5 novice, la compréhension de cette fonction peut être un peu difficile. C'est pourquoi je l'expliquerai calmement dans la rubrique suivante.


2.0.3. Décomposition de la fonction DispatchMessage

Cette rubrique explique ce qui se passe dans la fonction DispatchMessage. Si vous comprenez comment cela fonctionne en regardant simplement le code, ce sujet ne vous apportera rien de nouveau.

Après les variables locales, nous déclarons les variables statiques.

static bool bMounting = false, bIsDT = false;
static double valueTp = 0, valueSl = 0, memLocal = 0;

Elles pourraient être déclarées comme variables privées dans la classe. Mais comme elles ne seront utilisées qu'à ce stade du code, il n'est pas logique que d'autres fonctions de la classe puissent les voir. Elles doivent être déclarées comme statiques, car elles doivent conserver leur valeur lorsque la fonction est appelée à nouveau. Si nous n'ajoutons pas le mot-clé "static", elles perdront leur valeur dès que la fonction se terminera. Une fois cela fait, nous commencerons à traiter les événements que MetaTrader 5 indique à l'EA.

Le premier événement est visible ci-dessous :

case CHARTEVENT_MOUSE_MOVE:
        Mouse.GetPositionDP(dt, price);
        mKeys   = Mouse.GetButtonStatus();
        bEClick  = (mKeys & 0x01) == 0x01;    //Left mouse click
        bKeyBuy  = (mKeys & 0x04) == 0x04;    //SHIFT pressed
        bKeySell = (mKeys & 0x08) == 0x08;    //CTRL pressed

Ici, nous collectons et isolons les données de la souris et de certaines touches du clavier. Une fois que nous avons fait cela, vient un long code qui commence par un test :

if (bKeyBuy != bKeySell)

Si vous appuyez sur la touche SHIFT ou CTRL, mais pas sur les deux en même temps, l'EA comprendra que vous souhaitez passer un ordre à un certain prix. Si c'est le cas, il convient de procéder à d'autres vérifications.

if (!bMounting)
{
        Mouse.Hide();
        bIsDT = Chart.GetBaseFinance(m_InfoSelection.vol, valueTp, valueSl);
        valueTp = Terminal.AdjustPrice(valueTp * Terminal.GetAdjustToTrade() / m_InfoSelection.vol);
        valueSl = Terminal.AdjustPrice(valueSl * Terminal.GetAdjustToTrade() / m_InfoSelection.vol);
        m_InfoSelection.it = IT_PENDING;
        m_InfoSelection.pr = price;
}

Si l'indicateur 0 n'a pas encore été activé, le test passe. La souris sera cachée, puis les valeurs du graphique seront capturées. Ces valeurs sont ensuite converties en points en fonction du niveau d'effet de levier indiqué par le trader dans Chart Trade. La valeur initiale à laquelle l’ordre sera passé sera affichée. Cette séquence ne doit se produire qu'une seule fois par cycle d'utilisation.

L'étape suivante consiste à créer les niveaux Take Profit et Stop Loss et à indiquer si nous allons acheter ou vendre.

m_InfoSelection.tp = m_InfoSelection.pr + (bKeyBuy ? valueTp : (-valueTp));
m_InfoSelection.sl = m_InfoSelection.pr + (bKeyBuy ? (-valueSl) : valueSl);
m_InfoSelection.bIsBuy = bKeyBuy;

Ils sont créés en dehors du cycle, car lorsque nous déplaçons la souris vers une autre fourchette de prix, nous devons également déplacer le Take Profit et le Stop Loss. Mais pourquoi le code ci-dessus ne figure-t-il pas dans le test d'assemblage ? En effet, si vous changez, relâchez la touche SHIFT et appuyez sur la touche CTRL, ou vice versa, sans déplacer la souris alors qu'il y a des indicateurs à l'écran, les valeurs des indicateurs Take Profit et Stop Loss seront échangées. Pour l’éviter, le fragment doit rester en dehors du test. Mais cela nous oblige à effectuer un nouveau test d'assemblage que l'on peut voir ci-dessous :

if (!bMounting)
{
        IndicatorAdd(m_InfoSelection.ticket = def_IndicatorTicket0);
        m_TradeLine.SpotLight(macroMountName(def_IndicatorTicket0, IT_PENDING, EV_LINE, false));
        m_InfoSelection.bIsMovingSelect = bMounting = true;
}

Pourquoi y a-t-il 2 tests ? Peut-on n’en avoir qu'un seul ? Ce serait l'idéal, mais la fonction mise en évidence dans le code ci-dessus ne nous permet pas de le faire. Il suffit de regarder IndicatorAdd pour le comprendre. Après avoir créé l'indication 0, nous la définissons comme sélectionnée et montrons qu'elle est déjà en cours d'exécution et construite. Vous pouvez ainsi le déplacer à la ligne suivante.

MoveSelection(price);

Mais même en respectant le même critère qui consiste à appuyer sur SHIFT ou CTRL pour placer un ordre en attente, il y a une dernière étape.

if ((bEClick) && (memLocal == 0))
{
        RemoveIndicator(def_IndicatorTicket0);
        CreateOrderPendent(m_InfoSelection.vol, bKeyBuy, memLocal = price,  price + m_InfoSelection.tp - m_InfoSelection.pr, price + m_InfoSelection.sl - m_InfoSelection.pr, bIsDT);
}

Cela ajoutera un ordre en attente exactement au point que nous visons. Deux conditions doivent être remplies. La première est le clic sur le bouton gauche de la souris, et la seconde est que nous ne l'ayons pas fait au même prix en une seule fois. C'est-à-dire que pour placer 2, ou plus, ordres au même prix, nous devons placer ce nouvel ordre avec un appel différent, car cela ne se produira pas dans le même appel. 

Simultanément à la suppression de l'indicateur 0 du graphique, un ordre avec des paramètres correctement remplis est envoyé au serveur de trading.

Passons maintenant à l'étape suivante.

if (bKeyBuy != bKeySell)
{

// ... code described so far ....

}else if (bMounting)
{
        RemoveIndicator(def_IndicatorTicket0);
        Mouse.Show();
        memLocal = 0;
        bMounting = false;
}

Si l'indicateur 0 a été activé mais que la condition n'a pas été remplie parce que seule la touche SHIFT ou CTRL a été enfoncée, le code mis en évidence s'exécute pour supprimer l'indicateur 0 de la liste des objets, tout en réinitialisant la souris et en laissant les variables statiques dans leur état initial. En d'autres termes, le système sera propre.

L'étape suivante et finale de la gestion de l'événement de la souris est illustrée ci-dessous :

if (bKeyBuy != bKeySell)
{

// ... previously described code ...

}else if (bMounting)
{

// ... previously described code ...

}else if ((!bMounting) && (bKeyBuy == bKeySell))
{
        if (bEClick) SetPriceSelection(price); else MoveSelection(price);
}


Le code en surbrillance est la dernière étape du traitement du message par la souris. Si nous n'avons pas mis l'indicateur 0, ni les touches SHIFT ou CTRL dans un état différent, c'est-à-dire qu'elles peuvent être pressées ou relâchées en même temps, nous avons le comportement suivant : si nous cliquons à gauche, le prix sera envoyé à l'indicateur, et si nous déplaçons seulement la souris, le prix sera utilisé pour déplacer l'indicateur. Mais une question se pose alors : quel indicateur ? Ne vous inquiétez pas, nous verrons bientôt de quel indicateur il s'agit. Mais au cas où vous vous poseriez la question, l'indicateur 0 n'utilise pas cette sélection. Si vous ne comprenez pas, revenez au début de cette section et lisez le fonctionnement du traitement des messages.

Vous trouverez ci-dessous le message suivant :

case CHARTEVENT_OBJECT_DELETE:
        if (GetIndicatorInfos(sparam, ticket, it, ev))
        {
                if (GetInfosTradeServer(ticket) == 0) break;
                CreateIndicatorTrade(ticket, it);
                if ((it == IT_PENDING) || (it == IT_RESULT))
                        PositionAxlePrice(ticket, it, m_InfoSelection.pr);
                ChartRedraw();
		m_TradeLine.SpotLight();
                m_InfoSelection.bIsMovingSelect = false;
                UpdateIndicators(ticket, m_InfoSelection.tp, m_InfoSelection.sl, m_InfoSelection.vol, m_InfoSelection.bIsBuy);
        }
        break;

Vous vous souvenez que j'ai dit plus haut que l'EA dispose d'un petit système de sécurité pour empêcher la suppression incorrecte d'indicateurs ? Ce système est contenu dans le code de traitement des messages relatifs aux événements envoyés par MetaTrader 5 lorsqu'un objet est supprimé.

Lorsque cela se produit, MetaTrader 5 signale, à l'aide du paramètre sparam le nom de l'objet supprimé, pour lequel il est vérifié s'il s'agissait d'un indicateur et, le cas échéant, lequel. L'objet concerné n'a pas d'importance. Ce que nous voulons savoir, c'est quel indicateur a été affecté. Nous vérifierons ensuite s'il y a un ordre ou une position associés à l'indicateur. Si c'est le cas, nous créerons à nouveau l'indicateur entier. Dans un cas extrême, si l'indicateur affecté est l'indicateur de base, nous le repositionnons immédiatement et forçons MetaTrader 5 à placer l'indicateur sur le graphique immédiatement, quel que soit l'indicateur. Nous supprimons l'indication de sélection et passons un ordre de mise à jour des données du seuil indicateur. 

L'événement suivant à gérer est très simple : il demande simplement de redimensionner tous les indicateurs à l'écran. Son code est présenté ci-dessous.

case CHARTEVENT_CHART_CHANGE:
        ReDrawAllsIndicator();
        break;

Voici l'événement de clic sur l'objet :

case CHARTEVENT_OBJECT_CLICK:
        if (GetIndicatorInfos(sparam, ticket, it, ev)) switch (ev)
        {
//....
        }
        break;

Il démarre comme indiqué ci-dessus : MetaTrader 5 nous indique quel objet a été cliqué afin que l'EA puisse vérifier quel type d'événement doit être traité. Jusqu'à présent, nous avons 2 événements : CLOSE (fermer) et MOVE (déplacer). Considérons tout d'abord l'événement CLOSE, qui fermera et définira la fin de l'indicateur à l'écran.

case EV_CLOSE:
        if ((cRet = GetInfosTradeServer(ticket)) != 0) switch (it)
        {
                case IT_PENDING:
                case IT_RESULT:
                        if (cRet < 0) RemoveOrderPendent(ticket); else ClosePosition(ticket);
                        break;
                case IT_TAKE:
                case IT_STOP:
			m_InfoSelection.ticket = ticket;
			m_InfoSelection.it = it;
                        m_InfoSelection.bIsMovingSelect = true;
                        SetPriceSelection(0);
                        break;
        }
        break;

L'événement de clôture a les fonctions suivantes : il utilise le ticket pour rechercher sur le serveur ce qui doit être clôturé et pour vérifier s'il y a quelque chose à clôturer, car il se peut qu'à ce moment-là le serveur l'ait déjà fait, mais que l'EA n'en soit pas encore informé. Puisque nous avons quelque chose à fermer, faisons-le correctement pour avoir les vérifications nécessaires et la bonne façon d'informer la classe de fermer ou de supprimer un indicateur du graphique.

Nous en sommes donc à la dernière étape de ce sujet :

case EV_MOVE:
        if (m_InfoSelection.bIsMovingSelect)
        {
                m_TradeLine.SpotLight();
                m_InfoSelection.bIsMovingSelect = false;
        }else
        {
                m_InfoSelection.ticket = ticket;
                m_InfoSelection.it = it;
                if (m_InfoSelection.bIsMovingSelect = (GetInfosTradeServer(ticket) != 0))
                m_TradeLine.SpotLight(macroMountName(ticket, it, EV_LINE, false));
        }
        break;

MOVE est un événement qui fait exactement cela : il sélectionne l'indicateur à déplacer. Il ne fait donc que sélectionner. Le mouvement lui-même est effectué lors d'un événement de déplacement de la souris. Rappelez-vous, au début, j'ai dit qu'il y a une condition dans laquelle nous n'avons pas affaire à l'indicateur 0, et que même dans ce cas, quelque chose bougera quand même. Ce quelque chose est indiqué à ce stade, dans l'événement de déplacement. Nous vérifions ici si un élément est sélectionné pour être déplacé. Si c'est le cas, l'indicateur qui était sélectionné sera désélectionné et ne recevra pas les événements de mouvement de la souris, et le nouvel indicateur ne sera pas sélectionné. Dans ce cas, les données du nouvel indicateur qui recevra les données de la souris seront stockées dans une structure, et cet indicateur recevra un changement qui indiquera qu'il est sélectionné. Ce changement est visible dans l'épaisseur de la ligne.


2.0.4. Une nouvelle classe d'Objet Souris

En plus des améliorations décrites ci-dessus, d'autres méritent d'être mentionnées.

Si la plupart des traders n'ont pas besoin d'un système d'indicateurs basé sur la souris dans un EA, d'autres peuvent en avoir besoin et souhaiter que le système fonctionne parfaitement. Mais le trader peut supprimer par erreur certains des objets qui composent l'indicateur de la souris, ce qui conduira à son échec. Nous pouvons heureusement l’éviter en utilisant le système d’évènements. Lorsqu'un événement de suppression d'objet est détecté et envoyé à l'EA, la classe à laquelle appartient l'objet peut recréer l'objet, ce qui assure la stabilité du système. Mais il est bon de réduire autant que possible la liste des points, de les créer au fur et à mesure des besoins et de les supprimer lorsqu'ils ne sont plus nécessaires. C'est ce que nous avons fait jusqu'à présent. Mais il manquait la classe Mouse.

Commençons par créer des définitions pour remplacer le système de création de noms de constantes.

#define def_MousePrefixName "MOUSE "
#define def_NameObjectLineH def_MousePrefixName + "H"
#define def_NameObjectLineV def_MousePrefixName + "TMPV"
#define def_NameObjectLineT def_MousePrefixName + "TMPT"
#define def_NameObjectBitMp def_MousePrefixName + "TMPB"
#define def_NameObjectText  def_MousePrefixName + "TMPI"

La nouvelle fonction d'initialisation se ensuite présente comme suit :

void Init(color c1, color c2, color c3)
{
        m_Infos.cor01 = c1;
        m_Infos.cor02 = c2;
        m_Infos.cor03 = c3;
        ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_MOUSE_MOVE, true);
        ChartSetInteger(Terminal.Get_ID(), CHART_CROSSHAIR_TOOL, false);
        Show();
}

Veuillez noter qu'elle est beaucoup plus simple que la version précédente. À ce stade, nous avons l'appel qui affichera le système de la souris. L'appel est effectué au point mis en évidence dans le code précédent. Il appellera le code qui créera effectivement un système d'indication sur l'axe des prix.

inline void Show(void)
{
        if (ObjectGetDouble(Terminal.Get_ID(), def_NameObjectLineH, OBJPROP_PRICE) == 0)
        {
                ObjectCreate(Terminal.Get_ID(), def_NameObjectLineH, OBJ_HLINE, 0, 0, 0);
                ObjectSetString(Terminal.Get_ID(), def_NameObjectLineH, OBJPROP_TOOLTIP, "\n");
                ObjectSetInteger(Terminal.Get_ID(), def_NameObjectLineH, OBJPROP_BACK, false);
        }
        ObjectSetInteger(Terminal.Get_ID(), def_NameObjectLineH, OBJPROP_COLOR, m_Infos.cor01);
}

Ce code est très intéressant : il vérifie si l'objet pointeur de la souris existe dans le prix ou non. Si la vérification est réussie, cela signifie qu'il y a une ligne sur le graphique ou quelque chose lié à la souris. Donc tout ce que nous faisons est d'ajuster la couleur de la ligne horizontale. Mais pourquoi effectuer ce contrôle ? Pour le comprendre, regardez la fonction chargée de cacher, ou plutôt d'enlever les objets connectés à la souris. Voici la fonction :

inline void Hide(void)
{
        ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false);
        ObjectsDeleteAll(Terminal.Get_ID(), def_MousePrefixName + "T");
        ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true);
        ObjectSetInteger(Terminal.Get_ID(), def_NameObjectLineH, OBJPROP_COLOR, clrNONE);
}

Il s'agit d'un mode de fonctionnement intéressant. Tous les objets connectés à la souris et ayant le nom spécifié seront supprimés du graphique MetaTrader 5 et la liste des objets sera donc toujours réduite. Mais la ligne horizontale ne sera pas supprimée, seule sa couleur sera modifiée. La fonction montrant la souris effectue ainsi un contrôle avant de créer l'objet, car celui-ci n'est pas réellement exclu de la liste des objets, il est seulement caché. Mais tous les autres objets sont supprimés de la liste des objets. Mais alors, comment allons-nous utiliser ces autres objets pendant les études ? Étant donné que les études sont des moments courts où nous voulons simplement découvrir quelques détails, il est inutile de conserver les objets dans la liste pour les utiliser une ou deux fois. Il est préférable de les créer, de réaliser l'étude, puis de les retirer de la liste, afin d'obtenir un système plus fiable.

Cela peut sembler stupide, mais le système d'ordre que nous montrons est basé sur l'utilisation d'objets. Plus il y a d'objets dans la liste, plus MetaTrader 5 devra faire de recherches dans la liste lorsque nous voulons accéder à un certain objet. Nous ne laisserons ainsi pas d'objets supplémentaires sur le graphique ou dans la liste des objets, pour garder le système aussi léger que possible.

Il convient à présent de prêter attention à la fonction DispatchMessage qui commence comme suit :

void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        int     w = 0;
        uint    key;
        static int b1 = 0;
        static double memPrice = 0;

Nous avons le code qui commencera à gérer le premier événement juste après :

switch (id)
{
        case CHARTEVENT_MOUSE_MOVE:
                Position.X = (int)lparam;
                Position.Y = (int)dparam;
                ChartXYToTimePrice(Terminal.Get_ID(), Position.X, Position.Y, w, Position.dt, Position.price);
                ObjectMove(Terminal.Get_ID(), def_NameObjectLineH, 0, 0, Position.price = Terminal.AdjustPrice(Position.price));
                if (b1 > 0) ObjectMove(Terminal.Get_ID(), def_NameObjectLineV, 0, Position.dt, 0);
                key = (uint) sparam;
                if ((key & 0x10) == 0x10)    //Middle button....
                {
                        CreateObjectsIntern();
                        b1 = 1;
                }

Lorsque nous appuyons sur le bouton du milieu de la souris, nous générons un appel. Mais ce n'est plus le cas aujourd'hui. Nous verrons ensuite ce que fait cette fonction. Notez que nous essayons de déplacer un objet qui n'existe pas car il n'est pas dans la liste des objets supportés par MetaTrader 5. Cet appel ne se produit que lorsque le bouton du milieu de la souris est enfoncé. Notez que la variable b1 qui contrôle à quel moment le trader se trouve à l'intérieur de l'ensemble impliqué dans la génération de l'étude.

Dès que l'utilisateur clique sur le bouton gauche de la souris et que la première étape est terminée, le code suivant s'exécute :

if (((key & 0x01) == 0x01) && (b1 == 1))
{
        ChartSetInteger(Terminal.Get_ID(), CHART_MOUSE_SCROLL, false);
        ObjectMove(Terminal.Get_ID(), def_NameObjectLineT, 0, Position.dt, memPrice = Position.price);
        b1 = 2;
}

Il positionnera la ligne de tendance et appellera l'étape suivante dans laquelle la valeur de la variable b1 est modifiée. À ce stade, nous pouvons passer au fragment de code suivant.

if (((key & 0x01) == 0x01) && (b1 == 2))
{
        ObjectMove(Terminal.Get_ID(), def_NameObjectLineT, 1, Position.dt, Position.price);
        ObjectSetInteger(Terminal.Get_ID(), def_NameObjectLineT, OBJPROP_COLOR, (memPrice > Position.price ? m_Infos.cor03 : m_Infos.cor02));
        ObjectSetInteger(Terminal.Get_ID(), def_NameObjectText, OBJPROP_COLOR, (memPrice > Position.price ? m_Infos.cor03 : m_Infos.cor02));
        ObjectMove(Terminal.Get_ID(), def_NameObjectBitMp, 0, Position.dt, Position.price);
        ObjectSetInteger(Terminal.Get_ID(), def_NameObjectBitMp, OBJPROP_ANCHOR, (memPrice > Position.price ? ANCHOR_RIGHT_UPPER : ANCHOR_RIGHT_LOWER));
        ObjectSetString(Terminal.Get_ID(), def_NameObjectText, OBJPROP_TEXT, StringFormat("%.2f ", Position.price - memPrice));
        ObjectMove(Terminal.Get_ID(), def_NameObjectText, 0, Position.dt, Position.price);
        ObjectSetInteger(Terminal.Get_ID(), def_NameObjectText, OBJPROP_ANCHOR, (memPrice > Position.price ? ANCHOR_RIGHT_UPPER : ANCHOR_RIGHT_LOWER));
}

Le fragment ci-dessus est ce qui affichera l'étude à l'écran. Tous les objets qui se trouvent dans ce fragment n'existeront pas lorsque l'étude sera terminée : ils seront créés et détruits dans le cadre de cette routine. Bien que cela ne semble pas très efficace, je n'ai pas remarqué de diminution ou d'augmentation du temps de traitement au cours de la phase d'étude. J'ai même plutôt constaté une légère amélioration du système d’ordre, quelque chose de très subtil, qui se situe pratiquement dans la marge d'erreur de l'estimation comparative. Je ne peux donc pas dire que ces changements ont réellement apporté des améliorations en termes de traitement.

Mais notez que l'étude sera réalisée tant que le bouton gauche de la souris sera enfoncé : dès que nous le relâcherons, le fragment suivant sera exécuté.

if (((key & 0x01) != 0x01) && (b1 == 2))
{
        b1 = 0;
        ChartSetInteger(Terminal.Get_ID(), CHART_MOUSE_SCROLL, true);
        Hide();
        Show();
}
Position.ButtonsStatus = (b1 == 0 ? key : 0);

Ici, nous supprimons de la liste des objets tous les objets utilisés pour créer l'étude. Montrons à nouveau la ligne de la souris à l'écran. Le code mis en évidence est une excellente idée car il empêche toute fonction ou sous-programme à l'intérieur de l'EA d'obtenir de fausses lectures lorsque nous capturons les boutons de la souris. Si une étude est en cours, l'EA doit ignorer les boutons. Nous utilisons les lignes surlignées à cette fin. Ce n'est pas une solution parfaite, mais c'est mieux que rien.

Nous n'avons pas pris en compte le code qui crée les objets nécessaires à l'exécution de l'étude. Mais comme il s'agit d'une fonction assez simple, je ne m'y attarderai pas dans cet article.


Conclusion

Bien que les changements puissent sembler mineurs, ils font tous une grande différence pour le système lui-même. Il ne faut pas oublier une chose : notre système d’ordres est basé sur des objets graphiques à l'écran. Donc, plus l'EA traite d'objets, plus ses performances seront faibles lorsque nous demanderons un objet particulier. Pour compliquer encore la situation, le système fonctionne en temps réel, c'est-à-dire que plus le système de notre EA est rapide, meilleures sont ses performances. Par conséquent, moins l'EA a de tâches à accomplir, mieux c'est. Idéalement, il devrait pouvoir fonctionner uniquement avec le système d'ordres. Nous devrions prendre tout le reste à un autre niveau, et MetaTrader 5 devrait s'en occuper. Nous le ferons, bien sûr, mais progressivement, car nous devrons procéder à de nombreux petits changements. Mais rien de trop compliqué. Cela sera fait dans les prochains articles consacrés uniquement à l'amélioration de la fiabilité de l'EA.

Je peux dire une chose avec certitude : à l'avenir, l'EA ne sera responsable que du système d’ordres. Dans le prochain article, nous donnerons à l'EA un aspect final très intéressant : nous réduirons encore le nombre d'objets présents dans la liste pendant le fonctionnement de l'EA, puisque le système d'ordres est un gros générateur d'objets, et nous verrons comment modifier ce système de manière à minimiser la charge qu'il crée sur MetaTrader 5.

Pour cette raison, je ne joins pas de modifications à cet article car le code lui-même sera toujours sujet à des changements. Mais ne vous inquiétez pas, cela vaut la peine d'attendre le prochain article. Ces changements amélioreront considérablement les performances globales de notre Expert Advisor. Nous vous donnons donc rendez-vous dans le prochain article de cette série.


Traduit du portugais par MetaQuotes Ltd.
Article original : https://www.mql5.com/pt/articles/10593

Évaluation des modèles ONNX à l'aide de mesures de régression Évaluation des modèles ONNX à l'aide de mesures de régression
La régression consiste à prédire une valeur réelle à partir d'un exemple non étiqueté. Les mesures dites de régression sont utilisées pour évaluer la précision des prédictions des modèles de régression.
Apprenez à concevoir un système de trading basé sur l’Alligator Apprenez à concevoir un système de trading basé sur l’Alligator
Dans cet article, nous compléterons notre série sur la façon de concevoir un système de trading basé sur les indicateurs techniques les plus populaires. Nous apprendrons comment créer un système de trading basé sur l'indicateur Alligator.
Développer un Expert Advisor de trading à partir de zéro (Partie 25) : Assurer la robustesse du système (II) Développer un Expert Advisor de trading à partir de zéro (Partie 25) : Assurer la robustesse du système (II)
Dans cet article, nous franchirons la dernière étape pour améliorer les performances de l’EA. Alors préparez-vous à une longue lecture. Pour fiabiliser notre Expert Advisor, nous allons d'abord supprimer du code tout ce qui ne fait pas partie du système de trading.
Envelopper les modèles ONNX dans des classes Envelopper les modèles ONNX dans des classes
La programmation orientée objet permet de créer un code plus compact, facile à lire et à modifier. Nous examinerons ici l'exemple de 3 modèles ONNX.