MetaTrader 5 : créez un marché adapté à votre stratégie — barres Renko, Range et Volume Egal, instruments synthétiques et tests de résistance sur symboles personnalisés
Introduction
Le graphique standard de MetaTrader 5 est un outil fiable, mais il est étroitement lié aux unités de temps (« timeframes »), au flux de cotations du courtier et à la grille calendaire. Dans le trading algorithmique moderne, cela s'avère souvent insuffisant : le bruit de marché dû à la volatilité, les trous dans l'historique et les intervalles de temps fixes faussent la dynamique réelle. Et si le terminal vous permettait non seulement de lire le marché, mais aussi de concevoir sa représentation pour répondre aux besoins d'une stratégie spécifique ?
Avec le développement de l'API des symboles personnalisés, MetaTrader 5 a modifié son architecture : le trader n'est plus seulement un consommateur passif de cotations, mais devient un architecte de ses propres données. Un symbole personnalisé n'est pas un graphique hors ligne destiné à la visualisation, mais un objet terminal à part entière avec son propre historique de ticks, sa spécification de contrat et une prise en charge native dans le Testeur de Stratégie.
Vous pouvez désormais générer des graphiques indépendants du temps (barres Renko, Range Bars, barres à Volume Egal), assembler des instruments et des paniers synthétiques et effectuer des tests de résistance en modifiant arbitrairement le spread, les niveaux de stop et les exigences de marge directement dans le code.
Dans cet article, nous explorerons les aspects pratiques de l'utilisation d'instruments personnalisés : structure de stockage des données, API MQL5 de base, algorithmes d'agrégation des ticks et modification de l'historique. Voyons comment adapter les EA standard pour acheminer les ordres de trading d'un symbole personnalisé vers un instrument de trading réel.
Fig. 1 : construisez votre marché à partir d’éléments standards
Qu'est-ce qu'un symbole personnalisé : architecture, stockage et API
Dans MetaTrader 5, un symbole personnalisé n'est pas un graphique hors ligne destiné à l'observation visuelle, contrairement aux versions précédentes de la plateforme. Il s'agit désormais d'un objet terminal à part entière, doté de son propre historique de ticks, de sa propre spécification de contrat et d'une prise en charge native dans le Testeur de Stratégie.
Contrairement aux instruments standard fournis par un courtier et dépendants de la qualité du flux de cotations de son fournisseur, un symbole personnalisé permet au trader de devenir le véritable architecte de ses propres données. Vous déterminez quelles cotations, sous quelle forme et avec quel niveau de précision, seront incluses dans l'historique. Cela ouvre la voie à l'analyse non seulement de marchés que le courtier ne propose pas (cryptomonnaies, spreads interbancaires), mais aussi à la transformation de données existantes (Renko, volumes égaux, indices synthétiques).
Les symboles personnalisés sont isolés des serveurs de trading du courtier. Les fichiers sont stockés localement, par exemple : « AppData\Roaming\MetaQuotes\Terminal\[InstanceID]\bases\Custom ».
Cette solution présente un avantage important : l’indépendance. Vous pouvez changer de courtier, mettre à jour votre terminal, mais vos symboles personnalisés et votre historique accumulé resteront intacts. Dans la fenêtre du Market Watch, ils sont toujours situés dans un dossier séparé appelé « Custom » afin de séparer visuellement les instruments réels des vôtres.
Noyau de l'API MQL5
La gestion des symboles personnalisés dans le code nécessite l'accès à des fonctions API spécialisées. La liste complète se trouve dans la documentation , mais pour les principaux cas d'utilisation, nous nous concentrerons sur trois groupes de fonctions.
Gestion du cycle de vie des symboles :
- CustomSymbolCreate() — crée un symbole personnalisé avec un nom spécifié dans un groupe spécifié
- CustomSymbolDelete() — supprime un symbole personnalisé portant un nom spécifié
- SymbolSelect() — ajoute le symbole personnalisé au Market Watch (sans cela, le symbole ne sera pas visible dans l'interface graphique)
Gestion du téléchargement des données :
- CustomTicksAdd() — ajoute les données du tableau de type MqlTick[] vers l'historique des prix d'un symbole personnalisé. Un symbole personnalisé doit être sélectionné dans la fenêtre du Market Watch.
- CustomTicksReplace() — remplace complètement l'historique des prix d'un symbole personnalisé dans un intervalle de temps spécifié par les données du tableau de type MqlTick[]
- CustomRatesUpdate() — ajoute les barres manquantes à l'historique du symbole personnalisé et/ou remplace les barres existantes par des données provenant du tableau de type MqlRates[]
Gestion des propriétés :
- CustomSymbolSetInteger() / CustomSymbolSetDouble() / CustomSymbolSetString() — définissent les propriétés du contrat (spread, précision, devise de marge, etc.)
L'appel direct aux fonctions de l'API nécessite la gestion de diverses erreurs et la vérification des statuts. Pour accélérer le développement, nous utilisons la classe wrapper CiCustomSymbol. Elle encapsule la logique de création, de clonage des propriétés des symboles réels et de chargement par lots des ticks. Le code complet de la classe CiCustomSymbol se trouve dans le fichier CiCustomSymbol.mqh ci-joint.
Voici un exemple d'initialisation d'un symbole personnalisé : nous créons un symbole, clonons les propriétés du symbole EURUSD et vérifions les codes de retour :
#include <CiCustomSymbol.mqh> //+------------------------------------------------------------------+ //| inputs | //+------------------------------------------------------------------+ input string CustomSymbolName = "EURUSD_Custom"; // new symbol name input string OriginSymbol = "EURUSD"; // source symbol //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { CiCustomSymbol symb; //--- attempt to create a symbol //--- return codes: -1 (error), 0 (already exists), 1 (successfully created) int res = symb.Create(CustomSymbolName, "", OriginSymbol, 1000000, true); if(res == -1) { Print("Error creating symbol: ", GetLastError()); return; } if(res == 0) { Print("Symbol ", CustomSymbolName, " already exists."); } else { Print("Symbol ", CustomSymbolName, " successfully created."); //--- set specific properties (for example, fixed spread of 20 points) //--- properties are changed BEFORE loading history if(!symb.SetProperty(SYMBOL_SPREAD, 20)) Print("Failed to set spread. Error: ", GetLastError()); if(!symb.SetProperty(SYMBOL_SPREAD_FLOAT, false)) Print("Failed to set spread type (fixed). Error: ", GetLastError()); } //--- symbol added to Market Watch ('true' parameter to Create) Print("Symbol created: ", CustomSymbolName); }
Vous trouverez le code source complet du script dans le fichier CreateCustomSymbol.mq5 ci-joint.
Tenez compte de plusieurs limitations décrites dans la documentation lors de la manipulation de symboles personnalisés :
Désactivation du réseau MQL5 Cloud Network :
L'optimisation des stratégies sur des symboles personnalisés via le réseau cloud est interdite. La raison est simple : les ordinateurs de différents traders peuvent contenir des symboles portant le même nom (par exemple, EURUSD_Custom), mais avec des historiques complètement différents. L'utilisation d'un réseau cloud entraînerait un chaos de synchronisation et la génération d'un trafic excessif. Les tests ne peuvent être effectués que localement ou par l'intermédiaire d'agents locaux.
Logique de calcul des marges :
Le Testeur de Stratégie utilise les taux de change croisés pour calculer la marge et le profit. Si vous avez créé et testez le symbole personnalisé AUDCAD (basé sur AUDUSD/USDCAD) et que votre compte est en USD, le Testeur doit :
- trouver le taux de change AUDUSD (la valeur de l'AUD en USD) - pour calculer la marge
- trouver le taux de change USDCAD (comment convertir les CAD en USD) - pour calculer le profit
Il est important de s'assurer que toutes les paires de devises requises figurent dans votre Market Watch et que l'historique est chargé, sinon le Testeur ne pourra pas calculer les indicateurs financiers.
Les propriétés du symbole ne changent pas pendant le test :
Les propriétés personnalisées des symboles (par exemple, SYMBOL_TRADE_STOPS_LEVEL est une distance minimale pour les stops) sont définies avant de démarrer le test. Il n'est pas possible de les modifier dynamiquement pendant l'exécution de l'EA (par exemple, pour augmenter la distance minimale des stops avant une annonce macroéconomique) dans la version actuelle du Testeur de Stratégie. Le Testeur fixe les paramètres des symboles lors de l'initialisation.
Digression analytique : à quoi sert réellement le backtesting
Pourquoi une architecture aussi complexe est-elle nécessaire ? L'utilisation de symboles personnalisés modifie l'approche de test des idées de trading. Au lieu d'adapter votre stratégie aux données potentiellement erronées d'un courtier spécifique (présentant des trous dans l'historique ou des écarts anormaux), vous construisez votre propre environnement de laboratoire idéal.
Vous pouvez vérifier comment la stratégie se comporte avec un spread de 50 pips (pour écarter les stratégies de scalping), ou comment elle se comporte sur des graphiques où le temps n'a pas d'importance (Range/Renko). Cela permet d'isoler la logique fondamentale d'une stratégie des artefacts dus au bruit du marché.
Graphiques indépendants du temps : Renko, Range Bars et Volume Egal
Les timeframes standard sont pratiques, mais ils imposent un calendrier artificiel au marché. Par exemple, la bougie M1 se ferme toutes les 60 secondes, quel que soit le nombre de ticks. Dans des conditions de faible liquidité, une telle barre transmet peu d'informations, et en période de forte volatilité, elle masque des mouvements importants dans un simple rectangle.
Les graphiques indépendants du temps résolvent ce problème en formant une nouvelle barre non pas après un certain temps, mais lorsque le prix ou le volume atteint un seuil spécifié. Cela permet :
- de filtrer les bruits du marché et de se concentrer sur les mouvements significatifs
- d’adapter automatiquement l'étape d'analyse à la volatilité actuelle
- d’identifier les zones d'accumulation/distribution en fonction du volume réel (ou du volume par tick), et non en fonction d'une minuterie
Examinons trois types populaires de ces graphiques — Renko, Range Bars et Volume Egal — et montrons comment les générer dans MetaTrader 5 à l'aide de symboles personnalisés. Pour commencer, comparons et décrivons ces trois types de barres dans un tableau :
| Type de graphique | Critères de formation d’une nouvelle barre | Meilleurs cas d'utilisation |
|---|---|---|
| Renko | Variation de prix d'un nombre donné de points | Stratégies de tendance, filtrage des mouvements latéraux |
| Range Bars | L’intervalle entre le plus haut et le plus bas de la barre atteint la valeur spécifiée | Scalping, trading intraday en période de forte volatilité |
| Volume Egal | Accumulation d'un nombre spécifié de ticks ou d'un volume réel | Analyse de la liquidité, recherche des zones d'intérêt des acteurs majeurs |
Note :
Tous les symboles personnalisés de MetaTrader 5 sont liés à la période M1. Il s'agit d'une limitation architecturale de la plateforme : même si une barre se forme en 2 secondes, elle recevra tout de même un horodatage dans l'historique avec une précision allant jusqu'à une minute. En cas de forte volatilité, plusieurs barres peuvent être générées dans la même minute (avoir la même heure) - l'EA doit en tenir compte.
Mise en œuvre technique : agrégation des ticks en barres
De manière générale, l'algorithme de génération des barres non basées sur le temps est le même pour les trois types :
- lecture des ticks à partir de l'historique ou réception en ligne via les fonctions CopyTicksRange() / OnTick(), respectivement
- accumulation des données dans la mémoire tampon : suivi des cours d’ouverture, de haut, de bas, de clôture et du volume
- vérification des conditions de formation d'une nouvelle barre (points/plage/volume)
- enregistrement d'une barre dans la base de données de symboles personnalisés via CustomRatesUpdate()
- émulation de ticks via CustomTicksAdd() pour activer les indicateurs et les EA sur les graphiques MetaTrader 5
Classe de base pour la gestion des symboles personnalisés : algorithme pour tous les types de barres
//+------------------------------------------------------------------+ //| class-aggregator of ticks into custom bars | //+------------------------------------------------------------------+ class CBarAggregator { private: string symbol_name; double threshold; int bar_type; ENUM_VOLUME_MODE volume_mode; MqlRates rates_buffer[]; int buffer_limit; int buffer_idx; MqlRates current_bar; double last_close; int trend; bool is_initialized; datetime last_bar_time; int symbol_digits; public: CBarAggregator(void); bool Init(const string _symbol_name, double _threshold, int _bar_type, ENUM_VOLUME_MODE _vol_mode = VOLUME_MODE_TICK); bool ProcessTick(const MqlTick &tick); void FlushBuffer(void); void Reset(void); private: void CreateBar(const MqlTick &tick, double open_price = 0.0); void CloseAndSaveBar(const MqlTick &tick, double forced_close = 0.0); void WriteBatch(void); }; //+------------------------------------------------------------------+ //| handle an incoming tick | //+------------------------------------------------------------------+ bool CBarAggregator::ProcessTick(const MqlTick &tick) { if(tick.bid <= 0 || tick.ask <= 0) return false; if(tick.time <= 0) return false; // protection from invalid ticks if(!is_initialized) { CreateBar(tick); last_close = tick.bid; last_bar_time = tick.time; // initialize with the first tick time is_initialized = true; return false; } bool bar_closed = false; if(bar_type == 0) // renko { double diff = tick.bid - last_close; double size = threshold * _Point; int req_move = (trend != 0) ? 2 : 1; if(diff >= size * req_move) { int bricks = (int)(diff / size); for(int i = 0; i < bricks; i++) { double target_close = NormalizeDouble(last_close + size, symbol_digits); CloseAndSaveBar(tick, target_close); last_close = target_close; CreateBar(tick, last_close); trend = 1; } bar_closed = true; } else if(diff <= -size * req_move) { int bricks = (int)(MathAbs(diff) / size); for(int i = 0; i < bricks; i++) { double target_close = NormalizeDouble(last_close - size, symbol_digits); CloseAndSaveBar(tick, target_close); last_close = target_close; CreateBar(tick, last_close); trend = -1; } bar_closed = true; } } else if(bar_type == 1) // range bars { double bid = NormalizeDouble(tick.bid, symbol_digits); if(bid > current_bar.high) current_bar.high = bid; if(bid < current_bar.low) current_bar.low = bid; if((current_bar.high - current_bar.low) >= threshold * _Point) { CloseAndSaveBar(tick); CreateBar(tick); bar_closed = true; } } else if(bar_type == 2) // equal volume bars { if(volume_mode == VOLUME_MODE_REAL) current_bar.real_volume += (long)tick.volume_real; else current_bar.tick_volume++; long current_vol = (volume_mode == VOLUME_MODE_REAL) ? current_bar.real_volume : current_bar.tick_volume; if(current_vol >= (long)threshold) { CloseAndSaveBar(tick); CreateBar(tick); bar_closed = true; } } return bar_closed; } //+------------------------------------------------------------------+ //| close the bar and save to the buffer | //+------------------------------------------------------------------+ void CBarAggregator::CloseAndSaveBar(const MqlTick &tick, double forced_close) { current_bar.close = (forced_close != 0.0) ? NormalizeDouble(forced_close, symbol_digits) : NormalizeDouble(tick.bid, symbol_digits); current_bar.high = NormalizeDouble(MathMax(current_bar.high, MathMax(current_bar.open, current_bar.close)), symbol_digits); current_bar.low = NormalizeDouble(MathMin(current_bar.low, MathMin(current_bar.open, current_bar.close)), symbol_digits); current_bar.spread = MathMax(0, current_bar.spread); // make sure last_bar_time > 0 (initialized) if(last_bar_time > 0 && current_bar.time <= last_bar_time) { // shift by 1 second instead of 60 to minimize distortion current_bar.time = last_bar_time + 1; } last_bar_time = current_bar.time; // final validation before the buffer if(current_bar.high < current_bar.low || current_bar.high < current_bar.open || current_bar.high < current_bar.close || current_bar.low > current_bar.open || current_bar.low > current_bar.close) { Print("🟡 Invalid bar skipped: O=", current_bar.open, " H=", current_bar.high, " L=", current_bar.low, " C=", current_bar.close, " Time=", TimeToString(current_bar.time)); return; } if(buffer_idx < buffer_limit) { rates_buffer[buffer_idx] = current_bar; buffer_idx++; } if(buffer_idx >= buffer_limit) WriteBatch(); } //+------------------------------------------------------------------+ //| save the buffer to the symbol history | //+------------------------------------------------------------------+ void CBarAggregator::WriteBatch(void) { if(buffer_idx > 0) { datetime t_from = rates_buffer[0].time; datetime t_to = rates_buffer[buffer_idx - 1].time; if(CustomRatesUpdate(symbol_name, rates_buffer) < 1) Print("CustomRatesUpdate error: ", GetLastError()); buffer_idx = 0; } } //+------------------------------------------------------------------+
Le code complet de la classe CBarAggregator se trouve dans le fichier CBarAggregator.mqh.
Indicateur de barres universel non basé sur le temps
Nous allons créer un indicateur universel pour une application pratique de la classe CBarAggregator. Il vous permet de basculer entre les modes Renko, Range et Volume Egal dans les entrées et génère un symbole personnalisé dans un intervalle d'historique donné.
Entrées :
input ENUM_CUSTOM_CHART_MODE InputMode = CMT_RENKO; // generation mode input double InputBoxSize = 10; // bar size (points) for renko/range input long InputVolLimit = 1000; // volume limit for equal-volume input string InputSuffix = ""; // symbol name suffix input datetime InputStartTime = 0; // history start (0 - last 7 days)
Énumération des modes de génération de barres :
enum ENUM_CUSTOM_CHART_MODE { CMT_RENKO = 0, // renko CMT_RANGE = 1, // range bars CMT_EQVOL_TICK = 2, // equal tick volumes CMT_EQVOL_REAL = 3 // equal real volumes };
Fonctionnement en temps réel — après le chargement de l'historique, l'indicateur continue de fonctionner en ligne, traitant les nouvelles données :
//+------------------------------------------------------------------+ //| handle a new tick | //+------------------------------------------------------------------+ void ProcessNewTicks() { if(last_tick_time_msc==0) return; MqlTick ticks[]; int copied=CopyTicksRange(_Symbol,ticks,COPY_TICKS_ALL,last_tick_time_msc,0); if(copied>0) { for(int i=0;i<copied;i++) { aggregator.ProcessTick(ticks[i]); last_tick_time_msc=ticks[i].time_msc; } aggregator.FlushBuffer(); } }
Cet indicateur ne génère pas de graphiques automatiquement. Son but est de créer et de remplir un symbole personnalisé avec des données. Pour commencer, faites glisser l'indicateur sur n'importe quel graphique (par exemple, EURUSD) – une fenêtre de propriétés s'ouvrira. Sélectionnez un mode (par exemple MODE_RENKO), une taille de boîte (InputBoxSize) et un point de départ de l'historique.
Séquence d'utilisation de l’indicateur :
- l'indicateur commencera à générer des barres (des messages concernant le début et la fin de la génération apparaîtront dans le journal)
- une fois que le message concernant la fin de la génération apparaît, ouvrez le Market Watch (Ctrl+M)
- recherchez et sélectionnez l'instrument créé dans la liste des symboles (par exemple, EURUSD_Renko_10)
- ouvrez un nouveau graphique (Ctrl+N)
- définissez l'unité de temps sur M1
Les résultats du fonctionnement de l'indicateur dans différents modes sont présentés dans les figures 2 à 4.

Fig. 2 : Barres Renko

Fig. 3 : Range Bars

Fig. 4 : barres de Volume Egal
Examinons brièvement les caractéristiques et des exemples d'utilisation des barres non basées sur le temps.
Renko — une nouvelle barre n'est affichée que lorsque le prix dépasse la taille de barre spécifiée en points par rapport au prix de clôture de la barre précédente. La direction de la barre (haussière/baissière) est déterminée par la direction de la cassure. Les ombres ne sont pas affichées par défaut, mais peuvent être utilisées si nous devons analyser la volatilité intra-barre. Avec un mouvement vigoureux, plusieurs barres peuvent se former en une minute. Le prix de clôture de la barre correspond au prix de cassure, et non au prix de clôture à la minute.
Exemple de stratégie basée sur Renko :
- entrée : intersection de la MA rapide (9) et de la MA lente (21) à la fin de la barre
- filtre : si la valeur de l’ATR(14) est supérieure au seuil spécifié, filtrer les fausses cassures dans le mouvement latéral
- sortie : franchissement inverse des moyennes mobiles ou stop fixe en points
- la particularité est que les transactions sont effectuées au prix PRICE_CLOSE, puisque la barre zéro du Renko (la dernière) est toujours incomplète
Range bars — une nouvelle barre s'ouvre lorsque l'écart entre le plus haut et le plus bas atteint la valeur spécifiée en points. Contrairement à Renko, elle prend en compte tous les mouvements à l'intérieur de la barre, et pas seulement la clôture. Au lieu d'une valeur de taille fixe, nous pouvons utiliser une taille de barre dynamique (Range) basée sur l'ATR.
Que signifie la valeur de « Range » ? La taille de la plage, ou range, définit le seuil de variation de prix à partir de laquelle une nouvelle barre commence à se former. « 1 Range » correspond à une variation de prix minimale. Cette valeur peut être représentée par l'équation suivante : 1 Range = Taille du tick.
Avantages pour une utilisation en scalping :
- en période de faible volatilité, les barres se forment lentement – moins de faux signaux
- lorsque la volatilité s'accentue, les barres deviennent plus fréquentes, ce qui nous permet de capter les mouvements rapides
- niveaux de support/résistance clairs aux limites de la barre
Barres de Volume Egal — une nouvelle barre est créée après l'accumulation d'un nombre spécifié de ticks (pour le Forex) ou de volume réel (pour les instruments de bourse). Des barres de Volume Egal nous permettent de comparer les mouvements sur une base égale :
- les zones où se forme rapidement une barre indiquent une forte activité, et par conséquent une cassure de la plage actuelle est possible
- les zones vides du graphique indiquent un manque d'intérêt, et par conséquent un retournement est possible
Remarques :
Le Testeur de Stratégie génère des ticks pour les symboles personnalisés selon des règles standard : en fonction de la configuration de la barre (OHLC). Cependant, pour les barres indépendantes du temps (en particulier les renko), le cours de clôture de la barre est par définition un élément prédictif du mouvement suivant. Le Testeur peut créer l'illusion d'une entrée idéale en observant la configuration de la barre zéro (inachevée).
Solution :
- prenez toujours les signaux à partir de la première barre (terminée), et non à partir de zéro
- configurez le Testeur pour qu'il fonctionne sur des barres fermées
- utilisez le routage d'ordres de symboles réels (voir ci-dessous) — cela éliminera les artefacts de génération de ticks dans le Testeur
Instruments synthétiques : spreads, paniers et liens entre marchés
Les marchés évoluent rarement de manière isolée — les instruments sont souvent liés. L'EURUSD a tendance à entraîner le GBPUSD dans son sillage, le pétrole influence le sentiment du marché canadien (CAD), australien (AUD) et norvégien (NOK), et le S&P 500 et le DAX évoluent souvent de concert. Et si, au lieu d'observer passivement les corrélations, nous créions un outil unique qui combine mathématiquement ces instruments ? Les symboles personnalisés de MetaTrader 5 vous permettent d'aller au-delà des outils standard des courtiers et de construire votre propre système d’analyse : spreads, paniers, paires d'arbitrage et indices d'anti-corrélation.
Mathématiques des synthétiques :
La base de tout instrument synthétique est une combinaison linéaire :
Synth = k₁·Asset_A ± k₂·Asset_B ± ... ± kₙ·Asset_N.
En pratique, on utilise le plus souvent une équation simplifiée pour calculer l'écart (le spread) entre deux actifs :
Spread = Prix_A - Ratio · Prix_B,
où Ratio est le coefficient de normalisation qui égalise la volatilité ou l'échelle de prix des actifs. Sans normalisation, l'écart sera biaisé en faveur de l'instrument au prix le plus élevé, ce qui faussera les signaux des oscillateurs et les calculs des exigences de marge.
La principale tâche technique consiste à synchroniser les ticks. La réception asynchrone de cotations provenant de différents pools de liquidités entraîne des spreads irréguliers et de fausses cassures. Dans MetaTrader 5, nous résolvons ce problème grâce à une fenêtre de temps tampon et à la fixation du dernier prix connu pour chaque jambe synthétique.
Générateur de ticks synthétiques :
//+------------------------------------------------------------------+ //| synthetic tick generator based on two assets | //+------------------------------------------------------------------+ class CSyntheticTickGenerator { private: string symbol_a; string symbol_b; string synth_name; double ratio; double last_price_a; double last_price_b; int symbol_digits; double point; public: //+------------------------------------------------------------------+ //| initializer | //+------------------------------------------------------------------+ bool Init(const string _sym_a, const string _sym_b, const string _synth, double _ratio) { symbol_a=_sym_a; symbol_b=_sym_b; synth_name=_synth; ratio=_ratio; symbol_digits=(int)SymbolInfoInteger(_sym_a, SYMBOL_DIGITS); point=SymbolInfoDouble(_sym_a, SYMBOL_POINT); last_price_a=0.0; last_price_b=0.0; return true; } //+------------------------------------------------------------------+ //| handle an incoming tick | //+------------------------------------------------------------------+ void ProcessTick(const MqlTick &tick, const string source_symbol) { if(tick.bid<=0 || tick.ask<=0) return; //--- update the last known price for the corresponding leg if(source_symbol==symbol_a) { last_price_a=tick.bid; } else if(source_symbol==symbol_b) { last_price_b=tick.bid; } //--- waiting for prices to appear for both assets if(last_price_a<=0 || last_price_b<=0) return; //--- calculate synthetic price with normalization double synth_bid=NormalizeDouble(last_price_a-ratio*last_price_b, symbol_digits); double base_spread=tick.ask-tick.bid; double synth_ask=NormalizeDouble(synth_bid+base_spread, symbol_digits); //--- tick structure formation MqlTick synth_tick={0}; synth_tick.time=tick.time; synth_tick.time_msc=tick.time_msc; synth_tick.bid=synth_bid; synth_tick.ask=synth_ask; synth_tick.flags=TICK_FLAG_BID | TICK_FLAG_ASK; //--- writing a custom symbol to the database MqlTick batch[1]; batch[0]=synth_tick; CustomTicksAdd(synth_name, batch); } }; //+------------------------------------------------------------------+
Exemple de stratégie — les spreads synthétiques sont idéaux pour les stratégies d’arbitrage statistique et de retour à la moyenne. L'approche traditionnelle consiste à suivre la déviation du spread par rapport à la moyenne mobile en unités d'écart type (z-score).
Logique des signaux :
- entrée — si |z-score| > 2,0, la déviation du spread est statistiquement anormale
- sortie — |z-score| < 0,5, un retour à la moyenne s'est produit, la position est fermée
- gestion des risques – le stop loss est lié à la volatilité du panier d’actifs (par exemple, 2,5 × ATR(20))
Les fonctions de calcul du z-score et de génération des signaux sont présentées ci-dessous :
//+------------------------------------------------------------------+ //| z-score calculation for synthetic spread | //+------------------------------------------------------------------+ double CalculateSpreadZScore(const string synth_symbol, int ma_period, int std_dev_period) { static int ma_handle=INVALID_HANDLE; static int std_handle=INVALID_HANDLE; //--- initialize handles on first call if(ma_handle == INVALID_HANDLE) { ma_handle=iMA(synth_symbol, PERIOD_M1, ma_period, 0, MODE_SMA, PRICE_CLOSE); if(ma_handle==INVALID_HANDLE) { Print("Error creating iMA handle: ", GetLastError()); return 0.0; } } if(std_handle == INVALID_HANDLE) { std_handle = iStdDev(synth_symbol, PERIOD_M1, std_dev_period, 0, MODE_SMA, PRICE_CLOSE); if(std_handle==INVALID_HANDLE) { Print("Error creating iStdDev handle: ", GetLastError()); return 0.0; } } double ma_buf[], std_buf[], close_buf[]; ArraySetAsSeries(ma_buf, true); ArraySetAsSeries(std_buf, true); ArraySetAsSeries(close_buf, true); //--- copy data from the 1st completed bar (index 1, quantity 1) if(CopyBuffer(ma_handle, 0, 1, 1, ma_buf) != 1) return 0.0; if(CopyBuffer(std_handle, 0, 1, 1, std_buf) != 1) return 0.0; if(CopyClose(synth_symbol, PERIOD_M1, 1, 1, close_buf) != 1) return 0.0; double spread_ma = ma_buf[0]; double spread_std = std_buf[0]; double current_spread = close_buf[0]; if(spread_std == 0.0) return 0.0; return (current_spread - spread_ma) / spread_std; } //+------------------------------------------------------------------+ //| trading signal generator | //+------------------------------------------------------------------+ int CheckSpreadSignal(const string synth_symbol) { double z = CalculateSpreadZScore(synth_symbol, 20, 20); if(z>2.0) return -1; // spread overbought -> spread short if(z<-2.0) return 1; // spread oversold -> spread long if(MathAbs(z)<0.5) return 0; // close signal return 0; }
La direction de la couverture dépend du signe du z-score : si l’écart est positif, nous vendons la jambe sur-achetée a et achetons la jambe sur-vendue b ; s’il est négatif, c’est l’inverse qui est vrai. Les exigences de marge sont calculées automatiquement par le Testeur si les taux de change croisés correspondants sont disponibles dans Market Watch.
Indices de panier et rééquilibrage dynamique
Si un spread est un instrument à deux jambes, alors un panier est un portefeuille de N symboles. Les facteurs de pondération peuvent être :
- statique - pondéré de manière égale (k = 1/N) ou fixé par la capitalisation/liquidité du symbole
- dynamique - recalculé en fonction de la volatilité inverse ou du volume en mouvement
Le rééquilibrage dynamique nécessite un recalcul périodique des coefficients k et un ajustement des propriétés du symbole via CustomSymbolSetDouble(). Dans la version actuelle du Testeur de Stratégie, les propriétés du contrat sont fixées au début de l'optimisation ; les modifications dynamiques de pondération doivent donc être simulées en modifiant l'historique des ticks ou en utilisant des indicateurs personnalisés pour calculer les signaux sans modifier les spécifications du contrat.
Digression analytique : Les pièges des instruments synthétiques
Corrélation n'est pas causalité — le lien historique entre les actifs peut être rompu au moment de chocs macro-économiques. L'écart synthétique EURUSD/GBPUSD, stable depuis des années, peut créer un décalage de 50 à 80 points suite à des annonces concernant le taux directeur de la banque centrale ou la situation géopolitique. La stratégie de retour à la moyenne ne tient pas compte des changements structurels des actifs (symboles).
Calcul des marges et des bénéfices dans le Testeur — le Testeur de Stratégie recherche automatiquement les taux croisés pour convertir la marge et obtenir un résultat financier. Si vous testez SYNTH_EURGBP.custom sur un compte en USD, le terminal recherche les paires dans l'ordre suivant :
- EURUSD.custom / GBPUSD.custom (personnalisé)
- EURUSD.b / GBPUSD.b (avec le suffixe du courtier)
- EURUSD / GBPUSD (paires de base)
Si ces données sont absentes du Market Watch, le Testeur renverra une erreur de calcul de marge ou fixera le profit à zéro.
Désactivation du réseau MQL5 Cloud Network — l'optimisation sur les symboles synthétiques via les agents cloud est désactivée. Étant donné que différentes machines peuvent stocker des symboles définis par l'utilisateur avec les mêmes noms mais des historiques ou des facteurs de normalisation différents, cela entraînerait une désynchronisation des résultats et un trafic excessif. Par conséquent, les tests ne sont possibles qu'en local ou sur un réseau local.
Filtrage des actualités — l'arbitrage de spread statistique est très vulnérable aux événements macro-économiques asymétriques (lorsque l'événement n'affecte qu'un seul pays émetteur). Il est recommandé d'ajouter le Calendrier Economique ou le filtre de volatilité qui désactive la stratégie 30 à 60 minutes avant la publication de données à fort impact.
Pipeline d'intégration des flux de travail
Dans le développement de systèmes de trading MQL5, un « pipeline d'intégration » est une chaîne de traitement de données et d'événements qui relie une source externe à l'interface de trading du terminal.
Autrement dit, il s'agit d'un pipeline par lequel les données brutes (ticks) passent par plusieurs étapes de traitement jusqu'à ce qu'elles soient transformées en un outil de trading ou d'analyse prêt à l'emploi. En pratique, la structure du pipeline (des données à la transaction) se compose de quatre étapes séquentielles :
- Lorsque les ticks bruts sont reçus du courtier, nous utilisons les fonctions SymbolInfoTick() et/ou l'historique des ticks CopyTicksRange(). Par exemple, les données de cotation pour l'EURUSD et le GBPUSD proviennent de serveurs différents.
- La classe d’agrégation (CBarAggregator ou CSyntheticTickGenerator) reçoit ces ticks, les synchronise dans le temps et applique une équation mathématique. Par exemple, il calcule l'écart entre le prix (EUR) et le prix (GBP).
- Le résultat est écrit dans la base de données du terminal via les fonctions CustomRatesUpdate() ou CustomTicksAdd(). Par exemple, la création du symbole personnalisé SPREAD_EURGBP.custom. Le terminal traite désormais cet instrument comme une paire de devises classique.
- Utilisation des outils standard de la plateforme pour le symbole créé - lancement de l'EA/indicateur dans le Testeur de Stratégie.
Tests de résistance via un historique modifié
Les backtests standards créent souvent l'illusion de sécurité. Vous exécutez un EA sur un historique parfait, avec une exécution parfaite, et vous obtenez une image magnifique de la courbe des capitaux propres. Mais le marché réel est un environnement truffé de ticks toxiques, où les spreads s'élargissent lors d'événements d'actualité et où les problèmes de liquidité s'aggravent.
MetaTrader 5 vous offre une opportunité unique de devenir votre propre courtier. Vous pouvez non seulement analyser l'historique, mais aussi le modifier, en dégradant délibérément les conditions de marché pour tester la robustesse de la stratégie. Si un robot gagne de l'argent avec des données idéales, mais perd tout le capital lorsque le spread s'élargit de 5 pips, alors ce système n'est pas viable.
Voyons comment utiliser des symboles personnalisés pour créer des scénarios de crise : de la majoration artificielle des coûts de transaction à la simulation des restrictions des courtiers pendant les périodes de forte volatilité.
La philosophie qui sous-tend les tests de résistance est assez simple : nous essayons de mettre le système en défaut avant de l'introduire sur le marché réel.
Nous prenons un historique de base de symboles et le modifions pour simuler des conditions de marché extrêmes.
Scénarios de modification :
- élargissement des spreads simule des hausses de coûts qui sont essentielles pour le scalping et le day trading
- augmentation du niveau de Stop/de Freeze — test de résistance aux restrictions des courtiers sur la définition des stops loss et des take profit
- évolution des exigences de marge – test de la résistance aux appels de marge lors de la modification de l’effet de levier
- injection de ticks toxiques - ajout de gaps ou de ruptures de liquidité
Le principal avantage de l'approche par symboles personnalisés est la reproductibilité. Vous pouvez exécuter le même test avec différents paramètres de stress et comparer les indicateurs (facteur de profit, perte maximale, facteur de récupération) sous forme de tableau.
Scénario 1 : Élargissement artificiel du spread
Le spread est le principal ennemi des stratégies à court terme. Dans la réalité, le spread n'est pas fixe. Il fluctue entre zéro et, en pratique, l'infini, et peut s'élargir considérablement lors de la publication d'informations macro-économiques importantes. Pour tester cela, nous pouvons prendre l'historique des ticks, cloner le symbole et réécrire les ticks, en ajoutant un nombre fixe de pips ou un pourcentage de l'écart actuel à chaque Ask.
Voici un exemple de script qui crée une version stressée d'un symbole avec un spread fixe élevé :
//+------------------------------------------------------------------+ //| inputs | //+------------------------------------------------------------------+ input string SourceSymbol = "EURUSD"; // Source symbol input string TargetSymbol = "EURUSD_Stress"; // New symbol name input int StressSpreadPoints = 50; // Fixed spread (in points) input datetime HistoryFromDate = D'2023.01.01'; // Start loading history input bool DeleteOldData = true; // Clear the target symbol's history before writing CiCustomSymbol stressSymb; //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { double point; int digits; //--- get the properties of the original symbol for calculations point = SymbolInfoDouble(SourceSymbol, SYMBOL_POINT); digits = (int)SymbolInfoInteger(SourceSymbol, SYMBOL_DIGITS); if(point == 0) { Print("Error: Failed to get point size for ", SourceSymbol); return; } Print("--- Start generating stress symbol ---"); PrintFormat("Source: %s | Target: %s | Spread: %d pp", SourceSymbol, TargetSymbol, StressSpreadPoints); //--- create a custom symbol via a class method //--- return codes: -1 (error), 0 (already exists), 1 (created) int createRes = stressSymb.Create(TargetSymbol, "", SourceSymbol, 1000000, true); if(createRes == -1) { Print("Error creating symbol via CiCustomSymbol.Create()"); return; } stressSymb.Select(true); //--- clone the contract properties (Digits, Point, Mode etc.) if(!stressSymb.Clone(SourceSymbol)) { Print("Error cloning symbol properties"); return; } //--- clear old history if(DeleteOldData) { Print("Clear history using CiCustomSymbol..."); if(stressSymb.TicksDelete(0, LONG_MAX) < 0) Print("Failed to clear ticks: ", GetLastError()); } //--- cyclic loading and modification of ticks (in 1-day batches) datetime current_date = HistoryFromDate; datetime stop_date = TimeCurrent(); if(current_date >= stop_date) { Print("Error: Invalid history start date."); return; } int total_ticks_added = 0; MqlTick ticks[]; //--- calculate the stress spread value double stress_spread_value = StressSpreadPoints * point; Print("Starting batch history processing..."); while(current_date < stop_date && !IsStopped()) { //--- define the boundaries of the day datetime day_start = current_date; datetime day_end = current_date + PeriodSeconds(PERIOD_D1); ulong t1 = (ulong)day_start * 1000; ulong t2 = (ulong)day_end * 1000; //--- read the original symbol ticks int copied = CopyTicksRange(SourceSymbol, ticks, COPY_TICKS_ALL, t1, t2); if(copied > 0) { //--- data stress modification for(int i = 0; i < copied; i++) { //--- set Ask = Bid + Fixed Spread ticks[i].ask = ticks[i].bid + stress_spread_value; //--- mark that both prices have changed ticks[i].flags = TICK_FLAG_BID | TICK_FLAG_ASK; } //--- write to the database via a class int added = stressSymb.TicksReplace(t1, t2, ticks); if(added != copied) { PrintFormat("Error recording ticks for %s via class. Registered: %d of %d", TimeToString(day_start, TIME_DATE), added, copied); } else { total_ticks_added += added; } } //--- move on to the next day current_date = day_end; Sleep(10); } Print("--- Generation complete ---"); PrintFormat("Total ticks processed: %d", total_ticks_added); } //+------------------------------------------------------------------+
Le script utilise la classe wrapper CiCustomSymbol pour gérer les symboles personnalisés. Le code complet du script se trouve dans le fichier StressTest_SpreadModifier.mq5 joint à l'article.
Scénario 2 : Niveau de Stop et niveau de Freeze
Lors des périodes de forte volatilité (par exemple, avant la publication des données NFP aux États-Unis), de nombreux courtiers augmentent la distance minimale pour la définition des ordres stop loss (niveau de Stop) et le niveau de gel des ordres (niveau de Freeze). Si votre stratégie fixe un stop loss à 5 pips et que votre courtier exige un minimum de 20 pips à l'entrée, l'ordre sera rejeté par le serveur ou la position restera sans stop loss.
Comment vérifier cela dans MetaTrader 5 ? Les propriétés du contrat sont spécifiées par la fonction CustomSymbolSetInteger(). Vous pouvez créer un symbole personnalisé où SYMBOL_TRADE_STOPS_LEVEL est défini à 5-10 fois la valeur normale.
//--- set an extreme stop level (for example, 500 points) long huge_stop_level = 500; if(!CustomSymbolSetInteger(stress_symbol, SYMBOL_TRADE_STOPS_LEVEL, huge_stop_level)) Print("Error setting stop level"); //--- set the freeze level (so that orders cannot be modified close to the market) long huge_freeze_level = 500; if(!CustomSymbolSetInteger(stress_symbol, SYMBOL_TRADE_FREEZE_LEVEL, huge_freeze_level)) Print("Error setting freeze level");
Après avoir exécuté le Testeur sur un tel symbole, consultez le journal. Si la stratégie tente de placer un stop trop près, le Testeur renverra « Erreur OrderSend 130 » (stops invalides). Cela vous permettra d'évaluer dans quelle mesure la stratégie repose sur la capacité à placer des stops serrés.
Scénario 3 : Exigences de marge et effet de levier
La modification des exigences de marge vous permet de tester la résilience de votre stratégie face aux appels de marge lorsque votre effet de levier disponible diminue. Cela est particulièrement vrai pour les stratégies de grille et les martingales, qui ont tendance à perdre leurs dépôts lorsqu'il n'y a pas suffisamment de marge libre. Vous pouvez gonfler artificiellement SYMBOL_MARGIN_INITIAL (marge initiale).
//--- increase the margin requirement 2 times double normal_margin = SymbolInfoDouble(source_symbol, SYMBOL_MARGIN_INITIAL); double stress_margin = normal_margin * 2.0; if(!CustomSymbolSetDouble(stress_symbol, SYMBOL_MARGIN_INITIAL, stress_margin)) Print("Error setting margin");
Digression analytique : Comment distinguer un risque d'un artefact
Lors de l'interprétation des résultats des tests de résistance, il est important de ne pas tomber dans les extrêmes :
- une dégradation progressive est souhaitable. Si la courbe des capitaux propres diminue progressivement à mesure que les conditions se détériorent (augmentation du spread, augmentation des stops), alors la stratégie dispose d'une marge de sécurité
- une rupture brutale est mauvaise. Si une stratégie affiche un résultat positif sur un spread de 20, mais une perte totale sur un spread de 21, il s'agit du Graal ajusté (sur-optimisé) à des conditions étroites. Dans des situations de trading réelles, un tel robot ne survivrait pas longtemps.
Sources de données
Pour un test de résistance correct, la qualité de l'historique des ticks est importante. Les barres de minutes intégrées (M1) ne conviennent pas aux tests de spread, car le spread est intégré au prix de clôture ou n'est pas présent en tant qu'entité distincte. Utilisez CopyTicks() et des symboles personnalisés construits sur des ticks.
Étapes de test de la stratégie :
- préparation des données : téléchargez des ticks de haute qualité (par exemple, depuis Dukascopy) ou assurez-vous que le terminal dispose d’un historique complet des ticks chargé
- création d'un environnement de test : utilisez un script qui clone le symbole de base (par exemple EURUSD → EURUSD_Stress_50) et applique des modificateurs (spread, stops, marge)
- exécution par lots - utilisez l'optimiseur du Testeur de Stratégie
Astuces :
Au lieu de modifier le code de l’EA, modifiez le symbole sur lequel il est testé. Exécuter l'optimisation pour un ensemble de symboles personnalisés : EURUSD_Normal, EURUSD_Spread_30, EURUSD_Spread_50. Combinez les résultats (bénéfice net, drawdown) dans un tableau ou une base de données. Une stratégie qui reste rentable dans tous les cas de figure peut être envisagée pour une utilisation sur un compte réel.
Intégration des flux de travail : Du Testeur au graphique en direct
Supposons que nous ayons créé des symboles personnalisés, généré des graphiques non dépendants du temps basés dessus (Renko, Range) et testé la stratégie dans le Testeur. Mais c'est là que réside le piège : le serveur de trading de votre courtier ne sait rien de l'existence de EURUSD_Renko_10.
Si vous essayez d'envoyer directement un ordre pour un symbole personnalisé, le terminal renverra l'erreur 4756 (Unknown Symbol (« symbole inconnu »)). Les symboles personnalisés n'existent que dans le terminal client. Comment peut-on alors créer un EA qui prend des décisions basées sur les barres Renko pour trader l'EUR/USD réel ?
Pour répondre à cette question, nous analyserons la structure des échanges avec routage des ordres. Nous allons créer un mécanisme qui remplacera de manière transparente les symboles dans les requêtes de trading, ce qui nous permettra d'analyser un instrument et d'en trader un autre.
Problème de routage et réalité virtuelle
Lorsqu'un EA est placé sur un graphique de symbole personnalisé, la variable système _Symbol renvoie le nom de cet instrument personnalisé (par exemple XAGUSD_Range_10), ce qui n'est pas surprenant. Dans le même temps, l'EA utilise généralement le champ _Symbol ou (ce qui revient au même) la fonction système Symbol() pour les opérations suivantes :
- envoi des ordres
- demande de cotations (SymbolInfoTick())
- vérification des positions ouvertes (PositionSelect, etc.)
Pour que cela fonctionne avec le marché réel, nous devons intercepter tous ces appels et remplacer _Symbol par un instrument réel (par exemple, XAGUSD). Réécrire manuellement le code pour chaque EA n'est pas notre approche. La solution est simple : la classe wrapper CustomOrder. Créons une classe qui reproduit les fonctions clés de l'API MQL5. Dans ces fonctions, une vérification est effectuée : si le symbole graphique actuel (personnalisé) est demandé, il est remplacé par le symbole réel.
Pour éviter de modifier le code source de l'EA, nous utiliserons la directive #define, qui remplacera les appels standard par les nôtres au niveau du préprocesseur.
L'implémentation de la classe permettant de gérer les symboles personnalisés est fournie ci-dessous :
//+------------------------------------------------------------------+ //| class for routing orders | //| purpose: replacing a custom symbol with a real one | //+------------------------------------------------------------------+ class CustomOrder { private: static string workSymbol; // real symbol name public: //--- set a replacement symbol static void setReplacementSymbol(const string replacement) { workSymbol = replacement; } //--- send a trade request static bool OrderSend(MqlTradeRequest &request, MqlTradeResult &result) { //--- replace a request symbol if(request.symbol == _Symbol && workSymbol != "") { request.symbol = workSymbol; //--- price adjustment if it is taken from a custom symbol if(request.type == ORDER_TYPE_BUY) request.price = SymbolInfoDouble(workSymbol, SYMBOL_ASK); else if(request.type == ORDER_TYPE_SELL) request.price = SymbolInfoDouble(workSymbol, SYMBOL_BID); } //--- call the original function return ::OrderSend(request, result); } //--- calculate profit static bool OrderCalcProfit(ENUM_ORDER_TYPE action, string symbol, double volume, double price_open, double price_close, double &profit) { if(symbol == _Symbol && workSymbol != "") symbol = workSymbol; return ::OrderCalcProfit(action, symbol, volume, price_open, price_close, profit); } //--- get the position string property static string PositionGetString(ENUM_POSITION_PROPERTY_STRING property_id) { string res = ::PositionGetString(property_id); //--- if a position symbol is requested, return the chart name, not the actual one if(property_id == POSITION_SYMBOL && res == workSymbol) return _Symbol; return res; } //--- get the order string property static string OrderGetString(ENUM_ORDER_PROPERTY_STRING property_id) { string res = ::OrderGetString(property_id); if(property_id == ORDER_SYMBOL && res == workSymbol) return _Symbol; return res; } //--- select position by symbol static bool PositionSelect(string symbol) { if(symbol == _Symbol && workSymbol != "") return ::PositionSelect(workSymbol); return ::PositionSelect(symbol); } }; //+------------------------------------------------------------------+ //| static initialization | //+------------------------------------------------------------------+ string CustomOrder::workSymbol = ""; //+------------------------------------------------------------------+ //| macros for transparent integration | //+------------------------------------------------------------------+ #define OrderSend(request, result) CustomOrder::OrderSend(request, result) #define OrderCalcProfit(action, symbol, volume, open, close, profit) CustomOrder::OrderCalcProfit(action, symbol, volume, open, close, profit) #define PositionGetString(prop) CustomOrder::PositionGetString(prop) #define OrderGetString(prop) CustomOrder::OrderGetString(prop) #define PositionSelect(symbol) CustomOrder::PositionSelect(symbol) //+------------------------------------------------------------------+
Cette implémentation a une particularité : les macros remplacent les fonctions de trading standard du terminal MetaTrader 5 avec les mêmes paramètres. Le code complet se trouve dans le fichier CustomOrder.mqh joint à cet article.
Exemple d'intégration dans l'EA
Imaginons que nous ayons un EA TrendFollower standard généré par l'Assistant MQL5. Pour pouvoir trader un actif réel sur un graphique Renko, nous devons effectuer trois étapes :
1. Inclure l'en-tête — la directive #include doit figurer en premier, avant l'inclusion des bibliothèques standard. Cela garantit que les macros remplaceront les appels avant la compilation du code de la bibliothèque.
//+------------------------------------------------------------------+ //| TrendFollower.mq5 | //+------------------------------------------------------------------+ #include <CustomOrder.mqh> #include <Expert\Expert.mqh> #include <Expert\Signal\MySignals\SignalMACD.mqh> //--- other declarations...
2. Saisir une entrée – nous ajoutons un paramètre pour indiquer à l’EA le symbole réel auquel envoyer des ordres :
input string InpWorkSymbol = "XAGUSD"; // Actual symbol to execute
3. Initialiser le symbole dans OnInit() — dans la fonction d'initialisation, nous passons le symbole réel à notre classe. Nous utilisons également une astuce ingénieuse pour le Testeur visuel : pour éviter que le graphique ne se bloque, nous devons forcer une demande de cotations pour le symbole réel :
int OnInit() { //--- configure the router if(InpWorkSymbol != "") { CustomOrder::setReplacementSymbol(InpWorkSymbol); //--- we force loading the history of the real symbol, so that the visualization goes smoothly MqlRates rates[1]; CopyRates(InpWorkSymbol, PERIOD_M1, 0, 1, rates); } //--- standard EA initialization... // ... return(INIT_SUCCEEDED); }
Après ces modifications, l'EA analysera les graphiques XAGUSD_Renko_10, mais tous les ordres seront traités sur XAGUSD et le symbole sera correctement affiché dans le journal des transactions.
Digression analytique : Éliminer le biais de prévision dans le Testeur
Pourquoi avons-nous besoin de tout cela si nous pouvons simplement tester sur des données réelles ? Rappelons la section concernant Renko. Nous avons constaté que le Testeur de Stratégie génère des ticks en fonction de la configuration de la barre (OHLC) lors des tests sur un symbole personnalisé. Pour la barre Renko, cela crée l'illusion d'une entrée idéale : l'EA voit la barre se fermer et entre immédiatement au prix de clôture. Mais en réalité, la brique Renko met un certain temps à se former. Le prix peut fluctuer à l'intérieur de la fourchette de formation de la barre.
Lorsque vous activez le routage (à l'aide de la classe CustomOrder), voici ce qui se produit :
- L'EA détecte un signal sur le graphique avec un symbole personnalisé (par exemple, l'intersection de la MA sur Renko),
- Il envoie une demande d'achat ou de vente.
- La classe CustomOrder remplace un symbole par EURUSD,
- La transaction est exécutée au cours actuel du marché de l'EURUSD réel.
Par conséquent, vous obtenez des résultats réalistes. Le prix d'entrée peut différer du prix indiqué sur le graphique Renko en raison d'un glissement ou d'une asynchronie des données. La différence entre un test de symbole personnalisé et un test de routage réside dans le coût de votre hypothèse de trading.
Avant de commencer à trader en réel sur des symboles personnalisés, veuillez vous assurer des points suivants :
- Synchronisation de l'historique - assurez-vous que le symbole réel possède un historique complet dans le terminal. En l'absence de données, CustomOrder risque de calculer incorrectement la marge ou le prix d'entrée ;
- Aucune erreur avec le code 4756 - consultez le journal de l'expert. Si vous voyez « Unknown Symbol », cela signifie que les macros n'ont pas fonctionné (vous avez peut-être inclus CustomOrder.mqh après d'autres bibliothèques) ;
- Slippage - Lorsque vous effectuez une transaction via un wrapper, vous obtenez le prix du marché. Assurez-vous que le slippage autorisé dans les paramètres de l'EA est suffisamment élevé pour garantir l'exécution des ordres lors de mouvements rapides, lorsque les signaux Renko sont générés plus fréquemment que la mise à jour du tick du symbole réel.
Si vous utilisez des symboles personnalisés pour l'analyse, l'optimisation cloud sur le réseau MQL5 Cloud Network n'est pas disponible. Utilisez un réseau local et/ou un ordinateur local.
Conclusion
MetaTrader 5 a connu une évolution discrète mais fondamentale : le terminal n’est plus un simple récepteur passif de cotations et est devenu un véritable laboratoire d'ingénierie. Désormais, les traders n'ont plus à s'adapter à ce que propose le courtier, mais peuvent construire leur propre environnement analytique optimal pour une idée spécifique.
Nous avons appris à nous découpler du calendrier grâce aux graphiques Renko, Range et Volume Egal, à synthétiser les relations entre marchés grâce aux spreads et aux paniers, à détériorer volontairement l’historique pour les tests de résistance et, enfin, à relier le trading virtuel à l'exécution réelle grâce à un routage d'ordres transparent. Chacune de ces étapes aborde un problème spécifique pour le développeur du système : le bruit temporel, le manque d’outils nécessaires, l’illusion d’un backtest parfait et l’écart entre le graphique et le serveur.
L'intérêt principal des symboles personnalisés réside dans le contrôle des données. Vous testez non seulement les paramètres de l'EA, mais aussi la résistance de la logique elle-même aux coûts de transaction, à l'asynchronisme des ticks et aux restrictions du courtier. Si une stratégie maintient une valeur attendue positive avec un spread fixe de 50 pips, des gaps artificiels et un niveau de stop dynamique, elle est prête pour le marché réel. Sinon, vous préservez votre capital en découvrant une faiblesse lors de la phase de simulation.
Commencez petit : prenez n’importe quel EA de scalping ou de tendance, créez un graphique avec des barres de Volume Egal, exécutez un scénario de stress avec un spread accru et mettez en place le routage via CustomOrder. Comparez les indicateurs.
Vous serez peut-être surpris de voir comment la courbe des capitaux propres évolue lorsque le bruit lié au découpage temporel est éliminé et que les risques liés aux transactions sont calculés à l'avance. La documentation MQL5, la CodeBase et le forum communautaire sont vos principaux alliés pour construire votre propre réalité de marché.
Ressources recommandées pour apprendre à utiliser les symboles personnalisés dans MetaTrader 5 :
- Documentation sur les symboles personnalisés,
- Symboles personnalisés : notions pratiques de base,
- Création et test de symboles personnalisés dans MetaTrader 5,
- Tests de résistance des stratégies de trading à l'aide de symboles personnalisés,
- Symboles personnalisés MQL5 : création d'un symbole de barres 3D.
Liste des fichiers joints à l'article :
| Nom de fichier | Description |
|---|---|
| CiCustomSymbol.mqh | Code de la classe CiCustomSymbol |
| CreateCustomSymbol.mq5 | Exemple de code pour la création d'un symbole personnalisé. |
| CBarAggregator.mqh | Code de la classe CBarAggregator |
| CustomChartGenerator.mq5 | Code d'un indicateur qui génère des barres historiques et en temps réel de trois types : barres Renko, Range Bars et Volume Egal |
| CSyntheticTickGenerator.mqh | Code de la classe CSyntheticTickGenerator |
| StressTest_SpreadModifier.mq5 | Code d'un script d'exemple permettant de créer un symbole personnalisé pour des tests de résistance avec un spread large. |
| CustomOrder.mqh | Code de la classe CustomOrder |
Traduit du russe par MetaQuotes Ltd.
Article original : https://www.mql5.com/ru/articles/22391
Avertissement: Tous les droits sur ces documents sont réservés par MetaQuotes Ltd. La copie ou la réimpression de ces documents, en tout ou en partie, est interdite.
Comment Échanger des Données : Une DLL pour MQL5 en 10 minutes
Du CPU au GPU en MQL5 : un framework OpenCL pratique pour accélérer la recherche, l’optimisation et la détection de motifs
L'Histogramme des prix (Profile du Marché) et son implémentation en MQL5
MetaTrader 5 et le Calendrier Économique MQL5 : comment transformer les actualités en un système de trading reproductible
- 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
Un article très intéressant — je le relis…
Est-ce que quelqu'un a réussi à créer un graphique Renko à l'aide de l'indicateur présenté dans cet article ?
Pour une raison que j'ignore, cet indicateur ne veut tout simplement pas fonctionner chez moi.
Est-ce que quelqu'un a réussi à créer un graphique Renko à l'aide de l'indicateur présenté dans cet article ?
Pour une raison que j'ignore, cet indicateur ne veut absolument pas fonctionner chez moi.
Ils ont mis un « bloc » vraiment énorme.
Ils ont installé un très gros « bloc ».
Je l'ai placé tel qu'il était par défaut dans l'indicateur.
Merci, en effet, si l'on réduit la taille de la « brique », le graphique s'affiche.