ZoneMartingale EA est le robot de « zone recovery » codé en direct sur la chaîne, et dont le code source complet est distribué gratuitement. C'est un bon objet d'étude : le fichier tient en moins de 200 lignes, il n'a que quatre paramètres, et il montre sans détour comment fonctionne une martingale à zone. Il montre aussi pourquoi ce type de robot demande de la prudence.
Cet article reprend le code de la version 1.0 bloc par bloc. À la fin, vous saurez lire chaque décision du robot, refaire le calcul qui explique ses gains, et corriger les points faibles de cette première version.
Le principe en une minute
Le robot ouvre une première position quand le RSI sort d'une zone extrême. Autour du prix d'entrée, il trace une zone de largeur fixe et deux objectifs, un au-dessus et un en dessous.
- Si le prix atteint l'un des deux objectifs, toutes les positions sont fermées : le cycle est terminé.
- Si le prix traverse la zone dans l'autre sens, le robot ouvre une position opposée, avec un lot multiplié par un facteur (2 par défaut).
- Chaque nouvelle traversée ajoute une position, toujours plus grosse, dans le sens du mouvement.
Tant que le prix finit par atteindre un objectif, le cycle se termine en gain. Le risque est ailleurs : si le prix fait des allers-retours dans la zone, le lot double à chaque fois.
Les paramètres
input double lot = 0.01;
input double Range = 2000;
input double Target = 4000;
input double factor = 2.0;lot: le volume de la première position.Range: la largeur de la zone, en points.Target: la distance entre le bord de la zone et chaque objectif, en points.factor: le multiplicateur appliqué à chaque nouvelle position.
Un point, c'est le plus petit pas de prix du symbole (_Point). Sur un symbole coté avec deux décimales, 2 000 points font 20,00 de prix ; avec cinq décimales, 0,02000. Les distances se règlent donc symbole par symbole.
Le signal d'entrée : le RSI qui sort d'un extrême
int bars = iBars(_Symbol, PERIOD_CURRENT);
if(totalBars == bars) return;
totalBars = bars;
if(PositionsTotal() > 0) return;
if(!CopyBuffer(rsi_handle, 0,1,2, rsiData)) return;
if(rsiData[1] < OverSoldLevel && rsiData[0] > OverSoldLevel) {
MQL5_Lab_trade.Buy(lot, _Symbol, Bid, 0,0, "MQL5 Lab");Trois choses à remarquer :
- Le signal n'est cherché qu'une fois par bougie, à l'ouverture d'une nouvelle barre. Le robot ne réagit donc pas aux mouvements à l'intérieur d'une bougie pour entrer.
- Aucun signal n'est cherché tant qu'une position est ouverte : un seul cycle à la fois.
CopyBuffer(rsi_handle, 0, 1, 2, rsiData)copie le RSI des deux dernières bougies clôturées. SansArraySetAsSeries, le tableau garde l'ordre chronologique :rsiData[0]est la plus ancienne,rsiData[1]la plus récente. La conditionrsiData[1] < 30 && rsiData[0] > 30signifie donc : le RSI était au-dessus de 30 il y a deux bougies et il est passé sous 30 sur la dernière bougie clôturée.
Ce détail compte : le commentaire du code parle d'une « sortie de survente », mais la condition, lue dans l'ordre réel du tableau, détecte une entrée en survente. Le robot achète donc quand le RSI vient de passer sous 30, pas quand il en ressort. C'est un comportement défendable (acheter l'excès), mais ce n'est pas celui que le commentaire annonce. Si vous voulez l'achat sur la sortie de survente, inversez les indices. Le même raisonnement vaut pour la vente autour de 70.
La zone et les objectifs
Une fois l'achat exécuté, le robot lit le prix d'ouverture réel et trace ses niveaux :
double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
zoneHigh = NormalizeDouble(openPrice, _Digits);
zoneLow = NormalizeDouble(zoneHigh - zoneRange, _Digits);
zoneTargetHigh = NormalizeDouble(zoneHigh + zoneTarget, _Digits);
zoneTargetLow = NormalizeDouble(zoneLow - zoneTarget, _Digits);Pour un achat, le haut de la zone est le prix d'entrée et le bas se trouve Range points plus bas. Les objectifs sont à Target points au-delà de chaque bord. Pour une vente, c'est le symétrique : le bas de la zone est le prix d'entrée.
La récupération : une position opposée à chaque traversée
if(Bid > zoneHigh) {
if(isBuyDone == false || lastDirection < 0) {
MQL5_Lab_trade.Buy(lots_Rec, _Symbol, Bid, 0,0, "MQL5 Lab");
lastDirection = 1;
recovery_lot = recovery_lot*factor;À chaque tick, le robot regarde où se trouve le prix. S'il passe au-dessus du haut de la zone alors que la dernière position était une vente, il achète. S'il passe sous le bas alors que la dernière était un achat, il vend. Le lot de récupération démarre à lot × factor et est multiplié par factor à chaque nouvelle position.
La sortie : tout fermer sur un objectif
if (Bid > zoneTargetHigh || Bid < zoneTargetLow) {
deleteZoneLevels();
for (int i=PositionsTotal()-1; i>=0; i--) {
ulong ticket = PositionGetTicket(i);
...
MQL5_Lab_trade.PositionClose(ticket);Dès que le prix dépasse l'un des deux objectifs, toutes les positions sont fermées et les variables remises à zéro. Le robot attend alors un nouveau signal RSI.
Pourquoi le cycle finit en gain
Prenons les réglages par défaut : zone de largeur Z, objectifs à 2Z, facteur 2, et un premier lot de 1 (pour simplifier les calculs). On compte en multiples de Z, sans le spread.
| Scénario | Positions ouvertes | Résultat à l'objectif |
|---|---|---|
| Le prix monte directement à l'objectif haut | Achat 1 | +2 |
| Il descend, puis atteint l'objectif bas | Achat 1, vente 2 | −3 + 4 = +1 |
| Bas, haut, puis objectif haut | Achat 1, vente 2, achat 4 | +2 − 6 + 8 = +4 |
| Bas, haut, bas, puis objectif bas | Achat 1, vente 2, achat 4, vente 8 | −3 + 4 − 12 + 16 = +5 |
Chaque nouvelle position, deux fois plus grosse que la précédente et dans le sens du mouvement, couvre les pertes des autres. Le cycle finit donc toujours positif si le prix atteint un objectif, si les ordres s'exécutent aux niveaux prévus, et si le compte a la marge pour tenir.
Le prix à payer : l'exposition
Le volume total ouvert après n positions vaut lot × (2^n − 1) avec un facteur 2 :
| Positions ouvertes | Dernier lot | Volume total |
|---|---|---|
| 1 | 0,01 | 0,01 |
| 4 | 0,08 | 0,15 |
| 6 | 0,32 | 0,63 |
| 8 | 1,28 | 2,55 |
| 10 | 5,12 | 10,23 |
Neuf traversées de la zone d'affilée, ce qui arrive en période de range, transforment un premier lot de 0,01 en plus de 10 lots ouverts. Le gain du cycle, lui, reste de l'ordre de quelques Z au lot initial. Le spread se paie à chaque nouvelle position, et une exécution au-delà du niveau prévu (glissement, ouverture du lundi) entame le calcul du tableau précédent. C'est tout le problème des martingales : beaucoup de petits gains, et un risque rare mais très lourd.
Cinq corrections à faire avant de s'en servir
La version 1.0 a été écrite pour montrer le principe, en direct. Avant de l'utiliser, même en démo, voici ce qu'il faut corriger.
1. Ne gérer que ses propres positions
Le robot compte et ferme toutes les positions du compte, quel que soit le symbole. Une position manuelle sur un autre actif bloque les nouveaux signaux, et un objectif atteint la fermerait. Ajoutez un numéro magique et filtrez :
input long Magic = 2601;
// dans OnInit()
MQL5_Lab_trade.SetExpertMagicNumber(Magic);
// une fonction pour compter seulement les positions du robot
int MesPositions() {
int n = 0;
for(int i = PositionsTotal() - 1; i >= 0; i--) {
ulong t = PositionGetTicket(i);
if(t > 0 && PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == Magic) n++;
}
return n;
}Utilisez ce filtre partout où le code parcourt les positions, et remplacez PositionsTotal() > 0 par MesPositions() > 0.
2. Plafonner la récupération
Ajoutez un nombre maximal de positions et un lot maximal. Une fois le plafond atteint, le robot n'ajoute plus rien et vous prévient :
input int MaxPositions = 6;
input double LotMax = 0.32;
if(MesPositions() >= MaxPositions || lots_Rec > LotMax) {
Alert(_Symbol, " : plafond de récupération atteint");
return;
}C'est la décision la plus importante : elle fixe à l'avance la perte maximale que vous acceptez sur un cycle.
3. Acheter au bon prix
Les achats passent Bid comme prix. Un achat s'exécute au prix Ask ; sur un compte en exécution instantanée, un prix faux peut faire refuser l'ordre. Passez 0 : la classe CTrade prend alors le prix courant du bon côté.
MQL5_Lab_trade.Buy(lots_Rec, _Symbol, 0, 0, 0, "ZoneMartingale");4. Tracer le bon objectif bas
Une faute de frappe trace l'objectif bas sous le nom ZONE_L au lieu de ZONE_T_L. Comme cet objet existe déjà, la ligne orange n'apparaît jamais. Il suffit de corriger le nom dans les deux appels à drawZoneLevel.
5. Ne pas perdre le fil au redémarrage
La zone, le sens et le lot en cours sont gardés dans des variables. Si MetaTrader redémarre au milieu d'un cycle, le robot oublie tout : les positions restent ouvertes, sans objectif ni récupération. Enregistrez l'état dans des variables globales du terminal (GlobalVariableSet) et relisez-le dans OnInit(), ou fermez le cycle à la main avant tout redémarrage.
Une dernière ligne peut partir : EventSetTimer(60) démarre un minuteur qu'aucune fonction OnTimer() n'utilise.
Comment le tester
- Ouvrez le testeur de stratégie (Ctrl+R), choisissez le robot, le symbole et l'unité de temps.
- Prenez le mode Chaque tick basé sur les ticks réels : la récupération se joue tick par tick, un mode approximatif fausserait le résultat.
- Regardez le drawdown maximal et le volume maximal ouvert, pas seulement le profit. Un profit régulier avec un drawdown de 60 % n'est pas un bon résultat.
- Testez au moins une période de range. C'est là que la martingale montre ses vraies limites.
Ce qu'il faut retenir
ZoneMartingale EA est un excellent support pour apprendre : un signal simple, une gestion d'état lisible, une exécution sans détour. Le calcul du cycle explique pourquoi les martingales semblent gagner presque toujours, et le tableau d'exposition explique pourquoi elles finissent parfois mal. Corrigez les cinq points ci-dessus, fixez un plafond que vous acceptez de perdre, et gardez-le en démo tant que vous n'avez pas vu ce qu'il fait dans un marché sans direction.