Le MQL5 Cookbook : Gestion des événements du graphique personnalisés
Introduction
Cet article est la suite logique de l'article MQL5 Cookbook : Gestion des événements graphiques typiques. Il couvre les méthodes de travail avec des événements graphiques personnalisés. Ici, le lecteur peut trouver des exemples de développement et de gestion d'événements personnalisés. Toutes les idées discutées dans cet article ont été implémentées avec des outils orientés objet.
Comme le thème des événements personnalisés est assez large, c'est le cas lorsqu'un programmeur et un développeur peuvent introduire de la créativité dans leur travail.
1. Événement de graphique personnalisé
Il est clair que cet événement est défini par l'utilisateur. C'est au programmeur de décider quoi exactement et quelle tâche ou un bloc de programme pourrait prendre la forme d'un événement. Les développeurs MQL5 peuvent créer leurs propres événements, ce qui étend les capacités du langage pour la mise en œuvre d'algorithmes complexes.
Un événement personnalisé est le deuxième type possible d'événement de graphique. Le premier est un événement typique. Bien qu'il n'y ait pas de terme tel que « événement de graphique typique » dans la documentation, je suggère toujours de l'utiliser pour les dix premiers types d'événements de graphique.
Les développeurs suggèrent une seule énumération pour tous les événements de graphique : ENUM_CHART_EVENT.
Selon la documentation, il existe 65535 identifiants d'événements personnalisés. Le premier et le dernier identifiants des événements personnalisés sont définis par les valeurs explicites de CHARTEVENT_CUSTOM et CHARTEVENT_CUSTOM_LAST, qui sont numériquement égales à 1000 et 66534 en conséquence (Fig.1).
Fig.1 Le premier et le dernier identifiants des événements personnalisés
Des calculs simples prenant en compte le premier et le dernier identifiant produiront : 66534-1000+1=65535.
Avant d'utiliser des événements personnalisés, ils doivent d'abord être conçus. En ce sens, un développeur devient un cerveau et l'auteur du concept de l'événement, qui est ensuite implémenté comme un algorithme pour le futur Expert. Il serait utile d'avoir une classification des événements personnalisés. Cette méthode cognitive ne permettra pas de lever l'ambiguïté mais en réduira certainement le degré et arrangera le raisonnement.
Considérons un tel critère d'événement personnalisé comme source. Par exemple, le développeur Sergeev a suggéré une idée de prototype de robot de trading. Il divise tous les événements en trois groupes (Fig.2).
Fig.2 Groupes de sources d'événements personnalisées
Ensuite, selon cette idée principale, les événements personnalisés sont à développer en fonction de leur affiliation à un groupe.
Essayons de faire quelque chose de simple pour commencer. Dans un premier temps, nous prendrons le premier groupe, qui comprend les événements indicateurs. Les événements pouvant appartenir à ce groupe sont : la création et la suppression d'un indicateur, la réception d'un signal d'ouverture et de fermeture d'une position. Le deuxième groupe comprend les événements de changement d'état des ordres et des positions. Dans notre exemple, l'ouverture et la fermeture des positions seront dans ce groupe. Tout est très simple. Et, enfin, le groupe le plus complexe pour la formalisation est un groupe d'événements externes.
Prenons deux événements : activer et désactiver le trading manuel.
Fig.3 Sources d'événements personnalisés
Le modèle principal peut être établi par la méthode déductive (du général au spécial) (Fig.3). C'est le modèle même que nous allons utiliser plus tard pour créer des types d'événements dans la classe correspondante (tableau 1).
Tableau 1 Événements personnalisés
Ce tableau ne peut pas encore être qualifié de « concept d'événement », mais c'est un début. Voici une autre approche. Il est de notoriété publique qu'un modèle de système de trading abstrait se compose de trois sous-systèmes : des modules de base (Fig.4).
Fig.4 Modèle d'un système de trading abstrait
Les événements personnalisés basés sur le critère « source » peuvent être classés comme des événements générés dans :
- le sous-système de signalisation ;
- sous-système des positions ouvertes de suivi ;
- sous-système de gestion de l'argent.
Ce dernier, par exemple, peut inclure des événements tels que l'atteinte du niveau de tirage autorisé, l'augmentation d'un volume de trading d'une valeur définie, l'augmentation du pourcentage d'une limite de perte, etc.
2. Gestionnaire et générateur de ChartEvent
Les quelques lignes suivantes seront consacrées au gestionnaire et au générateur d'événements graphiques. Quant à la gestion d'un événement de graphique personnalisé, son principe est similaire à celui de la gestion d'un événement de graphique typique.
Un gestionnaire, la fonction OnChartEvent(), prend quatre constantes comme paramètres. Apparemment, les développeurs ont utilisé ce mécanisme pour mettre en œuvre l'idée d'identifier un événement et d'obtenir des informations supplémentaires à son sujet. À mon avis, c'est un mécanisme de programme très compact et pratique.
La fonction EventChartCustom() génère un événement de graphique personnalisé. Remarquablement, un événement de graphique personnalisé peut être créé pour un graphique « propre » et pour un graphique « étranger ». Je pense que l'article le plus intéressant sur la signification des graphiques propres et étrangers est La mise en œuvre d'un mode multi-devises dans MetaTrader 5.
À mon avis, il y a une discorde dans le fait que l'identifiant d'événement est de type ushort dans le générateur, alors que dans le gestionnaire, il est de type int. Il serait logique d'utiliser également le type de données ushort dans le gestionnaire.
3. Classe d'événement personnalisé
Comme je l'ai mentionné précédemment, le concept de l'événement appartient au développeur expert. Nous allons maintenant travailler avec les événements du tableau 1. Dans un premier temps, nous allons trier la classe de l'événement personnalisé CEventBase et ses dérivés (Fig.5).
Fig.5 Hiérarchie des classes d'événements
La classe de base se présente comme suit :
//+------------------------------------------------------------------+ //| Class CEventBase. | //| Purpose: base class for a custom event | //| Derives from class CObject. | //+------------------------------------------------------------------+ class CEventBase : public CObject { protected: ENUM_EVENT_TYPE m_type; ushort m_id; SEventData m_data; public: void CEventBase(void) { this.m_id=0; this.m_type=EVENT_TYPE_NULL; }; void ~CEventBase(void){}; //-- bool Generate(const ushort _event_id,const SEventData &_data, const bool _is_custom=true); ushort GetId(void) {return this.m_id;}; private: virtual bool Validate(void) {return true;}; };
Le type d'événement est défini par l'énumération ENUM_EVENT_TYPE :
//+------------------------------------------------------------------+ //| A custom event type enumeration | //+------------------------------------------------------------------+ enum ENUM_EVENT_TYPE { EVENT_TYPE_NULL=0, // no event //--- EVENT_TYPE_INDICATOR=1, // indicator event EVENT_TYPE_ORDER=2, // order event EVENT_TYPE_EXTERNAL=3, // external event };
Les membres de données comprennent l'identifiant d'événement et la structure de données.
La méthode Generate() de la classe de base CEventBase traite de la génération d'un événement. La méthode GetId() renvoie l'identifiant de l'événement et la méthode virtuelle Validate() vérifie la valeur de l'identifiant de l'événement. Au début, j'ai inclus la méthode de gestion des événements dans la classe, mais plus tard, j'ai réalisé que chaque événement est unique et qu'une méthode abstraite n'est pas suffisante ici. J'ai fini par déléguer cette tâche à la classe CEventProcessor qui gère les événements personnalisés.
4. Classe de gestionnaire d'événements personnalisé
La classe CEventProcessor est censée générer et gérer huit événements présentés. Les membres de données de la classe ressemblent à :
//+------------------------------------------------------------------+ //| Class CEventProcessor. | //| Purpose: base class for an event processor EA | //+------------------------------------------------------------------+ class CEventProcessor { //+----------------------------Data members--------------------------+ protected: ulong m_magic; //--- flags bool m_is_init; bool m_is_trade; //--- CEventBase *m_ptr_event; //--- CTrade m_trade; //--- CiMA m_fast_ema; CiMA m_slow_ema; //--- CButton m_button; bool m_button_state; //+------------------------------------------------------------------+ };
Parmi la liste des attributs, il y a des indicateurs d'initialisation et de trade. Le premier ne permettra pas à l'EA de trader s'il ne démarre pas correctement. Le second vérifie l'autorisation de trader.
Il y a aussi le pointeur vers l'objet du type CEventBase, qui fonctionne avec des événements de différents types en utilisant le polymorphisme. Une instance de la classe CTrade permet d'accéder aux opérations de trading.
Les objets de type CiMA facilitent le traitement des données reçues des indicateurs. Pour simplifier l'exemple, j'ai pris deux moyennes mobiles qui vont recevoir un signal de trading. Il existe également une instance de la classe «CButton» qui sera utilisée pour l'activation/la désactivation manuelle de l'EA.
Les méthodes de la classe étaient réparties selon le principe « modules - procédures - fonctions - macros » :
//+------------------------------------------------------------------+ //| Class CEventProcessor. | //| Purpose: base class for an event processor EA | //+------------------------------------------------------------------+ class CEventProcessor { //+-------------------------------Methods----------------------------+ public: //--- constructor/destructor void CEventProcessor(const ulong _magic); void ~CEventProcessor(void); //--- Modules //--- event generating bool Start(void); void Finish(void); void Main(void); //--- event processing void ProcessEvent(const ushort _event_id,const SEventData &_data); private: //--- Procedures void Close(void); void Open(void); //--- Functions ENUM_ORDER_TYPE CheckCloseSignal(const ENUM_ORDER_TYPE _close_sig); ENUM_ORDER_TYPE CheckOpenSignal(const ENUM_ORDER_TYPE _open_sig); bool GetIndicatorData(double &_fast_vals[],double &_slow_vals[]); //--- Macros void ResetEvent(void); bool ButtonStop(void); bool ButtonResume(void); };
Parmi les modules, il y en a trois qui ne génèrent que des événements : celui de départ—Start(), celui de fin—Finish() et le principal—Main(). Le quatrième module ProcessEvent() est à la fois un gestionnaire d'événements et un générateur.
4.1 Module de démarrage
Ce module est conçu pour être appelé dans le gestionnaire OnInit().
//+------------------------------------------------------------------+ //| Start module | //+------------------------------------------------------------------+ bool CEventProcessor::Start(void) { //--- create an indicator event object this.m_ptr_event=new CIndicatorEvent(); if(CheckPointer(this.m_ptr_event)==POINTER_DYNAMIC) { SEventData data; data.lparam=(long)this.m_magic; //--- generate CHARTEVENT_CUSTOM+1 event if(this.m_ptr_event.Generate(1,data)) //--- create a button if(this.m_button.Create(0,"Start_stop_btn",0,25,25,150,50)) if(this.ButtonStop()) { this.m_button_state=false; return true; } } //--- return false; }
Un pointeur vers l'objet d’événement indicateur est créé dans ce module. Ensuite, l'événement « Création d'indicateur » est généré. Un bouton est le dernier à être créé. Il passe en mode « Stop ». Cela signifie que si le bouton était enfoncé, l'expert cesserait de fonctionner.
La structure SEventData est également impliquée dans cette définition de méthode. Il s'agit d'un simple conteneur de paramètres transmis au générateur de l'événement personnalisé. Un seul champ de la structure sera rempli ici, c'est le champ de type long. Il contiendra le nombre magique d'EA.
4.2 Module de finition
Ce module est censé être appelé dans le gestionnaire OnDeinit().
//+------------------------------------------------------------------+ //| Finish module | //+------------------------------------------------------------------+ void CEventProcessor::Finish(void) { //--- reset the event object this.ResetEvent(); //--- create an indicator event object this.m_ptr_event=new CIndicatorEvent(); if(CheckPointer(this.m_ptr_event)==POINTER_DYNAMIC) { SEventData data; data.lparam=(long)this.m_magic; //--- generate CHARTEVENT_CUSTOM+2 event bool is_generated=this.m_ptr_event.Generate(2,data,false); //--- process CHARTEVENT_CUSTOM+2 event if(is_generated) this.ProcessEvent(CHARTEVENT_CUSTOM+2,data); } }
Ici, le pointeur d'événement précédent est effacé et l'événement « Suppression d'indicateur » est généré. Je dois noter que si un événement personnalisé est généré dans le gestionnaire OnDeinit(), vous obtiendrez une erreur d'exécution 4001 (erreur externe inattendue). Par conséquent, la génération et la gestion des événements sont effectuées dans cette méthode sans appeler OnChartEvent().
Encore une fois, le nombre magique d'EA sera stocké à l'aide de la structure SEventData.
4.3 Module principal
Ce module est censé être appelé dans le gestionnaire OnTick().
//+------------------------------------------------------------------+ //| Main module | //+------------------------------------------------------------------+ void CEventProcessor::Main(void) { //--- a new bar object static CisNewBar newBar; //--- if initialized if(this.m_is_init) //--- if not paused if(this.m_is_trade) //--- if a new bar if(newBar.isNewBar()) { //--- close module this.Close(); //--- open module this.Open(); } }
Les procédures Open() et Close() sont appelées dans ce module. La première procédure peut générer l'événement « Réception d'un signal d'ouverture » et la seconde, l'événement « Réception d'un signal de fermeture ». La version actuelle du module est entièrement fonctionnelle avec une nouvelle apparence de barre. Une classe pour détecter un nouvelle barre a été décrite par Konstantin Gruzdev.
4.4 Module de gestion des événements
Ce module est censé être appelé dans le gestionnaire OnChartEvent(). Ce module est le plus grand en termes de taille et de fonctionnalité.
//+------------------------------------------------------------------+ //| Process event module | //+------------------------------------------------------------------+ void CEventProcessor::ProcessEvent(const ushort _event_id,const SEventData &_data) { //--- check event id if(_event_id==CHARTEVENT_OBJECT_CLICK) { //--- button click if(StringCompare(_data.sparam,this.m_button.Name())==0) { //--- button state bool button_curr_state=this.m_button.Pressed(); //--- to stop if(button_curr_state && !this.m_button_state) { if(this.ButtonResume()) { this.m_button_state=true; //--- reset the event object this.ResetEvent(); //--- create an external event object this.m_ptr_event=new CExternalEvent(); //--- if(CheckPointer(this.m_ptr_event)==POINTER_DYNAMIC) { SEventData data; data.lparam=(long)this.m_magic; data.dparam=(double)TimeCurrent(); //--- generate CHARTEVENT_CUSTOM+7 event ushort curr_id=7; if(!this.m_ptr_event.Generate(curr_id,data)) PrintFormat("Failed to generate an event: %d",curr_id); } } } //--- to resume else if(!button_curr_state && this.m_button_state) { if(this.ButtonStop()) { this.m_button_state=false; //--- reset the event object this.ResetEvent(); //--- create an external event object this.m_ptr_event=new CExternalEvent(); //--- if(CheckPointer(this.m_ptr_event)==POINTER_DYNAMIC) { SEventData data; data.lparam=(long)this.m_magic; data.dparam=(double)TimeCurrent(); //--- generate CHARTEVENT_CUSTOM+8 event ushort curr_id=8; if(!this.m_ptr_event.Generate(curr_id,data)) PrintFormat("Failed to generate an event: %d",curr_id); } } } } } //--- user event else if(_event_id>CHARTEVENT_CUSTOM) { long magic=_data.lparam; ushort curr_event_id=this.m_ptr_event.GetId(); //--- check magic if(magic==this.m_magic) //--- check id if(curr_event_id==_event_id) { //--- process the definite user event switch(_event_id) { //--- 1) indicator creation case CHARTEVENT_CUSTOM+1: { //--- create a fast ema if(this.m_fast_ema.Create(_Symbol,_Period,21,0,MODE_EMA,PRICE_CLOSE)) if(this.m_slow_ema.Create(_Symbol,_Period,55,0,MODE_EMA,PRICE_CLOSE)) if(this.m_fast_ema.Handle()!=INVALID_HANDLE) if(this.m_slow_ema.Handle()!=INVALID_HANDLE) { this.m_trade.SetExpertMagicNumber(this.m_magic); this.m_trade.SetDeviationInPoints(InpSlippage); //--- this.m_is_init=true; } //--- break; } //--- 2) indicator deletion case CHARTEVENT_CUSTOM+2: { //---release indicators bool is_slow_released=IndicatorRelease(this.m_fast_ema.Handle()); bool is_fast_released=IndicatorRelease(this.m_slow_ema.Handle()); if(!(is_slow_released && is_fast_released)) { //--- to log? if(InpIsLogging) Print("Failed to release the indicators!"); } //--- reset the event object this.ResetEvent(); //--- break; } //--- 3) check open signal case CHARTEVENT_CUSTOM+3: { MqlTick last_tick; if(SymbolInfoTick(_Symbol,last_tick)) { //--- signal type ENUM_ORDER_TYPE open_ord_type=(ENUM_ORDER_TYPE)_data.dparam; //--- double open_pr,sl_pr,tp_pr,coeff; open_pr=sl_pr=tp_pr=coeff=0.; //--- if(open_ord_type==ORDER_TYPE_BUY) { open_pr=last_tick.ask; coeff=1.; } else if(open_ord_type==ORDER_TYPE_SELL) { open_pr=last_tick.bid; coeff=-1.; } sl_pr=open_pr-coeff*InpStopLoss*_Point; tp_pr=open_pr+coeff*InpStopLoss*_Point; //--- to normalize prices open_pr=NormalizeDouble(open_pr,_Digits); sl_pr=NormalizeDouble(sl_pr,_Digits); tp_pr=NormalizeDouble(tp_pr,_Digits); //--- open the position if(!this.m_trade.PositionOpen(_Symbol,open_ord_type,InpTradeLot,open_pr, sl_pr,tp_pr)) { //--- to log? if(InpIsLogging) Print("Failed to open the position: "+_Symbol); } else { //--- pause Sleep(InpTradePause); //--- reset the event object this.ResetEvent(); //--- create an order event object this.m_ptr_event=new COrderEvent(); if(CheckPointer(this.m_ptr_event)==POINTER_DYNAMIC) { SEventData data; data.lparam=(long)this.m_magic; data.dparam=(double)this.m_trade.ResultDeal(); //--- generate CHARTEVENT_CUSTOM+5 event ushort curr_id=5; if(!this.m_ptr_event.Generate(curr_id,data)) PrintFormat("Failed to generate an event: %d",curr_id); } } } //--- break; } //--- 4) check close signal case CHARTEVENT_CUSTOM+4: { if(!this.m_trade.PositionClose(_Symbol)) { //--- to log? if(InpIsLogging) Print("Failed to close the position: "+_Symbol); } else { //--- pause Sleep(InpTradePause); //--- reset the event object this.ResetEvent(); //--- create an order event object this.m_ptr_event=new COrderEvent(); if(CheckPointer(this.m_ptr_event)==POINTER_DYNAMIC) { SEventData data; data.lparam=(long)this.m_magic; data.dparam=(double)this.m_trade.ResultDeal(); //--- generate CHARTEVENT_CUSTOM+6 event ushort curr_id=6; if(!this.m_ptr_event.Generate(curr_id,data)) PrintFormat("Failed to generate an event: %d",curr_id); } } //--- break; } //--- 5) position opening case CHARTEVENT_CUSTOM+5: { ulong ticket=(ulong)_data.dparam; ulong deal=(ulong)_data.dparam; //--- datetime now=TimeCurrent(); //--- check the deals & orders history if(HistorySelect(now-PeriodSeconds(PERIOD_H1),now)) if(HistoryDealSelect(deal)) { double deal_vol=HistoryDealGetDouble(deal,DEAL_VOLUME); ENUM_DEAL_ENTRY deal_entry=(ENUM_DEAL_ENTRY)HistoryDealGetInteger(deal,DEAL_ENTRY); //--- if(deal_entry==DEAL_ENTRY_IN) { //--- to log? if(InpIsLogging) { Print("\nNew position for: "+_Symbol); PrintFormat("Volume: %0.2f",deal_vol); } } } //--- break; } //--- 6) position closing case CHARTEVENT_CUSTOM+6: { ulong ticket=(ulong)_data.dparam; ulong deal=(ulong)_data.dparam; //--- datetime now=TimeCurrent(); //--- check the deals & orders history if(HistorySelect(now-PeriodSeconds(PERIOD_H1),now)) if(HistoryDealSelect(deal)) { double deal_vol=HistoryDealGetDouble(deal,DEAL_VOLUME); ENUM_DEAL_ENTRY deal_entry=(ENUM_DEAL_ENTRY)HistoryDealGetInteger(deal,DEAL_ENTRY); //--- if(deal_entry==DEAL_ENTRY_OUT) { //--- to log? if(InpIsLogging) { Print("\nClosed position for: "+_Symbol); PrintFormat("Volume: %0.2f",deal_vol); } } } //--- break; } //--- 7) stop trading case CHARTEVENT_CUSTOM+7: { datetime stop_time=(datetime)_data.dparam; //--- this.m_is_trade=false; //--- to log? if(InpIsLogging) PrintFormat("Expert trading is stopped at: %s", TimeToString(stop_time,TIME_DATE|TIME_MINUTES|TIME_SECONDS)); //--- break; } //--- 8) resume trading case CHARTEVENT_CUSTOM+8: { datetime resume_time=(datetime)_data.dparam; this.m_is_trade=true; //--- to log? if(InpIsLogging) PrintFormat("Expert trading is resumed at: %s", TimeToString(resume_time,TIME_DATE|TIME_MINUTES|TIME_SECONDS)); //--- break; } } } } }
Il se compose de deux parties. La première est la gestion des événements liés à un clic sur l'objet « Bouton ». Ce clic générera un événement personnalisé externe, qui sera traité par le gestionnaire ultérieurement.
La deuxième partie est conçue pour le traitement des événements personnalisés générés. Il contient deux blocs, où après qu'un événement pertinent a été traité, un nouveau est généré. L'événement « Réception d'un signal d'ouverture » est traité dans le premier bloc. Son traitement réussi génère un nouvel événement d'ordre « Ouverture d'une position ». L'événement « Réception d'un signal de fermeture » est traité dans le deuxième bloc. Si le signal est traité, alors l'événement « Fermeture d'une position » a lieu.
L'expert CustomEventProcessor.mq5 est un bon exemple d'utilisation de la classe CEventProcessor. L'EA a été conçu pour créer des événements et y répondre de manière appropriée. Avec le paradigme OPP, nous avons pu réduire le code source à un nombre inférieur de lignes. Le code source d'EA se trouve en pièce jointe à cet article.
À mon avis, il n'est pas nécessaire de se référer à chaque fois au mécanisme d'un événement personnalisé. Il y a beaucoup de choses mineures, insignifiantes et sans événement en termes de stratégie qui peuvent avoir une forme différente.
Conclusion
Dans cet article, j'ai essayé d'illustrer les principes de travail avec des événements personnalisés dans l'environnement MQL5. J'espère que les idées couvertes dans cet article intéresseront les programmeurs ayant une expérience différente, pas seulement les novices.
Je suis content que le langage MQL5 se développe. Probablement, dans un avenir proche, il y aura des modèles de classe et peut-être des pointeurs vers des fonctions. Nous pourrons alors écrire un délégué à part entière pointant sur une méthode d'un objet arbitraire.
Les fichiers source de l'archive peuvent être placés dans un dossier de projet. Dans mon cas, il s'agit du dossier MQL5\Projects\ChartUserEvent.
Traduit du russe par MetaQuotes Ltd.
Article original : https://www.mql5.com/ru/articles/1163
- Applications de trading gratuites
- Plus de 8 000 signaux à copier
- Actualités économiques pour explorer les marchés financiers
Vous acceptez la politique du site Web et les conditions d'utilisation