Application du filtrage de tendance L1 dans MetaTrader 5
- la dynamique à long terme des données soit préservée ;
- les fluctuations et le bruit à court terme soient supprimés ;
- les points de rupture structurels (changements de pente de la tendance) soient détectés automatiquement.

Sommaire
- Introduction
- 1. Formulation du problème de filtrage de tendance
1.1. Filtre de Hodrick–Prescott
1.2. Méthode de filtrage de tendance L1
1.3. Rôle du paramètre de régularisation λ
1.4. Interprétation géométrique
1.5. Algorithme de calcul de λmax - 2. Méthodes MQL5 de calcul de la tendance L1
2.1. L1TrendFilterLambdaMax
2.2. L1TrendFilter - 3. Exemples de calcul de la tendance L1
3.1. Tendance L1 sur des données synthétiques (marche aléatoire)
3.2. Tendance L1 pour la série des cours du S&P 500
3.3. Propriétés d’échelle de λmax
3.3.1. Expérience numérique pour le mouvement brownien
3.3.2. Loi d’échelle pour les séries temporelles financières
3.3.3. Conséquences pratiques de la loi d’échelle
3.4. Indicateurs de tendance L1
3.4.1. L1TrendFilter.mq5 - indicateur de tendance L1
3.4.2. L1TrendFilterSlope.mq5 - indicateur de pente de la tendance L1
3.4.3. L1TrendFilterSlopeSign.mq5 - indicateur de direction de la tendance - 3.4.4. Indicateurs de volatilité basés sur la tendance L1
3.4.4.1. L1Volatility.mq5 - indicateur de volatilité résiduelle
3.4.4.2. L1VolatilitySmoothed.mq5 - indicateur de volatilité résiduelle lissée
3.4.4.3. L1VolatilityAbsolute.mq5 - indicateur de volatilité absolue
3.4.4.4. L1VolatilityNormalized.mq5 - indicateur de volatilité normalisée
3.4.4.5. L1VolatilityNormalizedSmoothed.mq5 - indicateur de volatilité normalisée lissée
3.4.4.6. L1VolatilityRegime.mq5 - détection du régime de marché basée sur la volatilité -
3.5. Application de la tendance L1 dans les stratégies de trading
3.5.1. Stratégie Moving Average
3.5.1.1. Méthodologie générale d’évaluation de l’efficacité du filtrage de tendance L1
3.5.1.2. Résultats pour la stratégie Moving Average
3.5.2. Stratégie MACD
3.5.2.1. Résultats pour la stratégie MACD
3.5.3. Stratégie ADX
3.5.3.1. Résultats pour la stratégie ADX
3.5.4. Stratégie EMA
3.5.4.1. Résultats pour la stratégie EMA
3.5.5. Synthèse de l’utilisation du filtre L1 dans les stratégies de trading MovingAverage, MACD, ADX et EMA - Conclusion
Introduction
Les séries temporelles financières se caractérisent par un niveau de bruit élevé, des valeurs aberrantes fréquentes et des changements de régime de marché. Dans les systèmes de trading réels, cela se manifeste de manière simple et mesurable : les filtres « lisses » classiques (moyennes mobiles, HP) sont en retard, estompent les moments de changement de pente et interprètent souvent les corrections locales comme des retournements — en conséquence, le nombre de fausses entrées/sorties augmente, le Facteur de Profit diminue et le drawdown s’accroît. De plus, le choix du paramètre de régularisation λ se résume généralement à un réglage manuel et se transpose mal d’un instrument, d’une période ou d’une longueur d’historique à l’autre.Cet article propose une solution pratique à ces problèmes, fondée sur le filtrage de tendance L1 : l’optimisation avec régularisation L1 des différences secondes produit automatiquement une approximation linéaire par morceaux avec des points de rupture explicites. Les principaux avantages sont une interprétation claire des points de rupture comme des changements de régime, la possibilité de fixer l’échelle de la régularisation grâce au calcul de λmax et au passage à un paramètre relatif λ = coef · λmax, ainsi qu’une complexité de calcul linéaire, adaptée à une implémentation en MQL5.
Nous présentons non seulement la théorie, mais aussi une feuille de route pratique complète : les méthodes de calcul de λmax et de la tendance L1, trois indicateurs (tendance, pente, signe de la pente), sept indicateurs de volatilité basés sur la tendance L1, l’intégration dans des Expert Advisors et un protocole de test reproductible (quatre modes de filtrage, export du solde et des fonds, et visualisation).
1. Formulation du problème de filtrage de tendance
Nous considérons une série temporelle scalaire représentée comme la somme de deux composantes :
,
où
est la composante de tendance et
un bruit ou une composante irrégulière.
L’objectif est d’estimer la tendance
à partir des données observées
.
Le problème peut être formulé comme un compromis entre la fidélité aux données d’origine et la régularité de la tendance estimée.
1.1. Filtre de Hodrick–Prescott
Le filtre de Hodrick–Prescott définit la tendance comme la solution du problème de minimisation :
,
où le paramètre λ contrôle le degré de lissage.
Principales propriétés du filtre HP :
- Linéarité par rapport aux données ;
- Complexité de calcul O(n) ;
- Pour de petites valeurs de λ, la tendance se rapproche des données d’origine ;
- Pour de grandes valeurs de λ, la tendance tend vers la meilleure approximation linéaire.
Cependant, le filtre HP produit toujours une tendance lisse et détecte mal les changements brusques de pente.
1.2. Méthode de filtrage de tendance L1
L’idée principale du filtrage de tendance L1 est de trouver une tendance proche des données d’origine, mais présentant le moins de changements de pente possible. Contrairement aux méthodes de lissage classiques, qui minimisent le carré de la courbure, l’approche L1 minimise la somme des valeurs absolues des différences secondes.
Cela aboutit à un résultat fondamentalement différent :
- la plupart des différences secondes deviennent nulles,
- la tendance est automatiquement divisée en segments linéaires.
Ainsi, le filtre L1 ne cherche pas à rendre la tendance lisse, mais trouve plutôt le nombre minimal de changements structurels qui expliquent les données observées. Cette méthode est donc particulièrement adaptée aux séries temporelles financières, dont la dynamique consiste souvent en des séquences de phases de croissance et de déclin quasi linéaires.
Dans le filtrage de tendance L1, la pénalité quadratique sur les différences secondes est remplacée par la norme L1, et la tendance est définie comme la solution d’un problème d’optimisation convexe :

Sous forme matricielle :

avec :
- y — série temporelle d’entrée ;
- x — la tendance estimée ;
- D — matrice des différences secondes ;
- λ>=0 — paramètre de régularisation.
L’utilisation de la norme L1 conduit à un résultat fondamentalement différent : de nombreuses différences secondes deviennent nulles, ce qui signifie que la tendance est linéaire par morceaux.
La différence seconde est définie comme suit :

Si
, alors les points
se trouvent sur une même droite.
Par conséquent, une différence seconde nulle correspond à un segment linéaire de la tendance, tandis qu’une différence seconde non nulle correspond à un point de rupture. La norme L1 favorise la parcimonie dans le vecteur Dx, ce qui signifie que la plupart des différences secondes deviennent nulles. Cela implique que, sur les intervalles correspondants, la tendance est linéaire. Les points où les différences secondes sont non nulles sont interprétés comme des points de rupture de tendance.
Ainsi, la méthode de filtrage de tendance L1 construit automatiquement la tendance sous la forme d’un ensemble de segments linéaires reliés aux points de changement structurel.
Principales propriétés du filtrage de tendance L1 :
- La tendance se compose de segments linéaires ;
- Les points de rupture sont interprétés comme des changements structurels de la série temporelle ;
- À λ = 0, la tendance coïncide avec les données d’origine ;
- Pour des valeurs de λ suffisamment grandes, la tendance devient exactement la meilleure approximation linéaire ;
- La complexité de calcul reste linéaire par rapport au nombre d’observations.
1.3. Rôle du paramètre de régularisation λ
Le paramètre λ contrôle le compromis entre la précision de l’approximation et la complexité de la tendance :
| Valeur de λ | Nature de la solution |
|---|---|
| λ=0 | x=y, pas de lissage |
| Petit λ | Lissage faible, nombreux points de rupture |
| λ moyen | Tendance linéaire par morceaux |
| Grand λ | Tendance quasi linéaire |
| λ≥λmax | Tendance strictement linéaire |
Tableau 1. Dépendance de la tendance L1 vis-à-vis du paramètre de régularisation λ
Ainsi, λ contrôle le nombre et la position des points de rupture de la tendance.
1.4. Interprétation géométrique du problème
La tendance recherchée x peut être vue comme un point dans un espace à n dimensions. Le premier terme de la fonction objectif, responsable de la précision de l’approximation, définit une boule euclidienne centrée sur le point d’observation y : plus x est proche de y, plus l’erreur est faible.Le terme de régularisation avec la norme L1 des différences secondes définit un ensemble polyédrique convexe (polyèdre). Contrairement aux ellipsoïdes lisses qui apparaissent avec la régularisation L2, ce polyèdre possède des sommets saillants. Ces sommets correspondent à des situations où certaines différences secondes de la tendance sont égales à zéro.
C’est précisément la présence de coins saillants dans la norme L1 qui conduit à des solutions parcimonieuses : la solution optimale tend à se situer sur un sommet du polyèdre, où seules certaines contraintes sont actives. Cela signifie que la plupart des différences secondes deviennent nulles et que la tendance prend automatiquement une forme linéaire par morceaux.
La solution optimale correspond au premier point de contact entre la boule euclidienne et le polyèdre L1. En ce point, la tendance se compose de segments linéaires reliés par un nombre limité de points de rupture.
Le paramètre λmax correspond à la situation où la boule euclidienne touche le polyèdre L1 non pas en un sommet, mais le long du sous-espace des fonctions linéaires. Dans ce cas, toutes les différences secondes sont nulles et la tendance est strictement linéaire.
Pour λ ≥ λmax, aucune des contraintes L1 ne devient active ; une augmentation supplémentaire de la régularisation ne modifie donc pas la solution, et la tendance reste linéaire.
1.5. Algorithme de calcul de λmax
Considérons le calcul du paramètre de régularisation maximal λmax pour un vecteur d’entrée y de longueur N.
1. Construisez la matrice des différences secondes D de taille (N−2)×N :

2. Calculez le vecteur de courbure Dy.
3. Résolvez le système d’équations linéaires :
![]()
4. Prenez l’élément maximal (en valeur absolue) du vecteur v :

Pour les séries temporelles financières, le paramètre λmax revêt une importance pratique considérable :
- Il permet de normaliser le paramètre de régularisation ;
- Il rend le choix de λ indépendant de l’échelle des données ;
- Il simplifie la comparaison entre différentes séries temporelles ;
- Il permet d’interpréter λ comme une fraction de la régularisation maximale.
L’utilisation d’un paramètre relatif de la forme : λ=coef_lambda_max⋅λmax, où coef_lambda_max ∈ (0,1), simplifie grandement l’application pratique.
Dans les exemples d’indicateurs et d’Expert Advisors qui suivent, λ sera exprimé en unités de λmax, et les paramètres indiqueront le multiplicateur coef_lambda_max.
2. Méthodes MQL5 de calcul de la tendance L1
Pour une utilisation pratique du filtrage de tendance L1, deux méthodes sont implémentées pour les vecteurs de type double et float.
- L1TrendFilterLambdaMax calcule le paramètre de régularisation maximal ;
- L1TrendFilter calcule la tendance L1 pour une valeur donnée du paramètre de régularisation λ, qui peut également être spécifié en unités de λmax.
2.1. L1TrendFilterLambdaMax
Méthode de calcul du paramètre de régularisation maximal λmax pour un vecteur de données.
Calcul pour vector<double> :
bool vector::L1TrendFilterLambdaMax( double &lambda_max // the maximum value of the regularization parameter lambda )Calcul pour vector<float> :
bool vectorf::L1TrendFilterLambdaMax( float &lambda_max // the maximum value of the regularization parameter lambda );
Paramètres
lambda
[out] Valeur maximale du paramètre de régularisation λmax, ou -1 en cas d’erreur.
Valeur de Retour
Renvoie true en cas de succès.
Note
La consommation de mémoire augmente linéairement avec la taille du vecteur.
2.2. L1TrendFilter
Méthode de calcul de la tendance L1 pour un vecteur de données.
Calcul pour vector<double> :
bool vector::L1TrendFilter( double lambda, // regularization parameter bool relative, // flag indicating lambda is in λmax units vector& result // output vector with L1 filtering result );
Calcul pour vector<float> :
bool vectorf::L1TrendFilter( float lambda, // regularization parameter bool relative, // flag indicating lambda is in λmax units vectorf& result // output vector with L1 filtering result );
Paramètres
lambda
[in] Valeur du paramètre de régularisation lambda (si relative = true, lambda est défini dans l’intervalle [0, 1] comme une fraction de λmax).
relative
[in] Booléen indiquant la façon dont λ est spécifié. Si true, λ est exprimé en unités de λmax ; sinon, la valeur absolue est utilisée.
result
[out] Vecteur contenant le résultat du filtrage L1.
Valeur de Retour
Renvoie true en cas de succès.
Note
La consommation de mémoire augmente linéairement avec la taille du vecteur.
Plages recommandées pour λ (mode relatif).
| Multiplicateur de λ | Résultat |
|---|---|
| 0,005 – 0,015 | quasi L2, bruité |
| 0,02 – 0,04 | micro-segments |
| 0,04 – 0,07 | optimal pour les signaux |
| 0,07 – 0,12 | tendances à moyen terme |
| 0,12 – 0,25 | régimes de marché |
| > 0,3 | peu de segments |
Tableau 2. Plages de travail de λ en unités de λmax
Pour les applications pratiques, il est recommandé d’utiliser des multiplicateurs compris entre 0,04 et 0,25.
3. Exemples d’application
Dans cette section, nous examinons le calcul de la tendance L1 sur des données simulées de mouvement brownien et sur les cours du S&P 500, ainsi que les propriétés d’échelle de λmax pour le mouvement brownien et pour les données du marché Forex.
Nous présentons également trois variantes d’indicateurs qui aident à déterminer les paramètres de régularisation optimaux (multiplicateurs de λmax) permettant d’obtenir la meilleure décomposition en tendance L1 pour des symboles et des périodes donnés.
En outre, les résultats du filtrage des signaux de trading (alignement sur la tendance L1) sont présentés pour les stratégies MovingAverage, MACD, ADX et EMA.
3.1. Calcul de la tendance L1 sur des données simulées (marche aléatoire)
À titre d’exemple, considérons le calcul de la tendance L1 avec différentes valeurs du paramètre de régularisation λ sur des données simulées de mouvement brownien.
Code du script :
//+------------------------------------------------------------------+ //| TestL1Trend.mq5 | //| Copyright 2000-2026, MetaQuotes Ltd. | //| http://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2000-2026, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #property script_show_inputs #include <Graphics\Graphic.mqh> //+------------------------------------------------------------------+ //| Generate Brown movement data | //+------------------------------------------------------------------+ void BMData(vector<double> &data,int &data_count) { data.Resize(data_count); data[0] = 0.0; for(int i=1; i<data_count; i++) data[i] = data[i-1] + (MathRand()/32767.0 - 0.5); } //+------------------------------------------------------------------+ //| CopyValues | //+------------------------------------------------------------------+ bool CopyValues(vector<double> &data_v,double &data[]) { int data_count=(int)data.Size(); if(data_count==0) return(false); ArrayResize(data,data.Size()); for(int i=0; i<data_count; i++) data[i]=data_v[i]; return(true); } //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { MathSrand(1); int data_count=1000; vector<double> data_test; BMData(data_test,data_count); //--- prepare arrays for chart double x[],y[]; ArrayResize(x,data_count); ArrayResize(y,data_count); for(int i=0; i<data_count; i++) x[i]=i; //--- CGraphic graphic; long chart=0; string name="test"; if(ObjectFind(chart,name)<0) graphic.Create(chart,name,0,0,0,1000,600); else graphic.Attach(chart,name); graphic.BackgroundMain("L1 Trend filtering (random walk) with different lambda"); graphic.BackgroundMainSize(16); graphic.HistoryNameWidth(60); graphic.HistoryColor(ColorToARGB(clrGray,255)); graphic.XAxis().AutoScale(false); graphic.XAxis().Min(0); graphic.XAxis().Max(data_count); //--- CopyValues(data_test,y); graphic.CurveAdd(x,y,CURVE_LINES,"Data").LinesWidth(1); //--- L1TrendFilterLambdaMax double lambda_max=0.0; if(data_test.L1TrendFilterLambdaMax(lambda_max)) PrintFormat("lambda_max=%f",lambda_max); //--- vector<double> data_l1; const double lambda_factors[]= {1.0,0.9,0.8,0.5,0.25,0.1,0.01,0.05,0.001,0.0005}; for(int i=0; i<ArraySize(lambda_factors); i++) { double lambda=lambda_max*lambda_factors[i]; PrintFormat("%d. lambda=%f",i+1,lambda); bool res=data_test.L1TrendFilter(lambda_factors[i],true,data_l1); if(res) { CopyValues(data_l1,y); graphic.CurveAdd(x,y,CURVE_LINES,"lambda="+DoubleToString(lambda,0)).LinesWidth(3); } } //--- graphic.CurvePlotAll(); graphic.Update(); DebugBreak(); } //+------------------------------------------------------------------+Sortie :
TestL1Trend (EURUSD,H1) lambda_max=51703.353749 TestL1Trend (EURUSD,H1) 1. lambda=51703.353749 TestL1Trend (EURUSD,H1) 2. lambda=46533.018374 TestL1Trend (EURUSD,H1) 3. lambda=41362.682999 TestL1Trend (EURUSD,H1) 4. lambda=25851.676874 TestL1Trend (EURUSD,H1) 5. lambda=12925.838437 TestL1Trend (EURUSD,H1) 6. lambda=5170.335375 TestL1Trend (EURUSD,H1) 7. lambda=517.033537 TestL1Trend (EURUSD,H1) 8. lambda=2585.167687 TestL1Trend (EURUSD,H1) 9. lambda=51.703354 TestL1Trend (EURUSD,H1) 10. lambda=25.851677
Dans cet exemple, on constate que la diminution du paramètre de régularisation λ permet une décomposition plus détaillée en segments de tendance (Fig. 1).
Si λ ≥ λmax, la solution devient une ligne droite correspondant à la régression linéaire (la tendance globale).

Fig. 1. Exemple de calcul du filtre L1 avec différentes valeurs de λ sur des données de mouvement brownien
Les fonctions de calcul de la tendance L1 sont disponibles pour les vecteurs double et float.
Le script de test permettant de comparer les calculs est présenté ci-dessous.
//+------------------------------------------------------------------+ //| TestL1TrendFloatDouble.mq5 | //| Copyright 2000-2026, MetaQuotes Ltd. | //| http://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2000-2026, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #include <Graphics\Graphic.mqh> uint32_t ExtSeed=1; //+------------------------------------------------------------------+ //| Generate Brown movement data | //+------------------------------------------------------------------+ template<typename T> void BMData(vector<T> &data,uint64_t data_count) { MathSrand(ExtSeed); data.Resize(data_count); data[0] = 0.0; for(uint64_t i=1; i<data_count; i++) data[i] = data[i-1] + T(MathRand()/32767.0 - 0.5); } //+------------------------------------------------------------------+ //| CopyValues | //+------------------------------------------------------------------+ template<typename T> bool CopyValues(double &data[],const vector<T> &data_v) { if(ArrayResize(data,data.Size())!=data.Size()) return(false); for(uint64_t i=0; i<data.Size(); i++) data[i]=data_v[i]; return(true); } //+------------------------------------------------------------------+ //| L1TrendCalculate | //+------------------------------------------------------------------+ template<typename T> bool L1TrendCalculate(double &result[],uint64_t data_count,double lambda,bool lambda_is_relative) { vector<T> data_test; BMData(data_test,data_count); vector<T> vres; if(!data_test.L1TrendFilter((T)lambda,lambda_is_relative,vres)) return(false); if(ArrayResize(result,(uint32_t)vres.Size())!=vres.Size()) return(false); for(uint64_t n=0; n<result.Size(); n++) result[n]=vres[n]; return(true); } //+------------------------------------------------------------------+ //| TestRun | //+------------------------------------------------------------------+ bool TestRun(uint32_t data_count,uint32_t mode) { //--- create graph CGraphic graphic; long chart=0; string name="L1TrendTest"; if(ObjectFind(chart,name)<0) graphic.Create(chart,name,0,0,0,1280,600); else graphic.Attach(chart,name); string mode_name="("; if((mode&1)==1) mode_name+="DOUBLE"; if((mode&3)==3) mode_name+=" & "; if((mode&2)==2) mode_name+="FLOAT"; mode_name+=")"; graphic.BackgroundMain("L1Trend filtering (random walk) with different lambda "+mode_name); graphic.BackgroundMainSize(16); graphic.HistoryNameWidth(60); graphic.HistoryColor(ColorToARGB(clrGray,255)); graphic.XAxis().AutoScale(false); graphic.XAxis().Min(0); graphic.XAxis().Max(data_count); //--- prepare arrays double x[]; double y[]; if(ArrayResize(x,data_count)!=data_count) return(false); for(uint32_t i=0; i<data_count; i++) x[i]=i; vector<double> v; BMData(v,data_count); v.Swap(y); graphic.CurveAdd(x,y,CURVE_LINES,"Data").LinesWidth(1); //--- calculate const double lambda_factors[]= {1.0,0.9,0.8,0.5,0.25,0.1,0.01,0.05,0.001,0.0005}; //--- double if((mode&1)==1) { for(uint64_t i=0; i<lambda_factors.Size(); i++) { if(L1TrendCalculate<double>(y,data_count,lambda_factors[i],true)) graphic.CurveAdd(x,y,CURVE_LINES,"DBL="+DoubleToString(lambda_factors[i],4)).LinesWidth(4); } } //--- float if((mode&2)==2) { for(uint64_t i=0; i<lambda_factors.Size(); i++) { if(L1TrendCalculate<float>(y,data_count,(float)lambda_factors[i],true)) graphic.CurveAdd(x,y,CURVE_LINES,"FLT="+DoubleToString(lambda_factors[i],4)).LinesWidth(2); } } //--- update graphic.CurvePlotAll(); graphic.Update(); return(true); } //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { for(uint32_t n=0; !IsStopped(); n++,Sleep(1000)) { TestRun(1000,1+n%3); if((n%3)==2) ExtSeed++; } } //+------------------------------------------------------------------+
Sortie :

3.2. Calcul de la tendance L1 pour la série des cours du S&P 500
Prenons le calcul de log(S&P 500) tiré de l’article original l_1 Trend Filtering, S.J. Kim, K. Koh, S. Boyd et D. Gorinevsky, SIAM Review, section Problems and Techniques, 51(2):339–360, mai 2009.
Le script utilise les données du fichier « snp500.txt ». Ce fichier doit être placé dans le dossier : terminal_data_folder\MQL5\Files
//+------------------------------------------------------------------+ //| TestL1TrendFilterSP500.mq5 | //| Copyright 2000-2026, MetaQuotes Ltd. | //| http://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2000-2026, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #property script_show_inputs #include <Graphics\Graphic.mqh> //+------------------------------------------------------------------+ //| LoadData | //+------------------------------------------------------------------+ void LoadData(string filename,vector<double> &data,int &data_count) { data_count=0; ResetLastError(); int file_handle=FileOpen(filename,FILE_READ|FILE_TXT|FILE_ANSI); if(file_handle!=INVALID_HANDLE) { while(!FileIsEnding(file_handle)) { string str=FileReadString(file_handle); if(data.Size()<=(ulong)data_count) data.Resize(data_count+1); data[data_count]=StringToDouble(str); data_count++; } FileClose(file_handle); } else PrintFormat("Failed to open %s file, Error code = %d",filename,GetLastError()); //--- data.Resize(data_count); } //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { long chart=0; string name="log SP500"; int data_count=0; vector<double> data_sp500; LoadData("snp500.txt",data_sp500,data_count); vector<double> data_l1_sp500; data_l1_sp500.Resize(data_count); //--- L1TrendFilterLambdaMax double lambda_max=0.0; if(data_sp500.L1TrendFilterLambdaMax(lambda_max)) PrintFormat("Lambda_max=%f",lambda_max); double lambda=50; //--- L1TrendFilter if(data_sp500.L1TrendFilter(lambda,false,data_l1_sp500)) { //--- prepare arrays for chart double x[],y[],y2[]; ArrayResize(x,data_count); ArrayResize(y,data_count); ArrayResize(y2,data_count); for(int i=0; i<data_count; i++) { x[i]=i; y[i]=data_sp500[i]; y2[i]=data_l1_sp500[i]; } //--- CGraphic graphic; if(ObjectFind(chart,name)<0) graphic.Create(chart,name,0,0,0,1000,600); else graphic.Attach(chart,name); graphic.BackgroundMain("log SP500 L1 trend filtering"); graphic.BackgroundMainSize(16); graphic.HistoryNameWidth(60); graphic.HistoryColor(ColorToARGB(clrGray,255)); graphic.XAxis().AutoScale(false); graphic.XAxis().Min(0); graphic.XAxis().Max(data_count); graphic.XAxis().DefaultStep(100); graphic.CurveAdd(x,y,CURVE_LINES,"SP500").LinesWidth(1); graphic.CurveAdd(x,y2,CURVE_LINES,"L1 trend").LinesWidth(3); graphic.CurvePlotAll(); graphic.Update(); DebugBreak(); } } //+------------------------------------------------------------------+
Le résultat de l’exécution du script est présenté sur la Fig. 2.

Fig. 2. Exemple d’estimation de la tendance L1 pour la série des logarithmes des cours de l’indice S&P 500
Dans l’onglet Experts, la valeur de λmax pour la série temporelle donnée sera affichée :
TestL1TrendFilterSP500 (EURUSD,H1) Lambda_max=37394.835512
Ce script illustre l’utilisation des méthodes L1TrendFilterLambdaMax et L1TrendFilter avec une valeur fixe λ = 50, comme dans l’article original des auteurs de la méthode.
Dans les exemples suivants, au lieu de valeurs absolues du paramètre de régularisation λ, nous utiliserons des valeurs relatives (en unités de λmax) avec le flag relative = true.
3.3. Propriétés d’échelle de λmax
Le paramètre λmax joue un rôle clé dans le filtrage L1, car il définit la borne supérieure de la régularisation à partir de laquelle la solution dégénère en une approximation linéaire globale. Une propriété intéressante de cette grandeur est sa loi d’échelle en fonction de la longueur de la série temporelle.
Des expériences numériques montrent que λmax croît selon une loi de puissance par rapport au nombre d’observations :
![]()
avec : T — longueur de la série temporelle, α — exposant d’échelle.
Pour une marche aléatoire (mouvement brownien), on peut montrer que l’exposant doit être proche de α ≈ 2,5. L’amplitude du mouvement brownien croît comme
Par conséquent, la combinaison de ces lois d’échelle conduit à la relation :
![]()
ce qui correspond à un exposant α ≈ 2,5.
Ainsi, à mesure que la longueur de la série temporelle augmente, la valeur de λmax croît beaucoup plus rapidement que de façon linéaire.
3.3.1. Expérience numérique pour le mouvement brownien
Pour vérifier la loi d’échelle, une expérience numérique a été menée.
Pour différentes longueurs T de la série temporelle, des réalisations de mouvement brownien ont été générées, puis la valeur moyenne de λmax a été calculée.
Une approximation logarithmique a été utilisée :
![]()
ce qui permet d’estimer l’exposant α par régression linéaire.
Le code de l’expérience est présenté ci-dessous.
//+------------------------------------------------------------------+ //| TestScalingLambdaMaxBrownMovement.mq5 | //| Copyright 2000-2026, MetaQuotes Ltd. | //| http://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2000-2026, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #include <Graphics\Graphic.mqh> //+------------------------------------------------------------------+ //| Generate Brownian motion | //+------------------------------------------------------------------+ void GenerateBrownian(int N,vector<double> &data) { data.Resize(N); data[0] = 0.0; for(int i=1; i<N; i++) data[i] = data[i-1] + (MathRand()/32767.0 - 0.5); } //+------------------------------------------------------------------+ //| LinearRegression | //+------------------------------------------------------------------+ void LinearRegression(const double &x[], const double &y[], int n, double &a, double &b) { double sx = 0.0, sy = 0.0, sxx = 0.0, sxy = 0.0; for(int i = 0; i < n; i++) { sx += x[i]; sy += y[i]; sxx += x[i] * x[i]; sxy += x[i] * y[i]; } double denom = n * sxx - sx * sx; a = (n * sxy - sx * sy) / denom; b = (sy - a * sx) / n; } //+------------------------------------------------------------------+ //| TestScaling with statistics | //+------------------------------------------------------------------+ void TestScalingStatistics() { MathSrand(42); int RUNS = 10; // int MC = 10; // Monte Carlo double alpha_values[]; ArrayResize(alpha_values, RUNS); // --- geometric grid of T int nT = 8; int Tvals[]; ArrayResize(Tvals, nT); int T0 = 64; for(int i = 0; i < nT; i++) Tvals[i] = T0 << i; Print("Scaling test with statistics"); //--- double logT[]; double logLambda[]; vector<double> bm; vector<double> l1_trend; for(int run = 0; run < RUNS; run++) { ArrayResize(logT, nT); ArrayResize(logLambda, nT); //--- for(int i = 0; i < nT; i++) { int T = Tvals[i]; double lambda_sum = 0.0; l1_trend.Resize(T); for(int k = 0; k < MC; k++) { GenerateBrownian(T, bm); double lambda_max=0.0; if (bm.L1TrendFilterLambdaMax(lambda_max)) lambda_sum += lambda_max; bm.L1TrendFilter(0.2,true,l1_trend); } double lambda_avg = lambda_sum / MC; logT[i] = MathLog((double)T); logLambda[i] = MathLog(lambda_avg); } // --- regression double alpha, c; LinearRegression(logT, logLambda, nT, alpha, c); alpha_values[run] = alpha; PrintFormat("run %d -> alpha = %.6f", run+1, alpha); } //--- statistics double mean = 0.0; for(int i=0;i<RUNS;i++) mean += alpha_values[i]; mean /= RUNS; // --- standard deviation double var = 0.0; for(int i=0;i<RUNS;i++) var += (alpha_values[i]-mean)*(alpha_values[i]-mean); var /= (RUNS - 1); double stddev = MathSqrt(var); // --- standard error of mean double sem = stddev / MathSqrt((double)RUNS); // --- theoretical comparison double alpha_theory=2.5; double percent_error=MathAbs(mean-alpha_theory)/alpha_theory*100.0; //--- results PrintFormat("mean alpha = %.6f", mean); PrintFormat("std deviation = %.6f", stddev); PrintFormat("standard error = %.6f", sem); PrintFormat("theory = %.4f", alpha_theory); PrintFormat("percent error from theory = %.4f %%", percent_error); } //+------------------------------------------------------------------+ //| TestScaling | //+------------------------------------------------------------------+ void TestScaling() { MathSrand(1); // --- geometric grid of T int nT = 8; int Tvals[]; ArrayResize(Tvals,nT); //--- int T0 = 64; for(int i=0; i<nT; i++) Tvals[i]=T0<<i; // 64 * 2^i //--- double logT[], logLambda[]; ArrayResize(logT,nT); ArrayResize(logLambda,nT); //--- Print("scaling test for lambda_max"); for(int i=0; i<nT; i++) { int T = Tvals[i]; //--- Monte-Carlo simulations int MC=1000; double lambda_sum = 0.0; for(int k=0; k<MC; k++) { vector<double> bm; GenerateBrownian(T, bm); double lambda_max=0.0; if(bm.L1TrendFilterLambdaMax(lambda_max)) lambda_sum += lambda_max; } double lambda_avg=lambda_sum/MC; logT[i]= MathLog((double)T); logLambda[i]=MathLog(lambda_avg); PrintFormat("T=%5d <lambda_max>=%.6f",T,lambda_avg); } // --- linear regression in log-log double alpha, c; LinearRegression(logT,logLambda,nT,alpha,c); //--- PrintFormat("estimated scaling exponent alpha = %.4f",alpha); double alpha_theory=2.5; PrintFormat("theoretical value = %.4f",alpha_theory); //--- plot scaling law CGraphic g; g.Create(0, "ScalingLaw",0,0,0,1000,600); g.BackgroundMain("Scaling law of lambda_max (Brownian motion)"); g.BackgroundMainSize(16); g.CurveAdd(logT, logLambda, CURVE_POINTS, "Simulation"); //--- double xfit[2], yfit[2]; xfit[0] = logT[0]; xfit[1] = logT[nT-1]; //--- yfit[0] = alpha*xfit[0] + c; yfit[1] = alpha*xfit[1] + c; //---least squares fit g.CurveAdd(xfit, yfit, CURVE_LINES, "LS fit"); g.CurvePlotAll(); g.Update(); DebugBreak(); } //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- calculate scaling with statistics TestScalingStatistics(); //--- show sample results TestScaling(); } //+------------------------------------------------------------------+
Sortie :
TestScalingLambdaMaxBrownMovement (EURUSD,H1) Scaling test with statistics TestScalingLambdaMaxBrownMovement (EURUSD,H1) run 1 -> alpha = 2.480774 TestScalingLambdaMaxBrownMovement (EURUSD,H1) run 2 -> alpha = 2.530977 TestScalingLambdaMaxBrownMovement (EURUSD,H1) run 3 -> alpha = 2.435511 TestScalingLambdaMaxBrownMovement (EURUSD,H1) run 4 -> alpha = 2.461984 TestScalingLambdaMaxBrownMovement (EURUSD,H1) run 5 -> alpha = 2.467093 TestScalingLambdaMaxBrownMovement (EURUSD,H1) run 6 -> alpha = 2.487965 TestScalingLambdaMaxBrownMovement (EURUSD,H1) run 7 -> alpha = 2.532371 TestScalingLambdaMaxBrownMovement (EURUSD,H1) run 8 -> alpha = 2.455831 TestScalingLambdaMaxBrownMovement (EURUSD,H1) run 9 -> alpha = 2.483485 TestScalingLambdaMaxBrownMovement (EURUSD,H1) run 10 -> alpha = 2.420283 TestScalingLambdaMaxBrownMovement (EURUSD,H1) mean alpha = 2.475627 TestScalingLambdaMaxBrownMovement (EURUSD,H1) std deviation = 0.036281 TestScalingLambdaMaxBrownMovement (EURUSD,H1) standard error = 0.011473 TestScalingLambdaMaxBrownMovement (EURUSD,H1) theory = 2.5000 TestScalingLambdaMaxBrownMovement (EURUSD,H1) percent error from theory = 0.9749 % TestScalingLambdaMaxBrownMovement (EURUSD,H1) scaling test for lambda_max TestScalingLambdaMaxBrownMovement (EURUSD,H1) T= 64 <lambda_max>=97.302362 TestScalingLambdaMaxBrownMovement (EURUSD,H1) T= 128 <lambda_max>=566.626861 TestScalingLambdaMaxBrownMovement (EURUSD,H1) T= 256 <lambda_max>=3162.076116 TestScalingLambdaMaxBrownMovement (EURUSD,H1) T= 512 <lambda_max>=18271.204936 TestScalingLambdaMaxBrownMovement (EURUSD,H1) T= 1024 <lambda_max>=100057.796790 TestScalingLambdaMaxBrownMovement (EURUSD,H1) T= 2048 <lambda_max>=578620.887399 TestScalingLambdaMaxBrownMovement (EURUSD,H1) T= 4096 <lambda_max>=3192555.936035 TestScalingLambdaMaxBrownMovement (EURUSD,H1) T= 8192 <lambda_max>=17895314.647170 TestScalingLambdaMaxBrownMovement (EURUSD,H1) estimated scaling exponent alpha = 2.4967 TestScalingLambdaMaxBrownMovement (EURUSD,H1) theoretical value = 2.5000
Le graphique en double échelle logarithmique montre une dépendance en loi de puissance de λmax vis-à-vis du nombre de points de données pour le mouvement brownien.
.

Fig. 3. Dépendance en loi de puissance de LambdaMax pour le mouvement brownien
Les résultats de la simulation montrent :
mean alpha = 2.4756 std deviation = 0.036 theory = 2.5 percent error ≈ 1%
L’expérience confirme donc la relation théorique :
![]()
Le graphique en double échelle logarithmique montre une relation linéaire entre log(λmax) et log(T).
3.3.2. Loi d’échelle pour les séries temporelles financières
Une expérience similaire a été menée sur des séries de cours du marché Forex. L’exposant α a été estimé pour différentes paires de devises et périodes.
Les résultats montrent que, pour des données financières réelles, la valeur de α se situe également dans l’intervalle α ≈ 2,45–2,60, ce qui est très proche de la valeur théorique du mouvement brownien. Cela signifie que le comportement d’échelle de λmax est quasi universel et se vérifie sur différents marchés et différentes périodes.
Le script TestScalingLambdaMaxSymbol.mq5 calcule l’exposant de λmax pour un symbole donné sur les périodes standard M1–H1.
//+------------------------------------------------------------------+ //| TestScalingLambdaMaxSymbol.mq5 | //| Copyright 2000-2026, MetaQuotes Ltd. | //| http://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2000-2026, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #property script_show_inputs //--- input parameters input string WorkSymbol = "EURUSD"; // Symbol input int YearStart = 2024; input int YearEnd = 2025; #include <Graphics\Graphic.mqh> //+------------------------------------------------------------------+ //| GetHistoricalData | //+------------------------------------------------------------------+ bool GetHistoricalData(double &data[], string symbol, ENUM_TIMEFRAMES tf, int year_start, int year_end) { datetime from = StringToTime(IntegerToString(year_start) + ".01.01 00:00"); datetime to = StringToTime(IntegerToString(year_end) + ".12.31 23:59"); int copied = CopyClose(symbol, tf, from, to, data); if(copied <= 0) { Print("Error in CopyClose: ", GetLastError()); ArrayResize(data, 0); return false; } //PrintFormat("Loaded bars: %d (%s %s)", ArraySize(data), symbol, EnumToString(tf)); return true; } //+------------------------------------------------------------------+ //| LinearRegression | //+------------------------------------------------------------------+ void LinearRegression(const double &x[], const double &y[], int n, double &a, double &b) { double sx = 0, sy = 0, sxx = 0, sxy = 0; for(int i = 0; i < n; i++) { sx += x[i]; sy += y[i]; sxx += x[i] * x[i]; sxy += x[i] * y[i]; } double denom = n*sxx - sx*sx; if(denom!=0) { a = (n*sxy-sx*sy)/denom; b = (sy-a*sx)/n; } } //+------------------------------------------------------------------+ //| Scaling test for one timeframe | //+------------------------------------------------------------------+ bool TestScalingLambaMaxTF(string symbol, ENUM_TIMEFRAMES tf, double &logT_out[], double &logLambda_out[], double &alpha_out) { MathSrand(42); double prices[]; if(!GetHistoricalData(prices, symbol, tf, YearStart, YearEnd)) return false; int Tvals[]; int nT=8; int T0=64; ArrayResize(Tvals, nT); for(int i = 0; i < nT; i++) Tvals[i] = T0 << i; ArrayResize(logT_out, nT); ArrayResize(logLambda_out, nT); int data_size = ArraySize(prices); vector<double> data_prices; for(int i = 0; i < nT; i++) { int T = Tvals[i]; int MC = 1000; double lambda_sum = 0.0; for(int k = 0; k < MC; k++) { if(data_size < T) break; int start = MathRand() % (data_size - T); data_prices.Resize(T); for(int j=0; j<T; j++) data_prices[j]=prices[start+j]; double lambda_max=0.0; if(data_prices.L1TrendFilterLambdaMax(lambda_max)) lambda_sum += lambda_max; } double lambda_avg = lambda_sum / MC; logT_out[i]=MathLog((double)T); logLambda_out[i]=MathLog(lambda_avg); //PrintFormat("TF=%s T=%5d <lambda_max>=%.6f", EnumToString(tf), T, lambda_avg); } double c; LinearRegression(logT_out, logLambda_out, nT, alpha_out, c); PrintFormat("%s (%s) estimated scaling exponent = %.4f", symbol,EnumToString(tf), alpha_out); return true; } //+------------------------------------------------------------------+ //| TestScalingLambdaMaxSymbol | //+------------------------------------------------------------------+ void TestScalingLambdaMaxSymbol(string symbol) { ENUM_TIMEFRAMES timeframes[] = {PERIOD_M1, PERIOD_M2, PERIOD_M3, PERIOD_M4, PERIOD_M5, PERIOD_M6, PERIOD_M10, PERIOD_M12, PERIOD_M15, PERIOD_M20, PERIOD_M30, PERIOD_H1 }; uint colors[] = {clrRed,clrBlue,clrGreen,clrOrange,clrPurple,clrDarkGreen,clrCyan, clrNavy,clrOrangeRed,clrDodgerBlue,clrCrimson,clrDarkRed }; //--- CGraphic g; g.Create(0,"ScalingLawTest",0,0,0,1000,600); g.BackgroundMain("Scaling law of lambda_max ("+symbol+")"); g.BackgroundMainSize(16); PrintFormat("%s scaling test for standard timeframes",symbol); for(int i = 0; i < ArraySize(timeframes); i++) { double logT[], logLambda[], alpha; // Print("processing timeframe: ", EnumToString(timeframes[i]), " -----"); if(TestScalingLambaMaxTF(symbol,timeframes[i],logT,logLambda,alpha)) { g.CurveAdd(logT,logLambda,ColorToARGB(colors[i % ArraySize(colors)],255),CURVE_POINTS_AND_LINES,EnumToString(timeframes[i])); } } g.CurvePlotAll(); g.Update(); //--- DebugBreak(); } //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- estimate lambda_max scale exponent for price data TestScalingLambdaMaxSymbol(WorkSymbol); } //+------------------------------------------------------------------+
Résultats pour EURUSD :
TestScalingLambdMaxSymbol (EURUSD,H1) EURUSD scaling test for standard timeframes TestScalingLambdMaxSymbol (EURUSD,H1) EURUSD (PERIOD_M1) estimated scaling exponent = 2.5038 TestScalingLambdMaxSymbol (EURUSD,H1) EURUSD (PERIOD_M2) estimated scaling exponent = 2.5350 TestScalingLambdMaxSymbol (EURUSD,H1) EURUSD (PERIOD_M3) estimated scaling exponent = 2.5034 TestScalingLambdMaxSymbol (EURUSD,H1) EURUSD (PERIOD_M4) estimated scaling exponent = 2.5422 TestScalingLambdMaxSymbol (EURUSD,H1) EURUSD (PERIOD_M5) estimated scaling exponent = 2.5341 TestScalingLambdMaxSymbol (EURUSD,H1) EURUSD (PERIOD_M6) estimated scaling exponent = 2.5132 TestScalingLambdMaxSymbol (EURUSD,H1) EURUSD (PERIOD_M10) estimated scaling exponent = 2.5188 TestScalingLambdMaxSymbol (EURUSD,H1) EURUSD (PERIOD_M12) estimated scaling exponent = 2.5126 TestScalingLambdMaxSymbol (EURUSD,H1) EURUSD (PERIOD_M15) estimated scaling exponent = 2.5208 TestScalingLambdMaxSymbol (EURUSD,H1) EURUSD (PERIOD_M20) estimated scaling exponent = 2.4887 TestScalingLambdMaxSymbol (EURUSD,H1) EURUSD (PERIOD_M30) estimated scaling exponent = 2.5695 TestScalingLambdMaxSymbol (EURUSD,H1) EURUSD (PERIOD_H1) estimated scaling exponent = 2.6118
Les résultats pour EURUSD (périodes standard M1–H1) sont présentés sur la Fig. 4.

Fig. 4. Dépendance en loi de puissance de λmax pour les différentes périodes de EURUSD
De même, d’autres paires de devises peuvent être analysées.
Pour USDJPY :
TestScalingLambdMaxSymbol (EURUSD,H1) USDJPY scaling test for standard timeframes TestScalingLambdMaxSymbol (EURUSD,H1) USDJPY (PERIOD_M1) estimated scaling exponent = 2.5851 TestScalingLambdMaxSymbol (EURUSD,H1) USDJPY (PERIOD_M2) estimated scaling exponent = 2.5825 TestScalingLambdMaxSymbol (EURUSD,H1) USDJPY (PERIOD_M3) estimated scaling exponent = 2.4889 TestScalingLambdMaxSymbol (EURUSD,H1) USDJPY (PERIOD_M4) estimated scaling exponent = 2.5099 TestScalingLambdMaxSymbol (EURUSD,H1) USDJPY (PERIOD_M5) estimated scaling exponent = 2.5059 TestScalingLambdMaxSymbol (EURUSD,H1) USDJPY (PERIOD_M6) estimated scaling exponent = 2.4939 TestScalingLambdMaxSymbol (EURUSD,H1) USDJPY (PERIOD_M10) estimated scaling exponent = 2.5548 TestScalingLambdMaxSymbol (EURUSD,H1) USDJPY (PERIOD_M12) estimated scaling exponent = 2.5641 TestScalingLambdMaxSymbol (EURUSD,H1) USDJPY (PERIOD_M15) estimated scaling exponent = 2.5525 TestScalingLambdMaxSymbol (EURUSD,H1) USDJPY (PERIOD_M20) estimated scaling exponent = 2.5390 TestScalingLambdMaxSymbol (EURUSD,H1) USDJPY (PERIOD_M30) estimated scaling exponent = 2.5805 TestScalingLambdMaxSymbol (EURUSD,H1) USDJPY (PERIOD_H1) estimated scaling exponent = 2.4645
Les résultats pour USDJPY sont également bien approximés par une relation en loi de puissance.

Fig. 5. Dépendance en loi de puissance de λmax pour les différentes périodes de USDJPY
Pour GBPUSD :
TestScalingLambdMaxSymbol (EURUSD,H1) GBPUSD scaling test for standard timeframes TestScalingLambdMaxSymbol (EURUSD,H1) GBPUSD (PERIOD_M1) estimated scaling exponent = 2.5235 TestScalingLambdMaxSymbol (EURUSD,H1) GBPUSD (PERIOD_M2) estimated scaling exponent = 2.5449 TestScalingLambdMaxSymbol (EURUSD,H1) GBPUSD (PERIOD_M3) estimated scaling exponent = 2.5439 TestScalingLambdMaxSymbol (EURUSD,H1) GBPUSD (PERIOD_M4) estimated scaling exponent = 2.5427 TestScalingLambdMaxSymbol (EURUSD,H1) GBPUSD (PERIOD_M5) estimated scaling exponent = 2.5248 TestScalingLambdMaxSymbol (EURUSD,H1) GBPUSD (PERIOD_M6) estimated scaling exponent = 2.5308 TestScalingLambdMaxSymbol (EURUSD,H1) GBPUSD (PERIOD_M10) estimated scaling exponent = 2.5293 TestScalingLambdMaxSymbol (EURUSD,H1) GBPUSD (PERIOD_M12) estimated scaling exponent = 2.5235 TestScalingLambdMaxSymbol (EURUSD,H1) GBPUSD (PERIOD_M15) estimated scaling exponent = 2.5069 TestScalingLambdMaxSymbol (EURUSD,H1) GBPUSD (PERIOD_M20) estimated scaling exponent = 2.4977 TestScalingLambdMaxSymbol (EURUSD,H1) GBPUSD (PERIOD_M30) estimated scaling exponent = 2.5659 TestScalingLambdMaxSymbol (EURUSD,H1) GBPUSD (PERIOD_H1) estimated scaling exponent = 2.5524
Une situation similaire est observée pour la série des cours de GBPUSD (Fig. 6).

Fig. 6. Dépendance en loi de puissance de λmax pour les différentes périodes de GBPUSD
Pour les séries EURUSD, USDJPY et GBPUSD considérées, les valeurs estimées de l’exposant sont également proches de 2,5.
Les relations linéaires en échelle log-log de la fonction λmax sur plusieurs périodes et paires de devises indiquent une dépendance en loi de puissance de λmax vis-à-vis du nombre d’observations.
3.3.3. Conséquences pratiques de la loi d’échelle
L’existence d’une dépendance en loi de puissance pour λmax a une conséquence pratique importante.
Puisque λmax ∝ T^2,5, la valeur absolue de λ dépend fortement :
- de la longueur de la fenêtre de données,
- de la période,
- de l’échelle de la série temporelle.
Par conséquent, l’utilisation d’une valeur absolue de λ n’est pas pratique.
Une approche beaucoup plus robuste consiste à utiliser un paramètre relatif λ=c⋅λmax, où 0<c<1.
Une telle approche :
- rend le paramètre de régularisation invariant par changement d’échelle,
- simplifie le transfert des paramètres entre différents instruments,
- permet d’utiliser les mêmes réglages sur différentes périodes.
C’est pourquoi, dans tous les exemples suivants, le paramètre λ sera spécifié en unités de λmax.
3.4. Indicateurs de tendance L1
Dans cette section, trois types d’indicateurs sont examinés :
- calcul de la tendance L1 à partir des cours de clôture ;
- calcul des coefficients de croissance linéaire (pente) de la tendance L1 ;
- calcul du signe de la pente de la tendance L1 ;
3.4.1. L1TrendFilter.mq5 - indicateur de tendance L1
Dans cet exemple, le filtre L1 est calculé à partir des cours de clôture sur un nombre de barres donné (ici, BarsToShow = 1000), avec le coefficient lambda spécifié en unités de λmax.
Le calcul utilise l’appel de méthode L1TrendFilter(relative = true), dans lequel le paramètre λ est défini en unités de λmax. Les valeurs de l’indicateur sont affichées directement dans la fenêtre du graphique.
Le code de l’indicateur L1TrendFilter.mq5 est présenté ci-dessous.
//+------------------------------------------------------------------+ //| L1TrendFilter.mq5 | //| Copyright 2000-2026, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2000-2026, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #property indicator_chart_window #property indicator_buffers 1 #property indicator_plots 1 //--- #property indicator_label1 "L1TrendFilter" #property indicator_type1 DRAW_LINE #property indicator_color1 clrDodgerBlue #property indicator_width1 2 //--- input int BarsToShow = 1000; // Number of bars to calculate L1 input double CoefLambda = 0.015; // Lambda in lambda_max units //--- double Trend[]; //+------------------------------------------------------------------+ //| Indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { SetIndexBuffer(0,Trend,INDICATOR_DATA); ArrayInitialize(Trend,EMPTY_VALUE); //--- PlotIndexSetDouble(0,PLOT_EMPTY_VALUE,EMPTY_VALUE); IndicatorSetInteger(INDICATOR_DIGITS,_Digits); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { //--- check bars static bool warned=false; if(rates_total < BarsToShow) { if(!warned) { Print("Waiting for enough bars: ",BarsToShow); warned=true; } ArrayInitialize(Trend,EMPTY_VALUE); return(0); } //--- check new bar static datetime last_bar_time=0; bool new_bar=(time[0]!=last_bar_time); bool need_recalc=(prev_calculated==0) || new_bar || (rates_total!=prev_calculated); if(!need_recalc) return(prev_calculated); last_bar_time=time[0]; //--- range int start=rates_total-BarsToShow; //--- hide old bars for(int i=0; i<start; i++) Trend[i]=EMPTY_VALUE; //--- int data_count=BarsToShow; //--- copy Close vector<double> DataClose; DataClose.Resize(data_count); for(int i=0; i<data_count; i++) DataClose[i]=close[start+i]; //--- lambda max double lambda_max=0.0; bool res=DataClose.L1TrendFilterLambdaMax(lambda_max); if(res) { PrintFormat("lambda_max=%f (%s,%s) Coef=%f lambda=%f", lambda_max,Symbol(),EnumToString(Period()),CoefLambda,lambda_max*CoefLambda); } //--- L1 trend filtering vector<double> filtered_data; filtered_data.Resize(data_count); if(DataClose.L1TrendFilter(CoefLambda,true,filtered_data)) { for(int i=0; i<data_count; i++) Trend[start+i]=filtered_data[i]; } //--- return(rates_total); } //+------------------------------------------------------------------+
La Fig. 7 présente un exemple de calcul de l’indicateur L1TrendFilter.mq5 avec CoefLambda = 0.015.

Fig. 7. Exemple de calcul de l’indicateur L1TrendFilter.mq5 avec CoefLambda = 0.015
À titre de comparaison, on peut calculer plusieurs variantes avec différents paramètres de régularisation.
La Fig. 8 montre les calculs avec les paramètres CoefLambda = 0.015, CoefLambda = 0.025 et CoefLambda = 0.055.

Fig. 8. Exemples de calcul de l’indicateur L1TrendFilter.mq5 avec différentes valeurs de CoefLambda
3.4.2. L1TrendFilterSlope.mq5 - indicateur de la dynamique de la tendance L1
Pour afficher la pente de la tendance, on peut utiliser l’incrément des valeurs de l’indicateur L1TrendFilter.
Prenons par exemple l’indicateur L1TrendFilterSlope, qui affiche ses valeurs dans une fenêtre séparée.
Le code de l’indicateur est présenté ci-dessous.
//+------------------------------------------------------------------+ //| L1TrendFilterSlope.mq5 | //| Copyright 2000-2026, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2000-2026, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #property indicator_separate_window #property indicator_buffers 1 #property indicator_plots 1 //--- #property indicator_label1 "L1TrendFilterSlope" #property indicator_type1 DRAW_LINE #property indicator_color1 clrDodgerBlue #property indicator_width1 2 //--- input int BarsToShow = 1000; // Number of bars to calculate L1 input double CoefLambda = 0.015; // Lambda in lambda_max units //--- double Trend[]; //+------------------------------------------------------------------+ //| Indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { SetIndexBuffer(0,Trend,INDICATOR_DATA); ArrayInitialize(Trend,EMPTY_VALUE); //--- PlotIndexSetDouble(0,PLOT_EMPTY_VALUE,EMPTY_VALUE); IndicatorSetInteger(INDICATOR_DIGITS,_Digits); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { //--- check bars static bool warned=false; if(rates_total < BarsToShow) { if(!warned) { Print("Waiting for enough bars: ",BarsToShow); warned=true; } ArrayInitialize(Trend,EMPTY_VALUE); return(0); } //--- check new bar static datetime last_bar_time=0; bool new_bar=(time[0]!=last_bar_time); bool need_recalc= (prev_calculated==0) || new_bar || (rates_total!=prev_calculated); if(!need_recalc) return(prev_calculated); last_bar_time=time[0]; //--- int start=rates_total-BarsToShow; int data_count=BarsToShow; //--- hide old bars for(int i=0;i<start;i++) Trend[i]=EMPTY_VALUE; //--- copy Close vector<double> DataClose; DataClose.Resize(data_count); for(int i=0;i<data_count;i++) DataClose[i]=close[start+i]; //--- lambda max double lambda_max=0.0; if(DataClose.L1TrendFilterLambdaMax(lambda_max)) { PrintFormat("lambda_max=%f (%s,%s) Coef=%f lambda=%f", lambda_max,Symbol(),EnumToString(Period()),CoefLambda,lambda_max*CoefLambda); } //--- L1 filtering vector<double> filtered_data; filtered_data.Resize(data_count); bool res=DataClose.L1TrendFilter(CoefLambda,true,filtered_data); if(res) { //--- slope (first difference) for(int i=1; i<data_count; i++) { double delta=filtered_data[i]-filtered_data[i-1]; Trend[start+i]=delta; } //--- copy first element Trend[start]=Trend[start+1]; } return(rates_total); } //+------------------------------------------------------------------+
Le résultat de l’utilisation conjointe des indicateurs L1TrendFilter.mq5 et L1TrendFilterSlope.mq5 est présenté sur la Fig. 9.

Fig. 9. Exemple de calcul des indicateurs L1TrendFilter.mq5 et L1TrendFilterSlope.mq5 avec CoefLambda = 0.015
3.4.3. L1TrendFilterSlopeSign.mq5 - indicateur de la direction de la tendance L1
De même, on peut calculer un indicateur qui affiche le signe de l’incrément de l’indicateur L1TrendFilterSlope.mq5.
Code de l’indicateur L1TrendFilterSlopeSign.mq5 :
//+------------------------------------------------------------------+ //| L1TrendFilterSlopeSign.mq5 | //| Copyright 2000-2026, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2000-2026, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #property indicator_separate_window #property indicator_buffers 1 #property indicator_plots 1 //--- #property indicator_label1 "L1TrendFilterSlope" #property indicator_type1 DRAW_LINE #property indicator_color1 clrDodgerBlue #property indicator_width1 2 //--- input int BarsToShow = 1000; // Number of bars to calculate L1 input double CoefLambda = 0.015; // Lambda in lambda_max units //--- double Trend[]; //+------------------------------------------------------------------+ //| Signum | //+------------------------------------------------------------------+ double Signum(const double value) { return((value>0)-(value<0)); } //+------------------------------------------------------------------+ //| Indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { SetIndexBuffer(0,Trend,INDICATOR_DATA); ArrayInitialize(Trend,EMPTY_VALUE); //--- PlotIndexSetDouble(0,PLOT_EMPTY_VALUE,EMPTY_VALUE); IndicatorSetInteger(INDICATOR_DIGITS,_Digits); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { //--- check bars static bool warned=false; if(rates_total < BarsToShow) { if(!warned) { Print("Waiting for enough bars: ",BarsToShow); warned=true; } ArrayInitialize(Trend,EMPTY_VALUE); return(0); } //--- check new bar static datetime last_bar_time=0; bool new_bar=(time[0]!=last_bar_time); bool need_recalc=(prev_calculated==0) || new_bar || (rates_total!=prev_calculated); if(!need_recalc) return(prev_calculated); last_bar_time=time[0]; //--- int start=rates_total-BarsToShow; int data_count=BarsToShow; //--- hide old bars for(int i=0; i<start; i++) Trend[i]=EMPTY_VALUE; //--- copy Close vector<double> DataClose; DataClose.Resize(data_count); for(int i=0; i<data_count; i++) DataClose[i]=close[start+i]; //--- lambda max double lambda_max=0.0; bool res=DataClose.L1TrendFilterLambdaMax(lambda_max); if(res) { PrintFormat("lambda_max=%f (%s,%s) Coef=%f lambda=%f", lambda_max,Symbol(),EnumToString(Period()),CoefLambda,lambda_max*CoefLambda); } //--- L1 filtering vector<double> filtered_data; filtered_data.Resize(data_count); res=DataClose.L1TrendFilter(CoefLambda,true,filtered_data); if(res) { Trend[start]=0; for(int i=1; i<data_count; i++) { double delta=filtered_data[i]-filtered_data[i-1]; Trend[start+i]=Signum(delta); } } return(rates_total); } //+------------------------------------------------------------------+
Un exemple d’utilisation conjointe des trois indicateurs est présenté sur la Fig. 10 (avec la même valeur de coefficient CoefLambda = 0.015).

Fig. 10. Exemple de calcul des indicateurs L1TrendFilter.mq5, L1TrendFilterSlope.mq5 et L1TrendFilterSlopeSign.mq5 avec CoefLambda = 0.015
3.4.4. Indicateurs de volatilité basés sur la tendance L1
Cette section présente des indicateurs conçus pour évaluer la volatilité d’un instrument financier à partir de la tendance L1.
Ces outils permettent d’identifier les périodes d’instabilité et de stabilité du marché, d’analyser la dynamique actuelle du marché et de prendre des décisions de trading plus éclairées.
Les indicateurs présentés dans cette section sont les suivants :
- L1Volatility.mq5 — volatilité résiduelle par rapport à la tendance L1 ;
- L1VolatilitySmoothed.mq5 — volatilité résiduelle lissée ;
- L1VolatilityAbsolute.mq5 — volatilité absolue ;
- L1VolatilityNormalized.mq5 — volatilité normalisée ;
- L1VolatilityNormalizedSmoothed.mq5 — volatilité normalisée lissée ;
- L1VolatilityRegime.mq5 — détection du régime de marché basée sur la volatilité.
Tous les indicateurs reposent sur un cadre commun fondé sur la tendance L1, ce qui garantit la cohérence de l’analyse et simplifie l’interprétation des résultats obtenus.
Ces indicateurs permettent d’identifier visuellement les périodes de forte et de faible volatilité, ainsi que de déterminer le régime de marché actuel : consolidation, tendance, expansion ou panique.
Un trader peut ainsi adapter ses stratégies de trading aux conditions de marché du moment, par exemple en appliquant des approches plus prudentes pendant les périodes de faible volatilité ou des stratégies plus actives lors de forts mouvements du marché.
3.4.4.1. L1Volatility.mq5 — indicateur de volatilité L1
L’indicateur calcule la volatilité résiduelle comme la différence entre les cours de clôture et la valeur correspondante de la tendance L1.
Cette approche permet d’identifier les périodes d’instabilité du marché et les moments précis d’entrée ou de sortie.
Visuellement, l’indicateur est affiché dans une fenêtre séparée du graphique, sous la forme d’une ligne orange.
L’indicateur permet de :
- évaluer les écarts des cours par rapport à la tendance L1 et mesurer la force du mouvement du marché ;
- détecter les pics de volatilité locaux pour une gestion des risques plus précise ;
- comparer la dynamique de différents instruments sur la même période.
L’indicateur est particulièrement utile dans les systèmes où il est nécessaire de surveiller les variations de volatilité à court terme sans perdre de vue le contexte de la tendance générale.
Le code de l’indicateur L1Volatility.mq5 est présenté ci-dessous.
//+------------------------------------------------------------------+ //| L1Volatility.mq5 | //| Copyright 2000-2026, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2000-2026, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #property indicator_separate_window #property indicator_buffers 1 #property indicator_plots 1 //--- #property indicator_label1 "L1Volatility" #property indicator_type1 DRAW_LINE #property indicator_color1 clrOrangeRed #property indicator_width1 2 //--- input int BarsToShow = 1000; // Number of bars to calculate L1 input double CoefLambda = 0.015; // Lambda in lambda_max units //--- double Volatility[]; //--- //+------------------------------------------------------------------+ //| Indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { SetIndexBuffer(0,Volatility,INDICATOR_DATA); ArrayInitialize(Volatility,EMPTY_VALUE); //--- PlotIndexSetDouble(0,PLOT_EMPTY_VALUE,EMPTY_VALUE); IndicatorSetInteger(INDICATOR_DIGITS,_Digits); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { //--- check bars static bool warned=false; if(rates_total<BarsToShow) { if(!warned) { Print("Waiting for enough bars: ",BarsToShow); warned=true; } ArrayInitialize(Volatility,EMPTY_VALUE); return(0); } //--- check new bar static datetime last_bar_time=0; bool new_bar=(time[0]!=last_bar_time); bool need_recalc=(prev_calculated==0) || new_bar || (rates_total!=prev_calculated); if(!need_recalc) return(prev_calculated); last_bar_time=time[0]; //--- int start=rates_total-BarsToShow; int data_count=BarsToShow; //--- hide old bars for(int i=0;i<start;i++) Volatility[i]=EMPTY_VALUE; //--- copy Close vector<double> DataClose; DataClose.Resize(data_count); for(int i=0; i<data_count; i++) DataClose[i]=close[start+i]; //--- lambda max double lambda_max=0.0; bool res=DataClose.L1TrendFilterLambdaMax(lambda_max); if(res) { PrintFormat("lambda_max=%f (%s,%s) Coef=%f lambda=%f", lambda_max,Symbol(),EnumToString(Period()),CoefLambda,lambda_max*CoefLambda); } //--- L1 filter vector<double> filtered_data; filtered_data.Resize(data_count); res=DataClose.L1TrendFilter(CoefLambda,true,filtered_data); if(res) { for(int i=0; i<data_count; i++) { double residual=close[start+i]-filtered_data[i]; Volatility[start+i]=residual; } } //--- return(rates_total); } //+------------------------------------------------------------------+
Le résultat du calcul est présenté sur la Fig. 11.

Fig. 11. Indicateur L1Volatility.mq5
3.4.4.2. L1VolatilitySmoothed.mq5 — indicateur de volatilité résiduelle lissée
Cet indicateur est une version lissée de L1Volatility, à laquelle une moyenne mobile simple (Simple Moving Average, SMA) est appliquée.
Le lissage permet :
- de réduire le bruit à court terme et les valeurs aberrantes ;
- d’obtenir une visualisation plus claire et plus facile à interpréter ;
- de se concentrer sur les changements persistants de la volatilité.
L’indicateur est utile pour les stratégies qui nécessitent d’évaluer les tendances de la volatilité sur une plus longue période, par exemple dans les systèmes de trading adaptatifs ou pour filtrer les faux signaux pendant les phases de tendance et de consolidation du marché.
Le code de l’indicateur L1VolatilitySmoothed.mq5 est présenté ci-dessous.
//+------------------------------------------------------------------+ //| L1VolatilitySmoothed.mq5 | //| Copyright 2000-2026, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2000-2026, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #property indicator_separate_window #property indicator_buffers 1 #property indicator_plots 1 #property indicator_label1 "L1VolatilitySmoothed" #property indicator_type1 DRAW_LINE #property indicator_color1 clrMediumVioletRed #property indicator_width1 2 //--- input int BarsToShow = 1000; // Number of bars to calculate L1 input double CoefLambda = 0.015; // Lambda in lambda_max units input int SmoothPeriod = 10; // Smooth period //--- double VolSmoothed[]; //+------------------------------------------------------------------+ //| Indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { SetIndexBuffer(0, VolSmoothed, INDICATOR_DATA); ArrayInitialize(VolSmoothed, EMPTY_VALUE); PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, EMPTY_VALUE); IndicatorSetInteger(INDICATOR_DIGITS, _Digits); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { if(rates_total<BarsToShow) { ArrayInitialize(VolSmoothed,EMPTY_VALUE); return(0); } //--- recalc only on new bar static datetime last_bar_time = 0; if(time[0] == last_bar_time && prev_calculated > 0) return(prev_calculated); last_bar_time=time[0]; //--- int start=rates_total-BarsToShow; for(int i=0; i<start; i++) VolSmoothed[i]=EMPTY_VALUE; //--- copy close prices vector<double> price(BarsToShow); for(int i=0; i<BarsToShow; i++) price[i] = close[start+i]; vector<double> l1(BarsToShow); bool res=price.L1TrendFilter(CoefLambda,true,l1); if(res) { //--- calculate raw volatility vector<double> rawVol(BarsToShow); for(int i=0; i<BarsToShow; i++) rawVol[i]=close[start+i]-l1[i]; //--- apply simple moving average smoothing for(int i=0; i<BarsToShow; i++) { double sum = 0.0; int count = 0; for(int j=MathMax(0,i-SmoothPeriod+1); j<=i; j++) { sum+=rawVol[j]; count++; } VolSmoothed[start+i]=sum/count; } } //--- return(rates_total); } //+------------------------------------------------------------------+
La Fig. 12 montre les indicateurs L1Volatility.mq5 et L1VolatilitySmoothed.mq5.

Fig. 12. Indicateurs L1Volatility.mq5 et L1VolatilitySmoothed.mq5
3.4.4.3. L1VolatilityAbsolute.mq5 — indicateur de volatilité absolue
L’indicateur calcule la valeur absolue de la différence entre les cours de clôture et la tendance L1.
Caractéristiques et applications :
- ignore la direction du mouvement et n’évalue que l’amplitude des fluctuations ;
- pratique pour analyser l’amplitude des oscillations des cours indépendamment de la direction de la tendance ;
- utile pour les systèmes basés sur les statistiques des valeurs extrêmes et l’analyse des risques.
La volatilité absolue reflète l’ampleur réelle des écarts de cours, ce qui permet au trader d’observer la force du mouvement du marché sans se laisser distraire par sa direction.
Le code de l’indicateur L1VolatilityAbsolute.mq5 est présenté ci-dessous.
//+------------------------------------------------------------------+ //| L1VolatilityAbsolute.mq5 | //| Copyright 2000-2026, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2000-2026, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #property indicator_separate_window #property indicator_buffers 1 #property indicator_plots 1 //--- #property indicator_label1 "L1VolatilityAbsolute" #property indicator_type1 DRAW_LINE #property indicator_color1 clrOrange #property indicator_width1 2 //--- input int BarsToShow = 1000; // Number of bars to calculate L1 input double CoefLambda = 0.015; // Lambda in lambda_max units //--- double Vol[]; //+------------------------------------------------------------------+ //| Indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { SetIndexBuffer(0,Vol,INDICATOR_DATA); ArrayInitialize(Vol,EMPTY_VALUE); PlotIndexSetDouble(0,PLOT_EMPTY_VALUE,EMPTY_VALUE); IndicatorSetInteger(INDICATOR_DIGITS,_Digits); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { //--- static bool warned=false; if(rates_total < BarsToShow) { if(!warned) { Print("Waiting bars ",BarsToShow); warned=true; } ArrayInitialize(Vol,EMPTY_VALUE); return(0); } static datetime last_bar=0; bool new_bar=(time[0]!=last_bar); //--- if(!(prev_calculated==0 || new_bar || rates_total!=prev_calculated)) return(prev_calculated); //--- last_bar=time[0]; int start=rates_total-BarsToShow; int N=BarsToShow; for(int i=0; i<start; i++) Vol[i]=EMPTY_VALUE; //--- vector<double> price; price.Resize(N); for(int i=0; i<N; i++) price[i]=close[start+i]; vector<double> l1; l1.Resize(N); bool res=price.L1TrendFilter(CoefLambda,true,l1); if(res) { for(int i=0; i<N; i++) Vol[start+i]=MathAbs(close[start+i]-l1[i]); } //--- return(rates_total); } //+------------------------------------------------------------------+
Un exemple de calcul de l’indicateur est présenté sur la Fig. 13.

Fig. 13. Indicateur L1VolatilityAbsolute.mq5
3.4.4.4. L1VolatilityNormalized.mq5 — indicateur de volatilité normalisée
L’indicateur normalise la volatilité à l’aide de l’ATR (Average True Range) combiné à la tendance L1.
Il calcule le rapport entre l’écart absolu du cours par rapport à la tendance et l’amplitude moyenne des cours sur la période de l’ATR. La normalisation supprime la dépendance à l’échelle des cours, ce qui permet de comparer différents instruments et différentes périodes.
Applications :
- identification des mouvements de marché relativement forts et faibles ;
- comparaison de la volatilité entre différents actifs ;
- évaluation des conditions de marché indépendamment du niveau des cours.
Le code de l’indicateur L1VolatilityNormalized.mq5 est présenté ci-dessous.
//+------------------------------------------------------------------+ //| L1VolatilityNormalized.mq5 | //| Copyright 2000-2026, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2000-2026, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #property indicator_separate_window #property indicator_buffers 1 #property indicator_plots 1 #property indicator_label1 "L1VolatilityNormalized" #property indicator_type1 DRAW_LINE #property indicator_color1 clrDodgerBlue #property indicator_width1 2 //--- input int BarsToShow = 1000; // Number of bars to calculate L1 input double CoefLambda = 0.015; // Lambda in lambda_max units //--- double VolNormalized[]; //--- //+------------------------------------------------------------------+ //| Indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- prepare SetIndexBuffer(0, VolNormalized,INDICATOR_DATA); ArrayInitialize(VolNormalized,EMPTY_VALUE); PlotIndexSetDouble(0,PLOT_EMPTY_VALUE,EMPTY_VALUE); IndicatorSetInteger(INDICATOR_DIGITS,_Digits); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { //--- check bars static bool warned=false; if(rates_total<BarsToShow) { if(!warned) { Print("Waiting for enough bars: ",BarsToShow); warned=true; } ArrayInitialize(VolNormalized,EMPTY_VALUE); return(0); } //--- check new bar static datetime last_bar_time=0; bool new_bar=(time[0]!=last_bar_time); bool need_recalc=(prev_calculated==0) || new_bar || (rates_total!=prev_calculated); if(!need_recalc) return(prev_calculated); last_bar_time=time[0]; int start=rates_total-BarsToShow; //--- for(int i=0; i<start; i++) VolNormalized[i]=EMPTY_VALUE; //--- copy close prices vector<double> price(BarsToShow); for(int i=0; i<BarsToShow; i++) price[i]=close[start+i]; //--- vector<double> l1(BarsToShow); bool res=price.L1TrendFilter(CoefLambda,true,l1); if(res) { //--- compute normalized volatility double mean=0.0; double stddev=0.0; for(int i=0; i<BarsToShow; i++) mean+=close[start+i]-l1[i]; mean/=BarsToShow; //--- for(int i=0; i<BarsToShow; i++) stddev+=MathPow(close[start+i]-l1[i]-mean,2); stddev=MathSqrt(stddev/BarsToShow); //--- for(int i=0; i<BarsToShow; i++) VolNormalized[start+i]=stddev>0?(close[start+i]-l1[i])/stddev:0; } //--- return(rates_total); } //+------------------------------------------------------------------+
Le résultat du calcul est présenté sur la Fig. 14.

Fig. 14. Indicateur L1VolatilityNormalized.mq5
3.4.4.5. L1VolatilityNormalizedSmoothed.mq5 — indicateur de volatilité normalisée lissée
Cet indicateur prolonge l’approche par normalisation en y ajoutant un lissage par moyenne mobile exponentielle (Exponential Moving Average, EMA).
Avantages :
- réduit l’influence du bruit à court terme et des pics brusques ;
- produit un profil de volatilité plus clair et plus facile à interpréter ;
- aide à évaluer la volatilité persistante et le régime de marché actuel.
L’indicateur est particulièrement utile pour les stratégies adaptatives qui nécessitent une estimation stable de la volatilité, par exemple pour la sélection automatique des modes de trading.
Le code de l’indicateur L1VolatilityNormalizedSmoothed.mq5 est présenté ci-dessous.
//+------------------------------------------------------------------+ //| L1VolatilityNormalizedSmoothed.mq5 | //| Copyright 2000-2026, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2000-2026, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #property indicator_separate_window #property indicator_buffers 1 #property indicator_plots 1 #property indicator_label1 "L1VolatilityNormalizedSmoothed" #property indicator_type1 DRAW_LINE #property indicator_color1 clrDeepSkyBlue #property indicator_width1 2 //--- input int BarsToShow = 1000; // Number of bars to calculate L1 input double CoefLambda = 0.015; // Lambda in lambda_max units input int SmoothPeriod = 10; // EMA smoothing period (1=no smoothing) //--- double NormVolSmooth[]; //+------------------------------------------------------------------+ //| Indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- prepare SetIndexBuffer(0,NormVolSmooth,INDICATOR_DATA); ArrayInitialize(NormVolSmooth,EMPTY_VALUE); PlotIndexSetDouble(0,PLOT_EMPTY_VALUE,EMPTY_VALUE); IndicatorSetInteger(INDICATOR_DIGITS,_Digits); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { //--- check bars static bool warned=false; if(rates_total < BarsToShow) { if(!warned) { Print("Waiting for enough bars: ",BarsToShow); warned=true; } ArrayInitialize(NormVolSmooth,EMPTY_VALUE); return(0); } //--- check new bar static datetime last_bar_time=0; bool new_bar=(time[0]!=last_bar_time); bool need_recalc= (prev_calculated==0) || new_bar || (rates_total!=prev_calculated); if(!need_recalc) return(prev_calculated); last_bar_time=time[0]; int start=rates_total-BarsToShow; //--- for(int i=0; i<start; i++) NormVolSmooth[i]=EMPTY_VALUE; //--- copy close prices vector<double> price(BarsToShow); for(int i=0; i<BarsToShow; i++) price[i]=close[start+i]; //--- vector<double> l1(BarsToShow); bool res=price.L1TrendFilter(CoefLambda,true,l1); if(res) { //--- compute normalized volatility vector<double> VolNormalized(BarsToShow); double mean = 0, stddev = 0; for(int i=0; i<BarsToShow; i++) mean += close[start+i]-l1[i]; mean /= BarsToShow; //--- for(int i=0; i<BarsToShow; i++) stddev += MathPow(close[start+i]-l1[i]-mean,2); stddev = MathSqrt(stddev/BarsToShow); //--- for(int i=0; i<BarsToShow; i++) VolNormalized[i]=stddev>0 ? (close[start+i]-l1[i])/stddev: 0; //--- EMA smoothing vector<double> Smooth(BarsToShow); double alpha=(SmoothPeriod<=1) ? 1.0: 2.0/(SmoothPeriod+1.0); //--- Smooth[0] = VolNormalized[0]; for(int i=1; i<BarsToShow; i++) Smooth[i]=alpha*VolNormalized[i]+(1.0-alpha)*Smooth[i-1]; //--- copy to indicator buffer for(int i=0; i<BarsToShow; i++) NormVolSmooth[start+i]=Smooth[i]; } //--- return(rates_total); } //+------------------------------------------------------------------+
Le résultat du calcul est présenté sur la Fig. 15.

Fig. 15. Indicateurs L1VolatilityNormalized.mq5 et L1VolatilityNormalizedSmoothed.mq5
3.4.4.6. L1VolatilityRegime.mq5 — indicateur de détection du régime de marché
L’indicateur classe le régime de marché actuel à partir de la volatilité normalisée et lissée, en distinguant quatre états du marché.
Caractéristiques de l’indicateur :
- entièrement autonome, il ne nécessite aucune donnée externe ;
- fournit une visualisation claire de la dynamique du marché pour les stratégies adaptatives ;
- les paramètres de seuil LowVolThresh et HighVolThresh peuvent être ajustés selon les instruments et les périodes.
| Valeur | Régime | Description |
|---|---|---|
| 0 | Consolidation | Faible volatilité, marché latéral |
| 1 | Tendance | Volatilité modérée, présence d’une tendance |
| 2 | Expansion | Mouvement fort, expansion du marché |
| 3 | Panique | Volatilité extrême, mouvements brusques |
Tableau 3. Régimes de l’indicateur L1VolatilityRegime.mq5
- déterminer rapidement le régime de marché actuel ;
- adapter les stratégies de trading aux conditions du moment ;
- réduire le risque lors des mouvements extrêmes et améliorer l’efficacité du trading.
Le code de l’indicateur L1VolatilityRegime.mq5 est présenté ci-dessous.
//+------------------------------------------------------------------+ //| L1VolatilityRegime.mq5 | //| Copyright 2000-2026, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2000-2026, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #property indicator_separate_window #property indicator_buffers 1 #property indicator_plots 1 //--- #property indicator_label1 "L1 Volatility Regime" #property indicator_type1 DRAW_LINE #property indicator_color1 clrRoyalBlue #property indicator_width1 2 //--- input parameters input int BarsToShow = 1000; // Number of bars to calculate L1 input double CoefLambda = 0.015; // Lambda in lambda_max units input int ATRPeriod = 14; // ATR period input int SmoothPeriod = 10; // Smooth period input double L1MoveThresh = 0.0; // Move volatility input double LowVolThresh = 0.5; // Low volatility input double HighVolThresh = 1.5; // High volatility input double PanicMult = 2.0; // Panic volatility //--- double Regime[]; //+------------------------------------------------------------------+ //| Indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { SetIndexBuffer(0, Regime, INDICATOR_DATA); PlotIndexSetDouble(0,PLOT_EMPTY_VALUE, EMPTY_VALUE); IndicatorSetInteger(INDICATOR_DIGITS, 0); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { //--- check bars if(rates_total<BarsToShow+ATRPeriod) { ArrayInitialize(Regime,EMPTY_VALUE); return(0); } //--- check new bar static datetime last_bar_time=0; bool new_bar=(time[0]!=last_bar_time); bool need_recalc=(prev_calculated==0) || new_bar || (rates_total!=prev_calculated); if(!need_recalc) return(prev_calculated); last_bar_time=time[0]; //--- int start=rates_total-BarsToShow; int count=BarsToShow; //--- for(int i=0; i<start; i++) Regime[i]=EMPTY_VALUE; //--- vector<double> DataClose(count),DataHigh(count),DataLow(count); for(int i=0; i<count; i++) { DataClose[i]=close[start+i]; DataHigh[i]=high[start+i]; DataLow[i]=low[start+i]; } //--- vector<double> L1(count); bool res=DataClose.L1TrendFilter(CoefLambda,true,L1); if(!res) return(prev_calculated); //--- vector<double> TR(count),ATR(count); for(int i=0; i<count; i++) { if(i==0) TR[i]=DataHigh[i]-DataLow[i]; else { double h_l=DataHigh[i]-DataLow[i]; double h_pc=MathAbs(DataHigh[i]-DataClose[i-1]); double l_pc=MathAbs(DataLow[i]-DataClose[i-1]); TR[i]=MathMax(h_l,MathMax(h_pc,l_pc)); } int from=MathMax(0,i-ATRPeriod+1); double sumTR=0.0; int n = i-from+1; for(int j=from; j<=i;j++) sumTR += TR[j]; ATR[i]=sumTR/n; } //--- vector<double> NormVol(count), SmoothVol(count); for(int i=0; i<count; i++) NormVol[i]=(ATR[i]>0) ? MathAbs(DataClose[i]-L1[i])/ATR[i] : 0; double alpha=2.0/(SmoothPeriod+1.0); SmoothVol[0]=NormVol[0]; for(int i=1; i<count; i++) SmoothVol[i]=alpha*NormVol[i]+(1.0-alpha)*SmoothVol[i-1]; //--- for(int i=0; i<count; i++) { double vol=SmoothVol[i]; double deltaL1=(i>0) ? (L1[i]-L1[i-1]): 0.0; if(vol<LowVolThresh) Regime[start+i]=0; // Range else if(vol>=LowVolThresh && vol<HighVolThresh) Regime[start+i]=(MathAbs(deltaL1)>L1MoveThresh) ? 1:0; // Trend/Range else if(vol>=HighVolThresh && vol<HighVolThresh*PanicMult) Regime[start+i]=2; // Expansion else Regime[start+i]=3; // Panic } //--- return(rates_total); } //+------------------------------------------------------------------+
Le résultat du calcul est présenté sur la Fig. 16.

Fig. 16. Indicateur L1VolatilityRegime.mq5
Pour plus de commodité, on peut également utiliser une version qui affiche les régimes avec un code couleur.
Le code de l’indicateur L1VolatilityRegimeColor.mq5 est présenté ci-dessous.
//+------------------------------------------------------------------+ //| L1VolatilityRegimeColor.mq5 | //| Copyright 2000-2026, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2000-2026, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #property indicator_separate_window #property indicator_buffers 4 #property indicator_plots 4 //--- #property indicator_label1 "Range" #property indicator_type1 DRAW_LINE #property indicator_color1 clrDodgerBlue #property indicator_width1 2 //--- #property indicator_label2 "Trend" #property indicator_type2 DRAW_LINE #property indicator_color2 clrLime #property indicator_width2 2 //--- #property indicator_label3 "Expansion" #property indicator_type3 DRAW_LINE #property indicator_color3 clrOrange #property indicator_width3 2 //--- #property indicator_label4 "Panic" #property indicator_type4 DRAW_LINE #property indicator_color4 clrRed #property indicator_width4 2 //--- input parameters input int BarsToShow = 1000; // Number of bars to calculate L1 input double CoefLambda = 0.015; // Lambda in lambda_max units input int ATRPeriod = 14; // ATR period input int SmoothPeriod = 10; // Smooth period input double L1MoveThresh = 0.0; // Move volatility input double LowVolThresh = 0.5; // Low volatility input double HighVolThresh = 1.5; // High volatility input double PanicMult = 2.0; // Panic volatility //--- buffers double Regime[]; double BufRange[], BufTrend[], BufExpansion[], BufPanic[]; //+------------------------------------------------------------------+ //| Indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { SetIndexBuffer(0,BufRange,INDICATOR_DATA); SetIndexBuffer(1,BufTrend,INDICATOR_DATA); SetIndexBuffer(2,BufExpansion,INDICATOR_DATA); SetIndexBuffer(3,BufPanic,INDICATOR_DATA); //--- PlotIndexSetDouble(0,PLOT_EMPTY_VALUE,EMPTY_VALUE); PlotIndexSetDouble(1,PLOT_EMPTY_VALUE,EMPTY_VALUE); PlotIndexSetDouble(2,PLOT_EMPTY_VALUE,EMPTY_VALUE); PlotIndexSetDouble(3,PLOT_EMPTY_VALUE,EMPTY_VALUE); //--- IndicatorSetInteger(INDICATOR_DIGITS,0); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { if(rates_total<BarsToShow+ATRPeriod) { ArrayInitialize(Regime,EMPTY_VALUE); ArrayInitialize(BufRange,EMPTY_VALUE); ArrayInitialize(BufTrend,EMPTY_VALUE); ArrayInitialize(BufExpansion,EMPTY_VALUE); ArrayInitialize(BufPanic,EMPTY_VALUE); return(0); } //--- new bars static datetime last_bar_time=0; bool new_bar=(time[0]!=last_bar_time); bool need_recalc=(prev_calculated==0) || new_bar || (rates_total!=prev_calculated); if(!need_recalc) return(prev_calculated); last_bar_time=time[0]; //--- int start=rates_total-BarsToShow; int count=BarsToShow; //--- ArrayResize(Regime,rates_total); for(int i=0;i<start;i++) Regime[i]=EMPTY_VALUE; //--- vector<double> DataClose(count),DataHigh(count),DataLow(count); for(int i=0; i<count; i++) { DataClose[i]=close[start+i]; DataHigh[i]=high[start+i]; DataLow[i]=low[start+i]; } //--- vector<double> L1(count); bool res=DataClose.L1TrendFilter(CoefLambda,true,L1); if(!res) return(prev_calculated); //--- vector<double> TR(count),ATR(count); for(int i=0; i<count; i++) { if(i==0) TR[i]=DataHigh[i]-DataLow[i]; else { double h_l = DataHigh[i]-DataLow[i]; double h_pc = MathAbs(DataHigh[i]-DataClose[i-1]); double l_pc = MathAbs(DataLow[i]-DataClose[i-1]); TR[i] = MathMax(h_l, MathMax(h_pc, l_pc)); } int from=MathMax(0,i-ATRPeriod+1); double sumTR=0; int n=i-from+1; for(int j=from; j<=i; j++) sumTR+=TR[j]; ATR[i]=sumTR/n; } //--- vector<double> NormVol(count), SmoothVol(count); for(int i=0;i<count;i++) NormVol[i]=(ATR[i]>0) ? MathAbs(DataClose[i]-L1[i])/ATR[i] : 0; //--- double alpha=2.0/(SmoothPeriod+1.0); SmoothVol[0]=NormVol[0]; for(int i=1; i<count; i++) SmoothVol[i]=alpha*NormVol[i]+(1.0-alpha)*SmoothVol[i-1]; //--- calc Regime[] for(int i=0; i<count; i++) { double vol=SmoothVol[i]; double deltaL1=(i>0) ? (L1[i]-L1[i-1]):0.0; if(vol<LowVolThresh) Regime[start+i]=0; else if(vol<HighVolThresh) Regime[start+i]=(MathAbs(deltaL1)>L1MoveThresh) ? 1:0; else if(vol<HighVolThresh*PanicMult) Regime[start+i]=2; else Regime[start+i]=3; } //--- buffers for(int i=0; i<rates_total; i++) { BufRange[i] = (Regime[i]==0) ? Regime[i]: EMPTY_VALUE; BufTrend[i] = (Regime[i]==1) ? Regime[i]: EMPTY_VALUE; BufExpansion[i] = (Regime[i]==2) ? Regime[i]: EMPTY_VALUE; BufPanic[i] = (Regime[i]==3) ? Regime[i]: EMPTY_VALUE; } //--- return(rates_total); } //+------------------------------------------------------------------+
Le résultat du calcul combiné est présenté sur la Fig. 17.

Fig. 17. Indicateurs L1VolatilityRegime.mq5 et L1VolatilityRegimeColor.mq5
Les figures 18 à 20 présentent des exemples d’utilisation conjointe de tous les indicateurs de volatilité pour EURGBP, AUDCAD et CHFJPY.

Fig. 18. Indicateurs de volatilité pour EURGBP

Fig. 19. Indicateurs de volatilité pour AUDCAD

Fig. 20. Indicateurs de volatilité pour CHFJPY
3.5. Utilisation de la tendance L1 dans les stratégies de trading
Dans cette section, nous examinons les stratégies de trading MovingAverage, MACD, ADX et EMA avec différentes options d’application des filtres de signaux de trading.
L’ajout de filtres aux signaux de trading permet d’améliorer les caractéristiques des systèmes de trading. Pour analyser l’efficacité des filtres dans les Expert Advisors présentés, nous enregistrerons les données de solde et de fonds (à chaque nouvelle barre) dans des fichiers séparés, puis nous utiliserons un script Python pour visualiser les résultats des systèmes de trading dans les différents modes.
Tous les Expert Advisors présentés ont la même architecture.
Principe général d’alignement de la tendance L1 et des signaux de trading
- Le filtre d’ouverture (L1FilterOpen = true) ne permet d’ouvrir des transactions que dans le sens de la tendance dominante.
- Le filtre de clôture (L1FilterClose = true) aide à conserver les positions pendant les fortes tendances et réduit les sorties prématurées lors des corrections locales.
Paramètres d’entrée (communs à tous les Expert Advisors) :
//--- L1 filter parameters input int L1TotalBars = 1000; // Total bars for L1 filter input bool L1FilterOpen = false; // Use filter for Open input bool L1FilterClose = false; // Use filter for Close input double L1CoefLambda = 0.2; // Lambda in lambda_max units //--- save statistics input bool SaveStatistics = false; // Save statistics to file
Enregistrement des données de solde et de fonds dans un fichier
Le paramètre d’entrée SaveStatistics permet d’enregistrer les valeurs actuelles de l’heure, du cours de clôture, du solde, des fonds, etc., dans un fichier, à chaque nouvelle barre, dans : terminal_data_folder\Tester\Agent-127.0.0.1-3000\MQL5\Files.
La fonction d’enregistrement des données est appelée dans OnTick() et dépend de la valeur du paramètre d’entrée bool SaveStatistics.
//+------------------------------------------------------------------+ //| Expert OnTick function | //+------------------------------------------------------------------+ void OnTick() { //--- trade only at new bar if(!IsNewBar()) return; //--- check trade conditions if(SelectPosition()) CheckForClose(); else CheckForOpen(); //--- save account statistics if(SaveStatistics) SaveAccountStatistics(); }
Le préfixe du nom du fichier enregistré dépend de la combinaison de L1FilterOpen et L1FilterClose.
Le nom du fichier dépend de la stratégie et du symbole, et il est construit dans la fonction d’initialisation de l’Expert Advisor :
//+------------------------------------------------------------------+ //| PrepareStrategyFileName | //+------------------------------------------------------------------+ string PrepareStrategyFileName(string strategy_name) { int v=0; if(L1FilterOpen) v=v | 1; //--- if(L1FilterClose) v=v | 2; //--- string filename=IntegerToString(v)+"_"+strategy_name+"_"+_Symbol+".txt"; return filename; } //+------------------------------------------------------------------+ //| Expert initialization | //+------------------------------------------------------------------+ int OnInit() { //--- prepare filename ExtStrategyFileName=PrepareStrategyFileName(ExtStrategyName); //--- delete old file if exists if(FileIsExist(ExtStrategyFileName)) FileDelete(ExtStrategyFileName); //--- return INIT_SUCCEEDED; }
Application des filtres de signaux de trading dans différents modes :
//+------------------------------------------------------------------+ //| Save account statistics to file | //+------------------------------------------------------------------+ void SaveAccountStatistics() { //--- check file name if(ExtStrategyFileName=="") return; //--- int file=FileOpen(ExtStrategyFileName,FILE_WRITE|FILE_READ|FILE_TXT|FILE_SHARE_WRITE|FILE_ANSI); if(file==INVALID_HANDLE) { Print("File open error: ",GetLastError()); return; } //--- append FileSeek(file,0,SEEK_END); //--- account data double balance = AccountInfoDouble(ACCOUNT_BALANCE); double equity = AccountInfoDouble(ACCOUNT_EQUITY); double margin = AccountInfoDouble(ACCOUNT_MARGIN); double free_margin = AccountInfoDouble(ACCOUNT_MARGIN_FREE); double margin_lvl = AccountInfoDouble(ACCOUNT_MARGIN_LEVEL); //--- volume double volume=0.0; if(PositionSelect(_Symbol)) volume=PositionGetDouble(POSITION_VOLUME); //--- time datetime t[1]; if(CopyTime(_Symbol,_Period,0,1,t)<=0) { FileClose(file); return; } double current_close[1]; if(CopyClose(_Symbol,_Period,0,1,current_close)<=0) { FileClose(file); return; } string line=StringFormat("%s;%.2f;%.2f;%.2f;%.2f;%.2f;%.2f;%f",TimeToString(t[0],TIME_DATE|TIME_SECONDS), balance,equity,margin,free_margin,margin_lvl,volume,current_close[0]); //--- FileWrite(file,line); //--- FileClose(file); }
Application des filtres de signaux de trading dans différents modes
L’exécution successive de l’Expert Advisor dans le Testeur de Stratégie avec les 4 combinaisons :
- L1FilterOpen = false, L1FilterClose = false (trading sans filtres) ;
- L1FilterOpen = true, L1FilterClose = false (filtrage des entrées) ;
- L1FilterOpen = false, L1FilterClose = true (filtrage des sorties) ;
- L1FilterOpen = true, L1FilterClose = true (filtrage des entrées et des sorties).
produit des fichiers tels que : 0_MA_EURUSD.txt, 1_MA_EURUSD.txt, 2_MA_EURUSD.txt, 3_MA_EURUSD.txt.
Ces fichiers contiennent les données de solde et de fonds et permettent de comparer l’efficacité du filtrage des signaux de trading par alignement sur la tendance L1.
Visualisation des données
Pour construire les graphiques combinés, copiez les 4 fichiers dans un dossier séparé (par exemple, « C:\data\ ») et exécutez le script Python.
import pandas as pd import matplotlib.pyplot as plt import os # --- folder for charts output_dir = "C:\\data\\charts\\" os.makedirs(output_dir, exist_ok=True) symbol = "EURUSD" name_strategy = "MA" file_strategy = name_strategy+"_"+symbol title_strategy = " ("+symbol+" "+name_strategy+" strategy+filters)" file_prefix = symbol+"_"+name_strategy+"_" # --- files files = [ "C:\\data\\0_"+file_strategy+".txt", "C:\\data\\1_"+file_strategy+".txt", "C:\\data\\2_"+file_strategy+".txt", "C:\\data\\3_"+file_strategy+".txt" ] # --- labels labels = [ "No filters", "Open L1 filter", "Close L1 filter", "Open+Close L1 filter" ] # --- load data def load_file(filename): df = pd.read_csv( filename, sep=";", header=None, names=[ "time", "balance", "equity", "margin", "free_margin", "margin_level", "volume", "close" ] ) df["time"] = pd.to_datetime(df["time"]) return df # --- close price chart plt.figure(figsize=(10,6), dpi=100) for file, label in zip(files, labels): df = load_file(file) plt.plot(df["time"], df["close"], color='gray') plt.title(symbol+" Close Price") plt.xlabel("Time") plt.ylabel("closing price") plt.legend() plt.grid() plt.tight_layout() plt.savefig(output_dir + file_prefix+"close_price.png", dpi=100) plt.show() # --- balance chart plt.figure(figsize=(10,6), dpi=100) for file, label in zip(files, labels): df = load_file(file) plt.plot(df["time"], df["balance"], label=label) plt.title("Balance" + title_strategy) plt.xlabel("Time") plt.ylabel("Balance") plt.legend() plt.grid() plt.tight_layout() plt.savefig(output_dir + file_prefix+"balance.png", dpi=100) plt.show() plt.close() # --- equity chart plt.figure(figsize=(10,6), dpi=100) for file, label in zip(files, labels): df = load_file(file) plt.plot(df["time"], df["equity"], label=label) plt.title("Equity" + title_strategy) plt.xlabel("Time") plt.ylabel("Equity") plt.legend() plt.grid() plt.tight_layout() plt.savefig(output_dir + file_prefix+"equity.png", dpi=100) plt.show() plt.close() #--- balance + equity chart plt.figure(figsize=(10,6), dpi=100) for i, (file, label) in enumerate(zip(files, labels)): df = load_file(file) # --- get matplotlib color color = plt.rcParams["axes.prop_cycle"].by_key()["color"][i % 10] #--- equity — solid line plt.plot( df["time"], df["equity"], color=color, linestyle="-", label=f"{label} equity" ) #--- balance — dashed line plt.plot( df["time"], df["balance"], color=color, linestyle="--", label=f"{label} balance" ) plt.title("Balance + Equity" + title_strategy) plt.xlabel("Time") plt.ylabel("Value") plt.legend() plt.grid() plt.tight_layout() plt.savefig(output_dir+file_prefix+"balance_equity.png", dpi=100) plt.show() plt.close()
Implémentation des filtres de signaux de trading
Les paramètres L1TotalBars, L1FilterOpen, L1FilterClose et L1CoefLambda définissent les réglages du calcul de la tendance L1 et son utilisation pour filtrer les signaux de trading.
Calcul de la tendance L1 et alignement sur les signaux de trading
Dans tous les Expert Advisors, un filtrage supplémentaire est appliqué à l’aide de l’analyse de la tendance L1 :
- une série de cours lissée est construite à partir des L1TotalBars dernières barres ;
- le coefficient de croissance de la tendance, delta, est calculé comme la différence entre les deux dernières valeurs filtrées ;
si delta > 0 — tendance haussière ; si delta < 0 — tendance baissière.
//+------------------------------------------------------------------+ //| CheckTrendL1 | //+------------------------------------------------------------------+ double CheckTrendL1() { int max_bars=L1TotalBars; MqlRates rates_data[]; ArrayResize(rates_data,max_bars); ArraySetAsSeries(rates_data,false); if(CopyRates(_Symbol,_Period,0,max_bars,rates_data) != max_bars) { Print("CopyRates failed for L1Trend"); return(0); } //--- prepare data (close prices vector) int data_count=max_bars; vector<double> data_close; data_close.Resize(data_count); for(int i=0; i<data_count; i++) data_close[i] = rates_data[i].close; //--- calculate L1 filter vector<double> data_filtered; data_filtered.Resize(data_count); double dp=0.0; bool res=data_close.L1TrendFilter(L1CoefLambda,true,data_filtered); if(res) dp = data_filtered[data_count-1]-data_filtered[data_count-2]; //--- return(dp); }
Le paramètre L1CoefLambda est spécifié en unités de λmax, ce qui rend le filtrage robuste vis-à-vis de la volatilité et du nombre de barres.
Filtre d’entrée en position
Si L1FilterOpen = true :
- les signaux BUY sont ignorés lorsque la tendance L1 est négative ;
- les signaux SELL sont ignorés lorsque la tendance L1 est positive.
Les transactions ne sont ouvertes que dans le sens de la tendance dominante.
//+------------------------------------------------------------------+ //| CheckForOpen | //+------------------------------------------------------------------+ void CheckForOpen() { ENUM_ORDER_TYPE signal; if(!GetTradeSignal(signal)) return; if(signal == WRONG_VALUE) return; //--- L1 filter if(L1FilterOpen) { double delta = CheckTrendL1(); if(signal == ORDER_TYPE_BUY && delta < 0) { signal = WRONG_VALUE; PrintFormat("Open BUY signal cancelled by L1 trend delta=%.5f", delta); } if(signal == ORDER_TYPE_SELL && delta > 0) { signal = WRONG_VALUE; PrintFormat("Open SELL signal cancelled by L1 trend delta=%.5f", delta); } } //--- if(signal == WRONG_VALUE) return; //--- if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) || Bars(_Symbol,_Period)<L1TotalBars) return; //--- double price = (signal==ORDER_TYPE_BUY) ? SymbolInfoDouble(_Symbol,SYMBOL_ASK): SymbolInfoDouble(_Symbol,SYMBOL_BID); //--- ExtTrade.PositionOpen(_Symbol, signal, TradeLot, price, 0, 0); }
Filtre de sortie de position
Si L1FilterClose = true :
- les positions BUY ne sont pas clôturées par des signaux inverses tant que la tendance L1 reste haussière ;
- les positions SELL ne sont pas clôturées tant que la tendance L1 reste baissière.
Cela permet de conserver les positions dans les fortes tendances et de réduire les sorties prématurées lors des corrections locales.
//+------------------------------------------------------------------+ //| CheckForClose | //+------------------------------------------------------------------+ void CheckForClose() { //--- check position if(!PositionSelect(_Symbol)) return; //--- check position magic if(PositionGetInteger(POSITION_MAGIC)!=MA_MAGIC) return; //--- check trade signal ENUM_ORDER_TYPE signal; if(!GetTradeSignal(signal)) return; //--- long type = PositionGetInteger(POSITION_TYPE); bool close_signal = false; //--- if(type == POSITION_TYPE_BUY && signal == ORDER_TYPE_SELL) close_signal = true; if(type == POSITION_TYPE_SELL && signal == ORDER_TYPE_BUY) close_signal = true; //--- check L1 filter if(L1FilterClose) { double delta = CheckTrendL1(); if(type == POSITION_TYPE_BUY && delta > 0) { close_signal = false; PrintFormat("Close BUY signal cancelled by L1 trend delta=%.5f", delta); } if(type == POSITION_TYPE_SELL && delta < 0) { close_signal = false; PrintFormat("Close SELL signal cancelled by L1 trend delta=%.5f", delta); } } //--- if(close_signal && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) && Bars(_Symbol,_Period)>=L1TotalBars) ExtTrade.PositionClose(_Symbol,3); }
3.5.1. Stratégie de trading MovingAverage
Comme premier exemple, prenons un Expert Advisor qui exécute des transactions selon la stratégie classique de suivi de tendance basée sur la Moyenne Mobile.
Les signaux de trading sont générés à partir du croisement du cours de clôture et de la moyenne mobile :
- BUY — lorsque le cours de clôture croise la moyenne mobile de bas en haut ;
- SELL — lorsque le cours de clôture croise la moyenne mobile de haut en bas.
Les signaux sont évalués sur les deux dernières barres clôturées, ce qui élimine l’influence de la barre en cours, non terminée, et réduit les faux signaux.
En outre, des filtres d’entrée et de sortie sont appliqués selon les réglages de la tendance L1 (L1TotalBars, L1FilterOpen, L1FilterClose, L1CoefLambda).
Code de l’Expert Advisor MovingAverageFilteredL1.mq5 :
//+------------------------------------------------------------------+ //| MovingAverageFilteredL1.mq5 | //| Copyright 2000-2026, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2000-2026, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" //--- best MovingAverage parameters for EURUSD,H1,2025 input int MovingPeriod = 61; // MA period input int MovingShift = 0; // MA shift //--- trade volume input double TradeLot = 0.1; // Lot size //--- L1 filter parameters input int L1TotalBars = 1000; // Total bars for L1 filter input bool L1FilterOpen = false; // Use filter for Open input bool L1FilterClose = false; // Use filter for Close input double L1CoefLambda = 0.2; // Lambda in lambda_max units //--- save statistics input bool SaveStatistics = false; // Save statistics to file //--- #define MA_MAGIC 1234501 #include <Trade\Trade.mqh> CTrade ExtTrade; int ExtHandle = INVALID_HANDLE; bool ExtHedging = false; string ExtStrategyName="MA"; string ExtStrategyFileName=""; //+------------------------------------------------------------------+ //| Check new bar | //+------------------------------------------------------------------+ bool IsNewBar() { static datetime last_time=0; datetime t[1]; //--- if(CopyTime(_Symbol,_Period,0,1,t)>0) { if(t[0]!=last_time) { last_time=t[0]; return(true); } } return(false); } //+------------------------------------------------------------------+ //| CheckTrendL1 | //+------------------------------------------------------------------+ double CheckTrendL1() { int max_bars=L1TotalBars; MqlRates rates_data[]; ArrayResize(rates_data,max_bars); ArraySetAsSeries(rates_data,false); if(CopyRates(_Symbol,_Period,0,max_bars,rates_data) != max_bars) { Print("CopyRates failed for L1Trend"); return(0); } //--- prepare data (close prices vector) int data_count=max_bars; vector<double> data_close; data_close.Resize(data_count); for(int i=0; i<data_count; i++) data_close[i] = rates_data[i].close; //--- calculate L1 filter vector<double> data_filtered; data_filtered.Resize(data_count); double dp=0.0; bool res=data_close.L1TrendFilter(L1CoefLambda,true,data_filtered); if(res) dp = data_filtered[data_count-1]-data_filtered[data_count-2]; //--- return(dp); } //+------------------------------------------------------------------+ //| GetTradeSignal | //+------------------------------------------------------------------+ bool GetTradeSignal(ENUM_ORDER_TYPE &signal) { signal = WRONG_VALUE; MqlRates bars[]; double ma[]; ArraySetAsSeries(bars,true); ArraySetAsSeries(ma,true); ArrayResize(bars,2); ArrayResize(ma,2); //-- two last closed bars if(CopyRates(_Symbol,_Period,2,2,bars) != 2) { Print("CopyRates failed"); return(false); } if(CopyBuffer(ExtHandle,0,2,2,ma) != 2) { Print("CopyBuffer failed"); return(false); } double close_prev = bars[1].close; double close_last = bars[0].close; double ma_prev = ma[1]; double ma_last = ma[0]; //--- check MA crossover if(close_prev < ma_prev && close_last > ma_last) signal = ORDER_TYPE_BUY; else if(close_prev > ma_prev && close_last < ma_last) signal = ORDER_TYPE_SELL; //--- log // PrintFormat("PrevBar: time=%s close=%.5f ma=%.5f | LastBar: time=%s close=%.5f ma=%.5f | Signal=%s", // TimeToString(bars[0].time,TIME_DATE|TIME_MINUTES), close_prev, ma_prev, // TimeToString(bars[1].time,TIME_DATE|TIME_MINUTES), close_last, ma_last, // (signal==ORDER_TYPE_BUY?"BUY":signal==ORDER_TYPE_SELL?"SELL":"NONE")); //--- return(true); } //+------------------------------------------------------------------+ //| CheckForOpen | //+------------------------------------------------------------------+ void CheckForOpen() { ENUM_ORDER_TYPE signal; if(!GetTradeSignal(signal)) return; if(signal == WRONG_VALUE) return; //--- L1 filter if(L1FilterOpen) { double delta = CheckTrendL1(); if(signal == ORDER_TYPE_BUY && delta < 0) { signal = WRONG_VALUE; PrintFormat("Open BUY signal cancelled by L1 trend delta=%.5f", delta); } if(signal == ORDER_TYPE_SELL && delta > 0) { signal = WRONG_VALUE; PrintFormat("Open SELL signal cancelled by L1 trend delta=%.5f", delta); } } //--- if(signal == WRONG_VALUE) return; //--- if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) || Bars(_Symbol,_Period)<L1TotalBars) return; //--- double price = (signal==ORDER_TYPE_BUY) ? SymbolInfoDouble(_Symbol,SYMBOL_ASK): SymbolInfoDouble(_Symbol,SYMBOL_BID); //--- ExtTrade.PositionOpen(_Symbol, signal, TradeLot, price, 0, 0); } //+------------------------------------------------------------------+ //| CheckForClose | //+------------------------------------------------------------------+ void CheckForClose() { //--- check position if(!PositionSelect(_Symbol)) return; //--- check position magic if(PositionGetInteger(POSITION_MAGIC)!=MA_MAGIC) return; //--- check trade signal ENUM_ORDER_TYPE signal; if(!GetTradeSignal(signal)) return; //--- long type = PositionGetInteger(POSITION_TYPE); bool close_signal = false; //--- if(type == POSITION_TYPE_BUY && signal == ORDER_TYPE_SELL) close_signal = true; if(type == POSITION_TYPE_SELL && signal == ORDER_TYPE_BUY) close_signal = true; //--- check L1 filter if(L1FilterClose) { double delta = CheckTrendL1(); if(type == POSITION_TYPE_BUY && delta > 0) { close_signal = false; PrintFormat("Close BUY signal cancelled by L1 trend delta=%.5f", delta); } if(type == POSITION_TYPE_SELL && delta < 0) { close_signal = false; PrintFormat("Close SELL signal cancelled by L1 trend delta=%.5f", delta); } } //--- if(close_signal && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) && Bars(_Symbol,_Period)>=L1TotalBars) ExtTrade.PositionClose(_Symbol,3); } //+------------------------------------------------------------------+ //| SelectPosition | //+------------------------------------------------------------------+ bool SelectPosition() { bool res = false; if(ExtHedging) { uint total = PositionsTotal(); for(uint i=0; i<total; i++) { string sym = PositionGetSymbol(i); if(sym == _Symbol && PositionGetInteger(POSITION_MAGIC)==MA_MAGIC) { res = true; break; } } } else { if(PositionSelect(_Symbol)) res = (PositionGetInteger(POSITION_MAGIC)==MA_MAGIC); } return(res); } //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- check parameters if(MovingPeriod<=0) { Print("Error: MovingPeriod parameter must be positive"); return(INIT_PARAMETERS_INCORRECT); } ExtHedging = (AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING); ExtTrade.SetExpertMagicNumber(MA_MAGIC); ExtTrade.SetMarginMode(); ExtTrade.SetTypeFillingBySymbol(_Symbol); //--- prepare indicator ExtHandle = iMA(_Symbol,_Period,MovingPeriod,MovingShift,MODE_SMA,PRICE_CLOSE); if(ExtHandle==INVALID_HANDLE) { Print("Failed to create MA handle"); return(INIT_FAILED); } //--- prepare filename ExtStrategyFileName=PrepareStrategyFileName(ExtStrategyName); //--- delete old file if exists if(FileIsExist(ExtStrategyFileName)) FileDelete(ExtStrategyFileName); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| PrepareStrategyFileName | //+------------------------------------------------------------------+ string PrepareStrategyFileName(string strategy_name) { int v=0; if(L1FilterOpen) v=v | 1; //--- if(L1FilterClose) v=v | 2; //--- string filename=IntegerToString(v)+"_"+strategy_name+"_"+_Symbol+".txt"; return(filename); } //+------------------------------------------------------------------+ //| Save account statistics to file | //+------------------------------------------------------------------+ void SaveAccountStatistics() { //--- check file name if(ExtStrategyFileName=="") return; //--- int file=FileOpen(ExtStrategyFileName,FILE_WRITE|FILE_READ|FILE_TXT|FILE_SHARE_WRITE|FILE_ANSI); if(file==INVALID_HANDLE) { Print("File open error: ",GetLastError()); return; } //--- append FileSeek(file,0,SEEK_END); //--- account data double balance = AccountInfoDouble(ACCOUNT_BALANCE); double equity = AccountInfoDouble(ACCOUNT_EQUITY); double margin = AccountInfoDouble(ACCOUNT_MARGIN); double free_margin = AccountInfoDouble(ACCOUNT_MARGIN_FREE); double margin_lvl = AccountInfoDouble(ACCOUNT_MARGIN_LEVEL); //--- volume double volume=0.0; if(PositionSelect(_Symbol)) volume=PositionGetDouble(POSITION_VOLUME); //--- time datetime t[1]; if(CopyTime(_Symbol,_Period,0,1,t)<=0) { FileClose(file); return; } double current_close[1]; if(CopyClose(_Symbol,_Period,0,1,current_close)<=0) { FileClose(file); return; } string line=StringFormat("%s;%.2f;%.2f;%.2f;%.2f;%.2f;%.2f;%f",TimeToString(t[0],TIME_DATE|TIME_SECONDS), balance,equity,margin,free_margin,margin_lvl,volume,current_close[0]); //--- FileWrite(file,line); //--- FileClose(file); } //+------------------------------------------------------------------+ //| Expert OnTick function | //+------------------------------------------------------------------+ void OnTick() { //--- trade only at new bar if(!IsNewBar()) return; //--- check trade conditions if(SelectPosition()) CheckForClose(); else CheckForOpen(); //--- save account statistics if(SaveStatistics) SaveAccountStatistics(); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- save account statistics if(SaveStatistics) SaveAccountStatistics(); //--- IndicatorRelease(ExtHandle); } //+------------------------------------------------------------------+
3.5.1.1. Méthodologie générale d’évaluation de l’efficacité du filtrage de tendance L1
Pour évaluer l’efficacité du filtre L1, il faut :
- trouver le meilleur ensemble de paramètres de la stratégie de trading, celui qui donne le profit maximal.
Il est préférable d’améliorer les signaux de trading des stratégies les plus performantes. - examiner les résultats des tests pour 4 variantes d’utilisation du filtre (en définissant les paramètres L1FilterOpen / L1FilterClose) :
- trading sans filtres ;
- trading avec filtrage des entrées ;
- trading avec filtrage des sorties ;
- trading avec filtrage des entrées et des sorties.
- exécuter un script Python pour générer les graphiques combinés.
Examinons ces étapes sur l’exemple de l’Expert Advisor MovingAverageFilteredL1.mq5, qui exécute des transactions sur EURUSD, en période H1, avec l’année 2025 comme période de test.
Pour le fonctionnement du filtre, nous utiliserons un nombre fixe de barres pour le calcul de la tendance : L1TotalBars = 1000. Le paramètre de régularisation λ sera spécifié en unités de λmax, avec une valeur fixe L1CoefLambda = 0.2.
Paramètres d’entrée :
//--- best MovingAverage parameters for EURUSD,H1,2025 input int MovingPeriod = 64; // MA period input int MovingShift = 0; // MA shift //--- trade volume input double TradeLot = 0.1; // Lot size //--- L1 filter parameters input int L1TotalBars = 1000; // Total bars for L1 filter input bool L1FilterOpen = false; // Use filter for Open input bool L1FilterClose = false; // Use filter for Close input double L1CoefLambda = 0.2; // Lambda in lambda_max units //--- save statistics input bool SaveStatistics = false; // Save statistics to file
Dans les paramètres d’optimisation, indiquez le symbole EURUSD, la période H1 et la période de test du 01/01/2025 au 31/12/2025.

Fig. 21. Paramètres d’optimisation de l’Expert Advisor MovingAverageFilteredL1.mq5
Pour une optimisation rapide, nous utiliserons le mode « 1 minute OHLC » (cette stratégie ne travaille que sur les nouvelles barres, cette approximation est donc acceptable) et sélectionnerons l’algorithme d’optimisation « Algorithme génétique rapide » avec le critère « Solde max ».

Fig. 22. Paramètres d’optimisation de l’Expert Advisor MovingAverageFilteredL1.mq5
Notre objectif à ce stade étant de trouver les meilleurs paramètres de la stratégie de trading, nous désactivons tous les filtres et l’enregistrement des fichiers dans les paramètres d’optimisation.
Par souci de simplicité, nous n’optimiserons qu’un seul paramètre, « MA Period », de 1 à 800, avec un pas de 1.

Fig. 23. Résultats d’optimisation de l’Expert Advisor MovingAverageFilteredL1.mq5
L’optimisation du paramètre « MA Period » a pris moins d’une minute ; les résultats et la liste des meilleures valeurs sont présentés sur la Fig. 23.
Pour un test plus précis, sélectionnez « Chaque tick basé sur des ticks réels » dans les paramètres de test :

Fig. 24. Paramètres de test de l’Expert Advisor MovingAverageFilteredL1.mq5
Lancez un test unique avec la meilleure valeur « MA Period » = 64 :

Fig. 25. Meilleurs paramètres d’optimisation de l’Expert Advisor MovingAverageFilteredL1.mq5

Fig. 26. Résultats du test avec les meilleurs paramètres pour l’Expert Advisor MovingAverageFilteredL1.mq5
Il faut maintenant lancer les tests avec enregistrement des données dans des fichiers.
Pour ce faire, définissez SaveStatistics = true et lancez les tests avec les 4 combinaisons de filtres des signaux de trading.

Fig. 27. Paramètres de test pour l’enregistrement des résultats dans des fichiers pour l’Expert Advisor MovingAverageFilteredL1.mq5
Après l’exécution des tests avec les 4 combinaisons, le répertoire du testeur contiendra les fichiers : 0_MA_EURUSD.txt, 1_MA_EURUSD.txt, 2_MA_EURUSD.txt et 3_MA_EURUSD.txt.
Ils contiennent l’heure, le cours de clôture, ainsi que le solde et les fonds pour chaque barre de l’intervalle de test.
Créez un répertoire séparé et copiez-y les fichiers (dans cet exemple, C:\Data).

Fig. 28. Fichiers des résultats de test pour les 4 modes de filtrage
L’analyse des données sera effectuée à l’aide d’un script Python :
import pandas as pd import matplotlib.pyplot as plt import os # --- folder for charts output_dir = "C:\\data\\charts\\" os.makedirs(output_dir, exist_ok=True) symbol = "EURUSD" name_strategy = "MA" file_strategy = name_strategy+"_"+symbol title_strategy = " ("+symbol+" "+name_strategy+" strategy+filters)" file_prefix = symbol+"_"+name_strategy+"_" # --- files files = [ "C:\\data\\0_"+file_strategy+".txt", "C:\\data\\1_"+file_strategy+".txt", "C:\\data\\2_"+file_strategy+".txt", "C:\\data\\3_"+file_strategy+".txt" ] # --- labels labels = [ "No filters", "Open L1 filter", "Close L1 filter", "Open+Close L1 filter" ] # --- load data def load_file(filename): df = pd.read_csv( filename, sep=";", header=None, names=[ "time", "balance", "equity", "margin", "free_margin", "margin_level", "volume", "close" ] ) df["time"] = pd.to_datetime(df["time"]) return df # --- close price chart plt.figure(figsize=(10,6), dpi=100) for file, label in zip(files, labels): df = load_file(file) plt.plot(df["time"], df["close"], color='gray') plt.title(symbol+" Close Price") plt.xlabel("Time") plt.ylabel("closing price") plt.legend() plt.grid() plt.tight_layout() plt.savefig(output_dir + file_prefix+"close_price.png", dpi=100) plt.show() # --- balance chart plt.figure(figsize=(10,6), dpi=100) for file, label in zip(files, labels): df = load_file(file) plt.plot(df["time"], df["balance"], label=label) plt.title("Balance" + title_strategy) plt.xlabel("Time") plt.ylabel("Balance") plt.legend() plt.grid() plt.tight_layout() plt.savefig(output_dir + file_prefix+"balance.png", dpi=100) plt.show() plt.close() # --- equity chart plt.figure(figsize=(10,6), dpi=100) for file, label in zip(files, labels): df = load_file(file) plt.plot(df["time"], df["equity"], label=label) plt.title("Equity" + title_strategy) plt.xlabel("Time") plt.ylabel("Equity") plt.legend() plt.grid() plt.tight_layout() plt.savefig(output_dir + file_prefix+"equity.png", dpi=100) plt.show() plt.close() #--- balance + equity chart plt.figure(figsize=(10,6), dpi=100) for i, (file, label) in enumerate(zip(files, labels)): df = load_file(file) # --- get matplotlib color color = plt.rcParams["axes.prop_cycle"].by_key()["color"][i % 10] #--- equity — solid line plt.plot( df["time"], df["equity"], color=color, linestyle="-", label=f"{label} equity" ) #--- balance — dashed line plt.plot( df["time"], df["balance"], color=color, linestyle="--", label=f"{label} balance" ) plt.title("Balance + Equity" + title_strategy) plt.xlabel("Time") plt.ylabel("Value") plt.legend() plt.grid() plt.tight_layout() plt.savefig(output_dir+file_prefix+"balance_equity.png", dpi=100) plt.show() plt.close()
Pour exécuter le script Python dans MetaEditor, cliquez sur « Compiler » (Fig. 29).

Fig. 29. Script PlotData.py dans MetaEditor
Après l’exécution de PlotData.py, les graphiques suivants s’afficheront :
- série des cours EURUSD (cours de clôture de chaque barre) ;
- graphiques du solde pour tous les modes de filtrage ;
- graphiques des fonds pour tous les modes de filtrage ;
- graphiques combinés Solde + Fonds (pour évaluer la réduction du drawdown).
Les graphiques seront également enregistrés dans C:\Data\Charts\ au format PNG.

Fig. 30. Graphique des cours EURUSD sur la période de test

Fig. 31. Graphiques du solde de l’Expert Advisor MovingAverageFilteredL1.mq5 pour les différents modes de filtrage

Fig. 32. Graphiques des fonds de l’Expert Advisor MovingAverageFilteredL1.mq5 pour les différents modes de filtrage

Fig. 33. Graphiques combinés du solde et des fonds de l’Expert Advisor MovingAverageFilteredL1.mq5 pour les différents modes de filtrage
Pour exécuter des scripts Python dans MetaEditor, installez Python (version 3.14 dans l’exemple) et indiquez son chemin dans les paramètres.

Fig. 34. Paramètres Python dans MetaEditor
Le script utilise les bibliothèques pandas et matplotlib. Si elles ne sont pas installées :
pip install pandas pip install matplotlib
3.5.1.2. Résultats de l’application des filtres L1 (stratégie MovingAverage)
Les résultats (graphiques du solde, des fonds et graphiques combinés) sont présentés sur les Fig. 35 à 37.
Couleurs :
- bleu (stratégie sans filtres) ;
- vert (filtre L1 à la clôture des positions) ;
- rouge (filtre L1 à l’ouverture et à la clôture) ;
- orange (filtre L1 à l’ouverture) ;

Fig. 35. Graphiques du solde de l’Expert Advisor MovingAverageFilteredL1.mq5 pour les différents modes de filtrage

Fig. 36. Graphiques des fonds de l’Expert Advisor MovingAverageFilteredL1.mq5 pour les différents modes de filtrage

Fig. 37. Graphiques combinés du solde et des fonds de l’Expert Advisor MovingAverageFilteredL1.mq5 pour les différents modes de filtrage
3.5.2. Stratégie de trading MACD
Comme autre exemple, prenons un Expert Advisor qui exécute des transactions selon les signaux d’une stratégie de trading construite sur l’indicateur MACD (Moving Average Convergence/Divergence).
Signaux de trading
Les signaux sont générés lorsque la ligne principale du MACD croise la ligne de signal :
- BUY — la ligne principale du MACD croise la ligne de signal de bas en haut.
- SELL — la ligne principale du MACD croise la ligne de signal de haut en bas.
Les signaux ne sont analysés qu’à la clôture de la barre, ce qui réduit le nombre de faux déclenchements à l’intérieur de la barre.
En outre, des filtres d’entrée et de sortie sont utilisés selon les réglages du filtre de tendance L1 (L1TotalBars, L1FilterOpen, L1FilterClose, L1CoefLambda).
Code de l’Expert Advisor MACDFilteredL1.mq5 :
//+------------------------------------------------------------------+ //| MACDFilteredL1.mq5 | //| Copyright 2000-2026, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2000-2026, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" //--- best MACD parameters for USDCHF,H1,2025 input int FastEMA = 43; // Fast EMA input int SlowEMA = 59; // Slow EMA input int SignalEMA = 37; // SignalEMA //--- trade volume input double TradeLot = 0.1; // Lot size //--- L1 filter parameters input int L1TotalBars = 1000; // Total bars for L1 filter input bool L1FilterOpen = false; // Use filter for Open input bool L1FilterClose = false; // Use filter for Close input double L1CoefLambda = 0.2; // Lambda in lambda_max units //--- save statistics input bool SaveStatistics = false; // Save statistics to file //--- #define MACD_MAGIC 1234502 #include <Trade\Trade.mqh> int ExtHandle = INVALID_HANDLE; bool ExtHedging = false; CTrade ExtTrade; string ExtStrategyName="MACD"; string ExtStrategyFileName=""; //+------------------------------------------------------------------+ //| Check new bar | //+------------------------------------------------------------------+ bool IsNewBar() { static datetime last_time=0; datetime t[1]; //--- if(CopyTime(_Symbol,_Period,0,1,t)>0) { if(t[0]!=last_time) { last_time=t[0]; return(true); } } return(false); } //+------------------------------------------------------------------+ //| CheckTrendL1 | //+------------------------------------------------------------------+ double CheckTrendL1() { int max_bars=L1TotalBars; MqlRates rates_data[]; ArrayResize(rates_data,max_bars); ArraySetAsSeries(rates_data,false); if(CopyRates(_Symbol,_Period,0,max_bars,rates_data) != max_bars) { Print("CopyRates failed for L1Trend"); return(0); } //--- prepare data (close prices vector) int data_count=max_bars; vector<double> data_close; data_close.Resize(data_count); for(int i=0; i<data_count; i++) data_close[i] = rates_data[i].close; //--- calculate L1 filter vector<double> data_filtered; data_filtered.Resize(data_count); double dp=0.0; bool res=data_close.L1TrendFilter(L1CoefLambda,true,data_filtered); if(res) dp = data_filtered[data_count-1]-data_filtered[data_count-2]; //--- return(dp); } //+------------------------------------------------------------------+ //| GetTradeSignal(MACD) | //+------------------------------------------------------------------+ bool GetTradeSignal(ENUM_ORDER_TYPE &signal) { signal = WRONG_VALUE; double macd_main[]; double macd_signal[]; //--- ArrayResize(macd_main,2); ArrayResize(macd_signal,2); //--- ArraySetAsSeries(macd_main, true); ArraySetAsSeries(macd_signal, true); //--- buffer 0 = MACD main, buffer 1 = signal line if(CopyBuffer(ExtHandle,0,1,2,macd_main)!=2) return(false); if(CopyBuffer(ExtHandle,1,1,2,macd_signal)!=2) return(false); //--- double main_prev = macd_main[1]; double main_last = macd_main[0]; double signal_prev = macd_signal[1]; double signal_last = macd_signal[0]; //--- MACD crossover if(main_prev < signal_prev && main_last > signal_last) signal = ORDER_TYPE_BUY; else if(main_prev > signal_prev && main_last < signal_last) signal = ORDER_TYPE_SELL; //--- return(true); } //+------------------------------------------------------------------+ //| CheckForOpen | //+------------------------------------------------------------------+ void CheckForOpen() { ENUM_ORDER_TYPE signal; if(!GetTradeSignal(signal) || signal == WRONG_VALUE) return; //--- if(L1FilterOpen) { double dp = CheckTrendL1(); if(signal == ORDER_TYPE_BUY && dp < 0) return; if(signal == ORDER_TYPE_SELL && dp > 0) return; } //--- if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) || Bars(_Symbol, _Period) < L1TotalBars) return; //--- double price = (signal == ORDER_TYPE_BUY) ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID); //--- ExtTrade.PositionOpen(_Symbol, signal, TradeLot, price, 0, 0); } //+------------------------------------------------------------------+ //| CheckForClose | //+------------------------------------------------------------------+ void CheckForClose() { //--- check position if(!PositionSelect(_Symbol)) return; //--- check position magic if(PositionGetInteger(POSITION_MAGIC)!=MACD_MAGIC) return; //--- check trade signal ENUM_ORDER_TYPE signal; if(!GetTradeSignal(signal)) return; //--- long type = PositionGetInteger(POSITION_TYPE); bool close_signal = false; //--- if(type == POSITION_TYPE_BUY && signal == ORDER_TYPE_SELL) close_signal = true; if(type == POSITION_TYPE_SELL && signal == ORDER_TYPE_BUY) close_signal = true; //--- check L1 filter if(L1FilterClose) { double dp = CheckTrendL1(); if(type == POSITION_TYPE_BUY && dp > 0) { close_signal = false; PrintFormat("Close BUY signal cancelled by L1 trend dp=%.5f", dp); } if(type == POSITION_TYPE_SELL && dp < 0) { close_signal = false; PrintFormat("Close SELL signal cancelled by L1 trend dp=%.5f", dp); } } //--- if(close_signal) ExtTrade.PositionClose(_Symbol, 3); } //+------------------------------------------------------------------+ //| SelectPosition | //+------------------------------------------------------------------+ bool SelectPosition() { bool res = false; if(ExtHedging) { uint total = PositionsTotal(); for(uint i=0; i<total; i++) { string sym = PositionGetSymbol(i); if(sym == _Symbol && PositionGetInteger(POSITION_MAGIC)==MACD_MAGIC) { res = true; break; } } } else { if(PositionSelect(_Symbol)) res = (PositionGetInteger(POSITION_MAGIC)==MACD_MAGIC); } return(res); } //+------------------------------------------------------------------+ //| Expert initialization | //+------------------------------------------------------------------+ int OnInit() { //--- check parameters if(FastEMA <= 0 || SlowEMA <= 0 || SignalEMA <= 0) { Print("Error: MACD parameters must be positive"); return(INIT_PARAMETERS_INCORRECT); } if(FastEMA >= SlowEMA) { Print("FastEMA must be less than SlowEMA"); return(INIT_PARAMETERS_INCORRECT); } //--- ExtHedging = (AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING); ExtTrade.SetExpertMagicNumber(MACD_MAGIC); ExtTrade.SetMarginMode(); ExtTrade.SetTypeFillingBySymbol(_Symbol); //--- prepare indicator ExtHandle=iMACD(_Symbol,_Period,FastEMA,SlowEMA,SignalEMA,PRICE_CLOSE); if(ExtHandle==INVALID_HANDLE) { Print("Failed to create MACD handle"); return(INIT_FAILED); } //--- prepare filename ExtStrategyFileName=PrepareStrategyFileName(ExtStrategyName); //--- delete old file if exists if(FileIsExist(ExtStrategyFileName)) FileDelete(ExtStrategyFileName); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| PrepareStrategyFileName | //+------------------------------------------------------------------+ string PrepareStrategyFileName(string strategy_name) { int v=0; if(L1FilterOpen) v=v | 1; //--- if(L1FilterClose) v=v | 2; //--- string filename=IntegerToString(v)+"_"+strategy_name+"_"+_Symbol+".txt"; return(filename); } //+------------------------------------------------------------------+ //| Save account statistics to file | //+------------------------------------------------------------------+ void SaveAccountStatistics() { //--- check file name if(ExtStrategyFileName=="") return; //--- int file=FileOpen(ExtStrategyFileName,FILE_WRITE|FILE_READ|FILE_TXT|FILE_SHARE_WRITE|FILE_ANSI); if(file==INVALID_HANDLE) { Print("File open error: ",GetLastError()); return; } //--- append FileSeek(file,0,SEEK_END); //--- account data double balance = AccountInfoDouble(ACCOUNT_BALANCE); double equity = AccountInfoDouble(ACCOUNT_EQUITY); double margin = AccountInfoDouble(ACCOUNT_MARGIN); double free_margin = AccountInfoDouble(ACCOUNT_MARGIN_FREE); double margin_lvl = AccountInfoDouble(ACCOUNT_MARGIN_LEVEL); //--- volume double volume=0.0; if(PositionSelect(_Symbol)) volume=PositionGetDouble(POSITION_VOLUME); //--- time datetime t[1]; if(CopyTime(_Symbol,_Period,0,1,t)<=0) { FileClose(file); return; } double current_close[1]; if(CopyClose(_Symbol,_Period,0,1,current_close)<=0) { FileClose(file); return; } string line=StringFormat("%s;%.2f;%.2f;%.2f;%.2f;%.2f;%.2f;%f",TimeToString(t[0],TIME_DATE|TIME_SECONDS), balance,equity,margin,free_margin,margin_lvl,volume,current_close[0]); //--- FileWrite(file,line); //--- FileClose(file); } //+------------------------------------------------------------------+ //| Expert OnTick function | //+------------------------------------------------------------------+ void OnTick() { //--- trade only at new bar if(!IsNewBar()) return; //--- check trade conditions if(SelectPosition()) CheckForClose(); else CheckForOpen(); //--- save account statistics if(SaveStatistics) SaveAccountStatistics(); } //+------------------------------------------------------------------+ //| Expert deinitialization | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- save account statistics if(SaveStatistics) SaveAccountStatistics(); //--- if(ExtHandle != INVALID_HANDLE) IndicatorRelease(ExtHandle); } //+------------------------------------------------------------------+
Les paramètres de test sont présentés sur la Fig. 38.

Fig. 38. Paramètres du Testeur de Stratégie pour l’Expert Advisor MACDFilteredL1.mq5

Fig. 39. Paramètres de test de l’Expert Advisor MACDFilteredL1.mq5

Fig. 40. Résultats du test de l’Expert Advisor MACDFilteredL1.mq5

Fig. 41. Paramètres de test pour l’enregistrement des résultats dans des fichiers pour l’Expert Advisor MACDFilteredL1.mq5
Après l’exécution des tests dans le Testeur de Stratégie avec les différentes configurations de filtres, des fichiers x_MACD_EURUSD.txt apparaîtront dans le répertoire du testeur.
Ils doivent être copiés dans le dossier C:\Data\, puis le script PlotData.py doit être exécuté.

Fig. 42. Fichiers des résultats de test de l’Expert Advisor MACDFiltered.mq5 pour les différents modes de filtrage
3.5.2.1. Résultats de l’application des filtres L1 (stratégie MACD)
Les résultats sont présentés sur les Fig. 43 à 45.

Fig. 43. Graphiques du solde de l’Expert Advisor MACDFilteredL1.mq5 pour les différents modes de filtrage

Fig. 44. Graphique des fonds de l’Expert Advisor MACDFilteredL1.mq5 pour les différents modes de filtrage

Fig. 45. Graphiques combinés du solde et des fonds de l’Expert Advisor MACDFilteredL1.mq5 pour les différents modes de filtrage
3.5.3. Stratégie de trading ADX
Comme autre exemple, prenons l’Expert Advisor ADXFilteredL1.mq5, qui implémente une stratégie de suivi de tendance basée sur l’Average Directional Movement Index (ADX).
Les principaux signaux de trading reposent sur l’analyse des lignes +DI et −DI, ainsi que sur le niveau de l’ADX, qui caractérise la force de la tendance.
Les signaux sont définis comme suit :
- BUY : +DI croise −DI de bas en haut ;
- SELL : +DI croise −DI de haut en bas.
En outre, la valeur de l’ADX est prise en compte. Si l’ADX est inférieur au seuil ADXTrendLevel, le marché est considéré comme faiblement directionnel ou en consolidation, et le signal est ignoré.
Les valeurs de l’indicateur sur les deux dernières barres clôturées sont utilisées, ce qui exclut l’influence de la barre en cours, non terminée, et réduit les faux signaux.
Des filtres d’entrée et de sortie sont également appliqués selon les réglages de la tendance L1 (L1TotalBars, L1FilterOpen, L1FilterClose, L1CoefLambda).
Le code de l’Expert Advisor ADXFilteredL1.mq5 est présenté ci-dessous.
//+------------------------------------------------------------------+ //| ADXFilteredL1.mq5 | //| Copyright 2000-2026, MetaQuotes Ltd. | //| http://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2000-2026, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" //--- best ADX parameters for EURUSD,H1,2025 input int ADXPeriod = 65; // ADX Period input double ADXTrendLevel = 7; // ADX Trend Level //--- trade volume input double TradeLot = 0.1; // Lot size //--- L1 filter parameters input int L1TotalBars = 1000; // Total bars for L1 filter input bool L1FilterOpen = false; // Use filter for Open input bool L1FilterClose = false; // Use filter for Close input double L1CoefLambda = 0.2; // Lambda in lambda_max units //--- save statistics input bool SaveStatistics = false; // Save statistics to file //--- #define ADX_MAGIC 1234503 #include <Trade\Trade.mqh> CTrade ExtTrade; int ExtHandle = INVALID_HANDLE; bool ExtHedging = false; string ExtStrategyName="ADX"; string ExtStrategyFileName=""; //+------------------------------------------------------------------+ //| Check new bar | //+------------------------------------------------------------------+ bool IsNewBar() { static datetime last_time=0; datetime t[1]; //--- if(CopyTime(_Symbol,_Period,0,1,t)>0) { if(t[0]!=last_time) { last_time=t[0]; return(true); } } return(false); } //+------------------------------------------------------------------+ //| CheckTrendL1 | //+------------------------------------------------------------------+ double CheckTrendL1() { int max_bars=L1TotalBars; MqlRates rates_data[]; ArrayResize(rates_data,max_bars); ArraySetAsSeries(rates_data,false); if(CopyRates(_Symbol,_Period,0,max_bars,rates_data) != max_bars) { Print("CopyRates failed for L1Trend"); return(0); } //--- prepare data (close prices vector) int data_count=max_bars; vector<double> data_close; data_close.Resize(data_count); for(int i=0; i<data_count; i++) data_close[i] = rates_data[i].close; //--- calculate L1 filter vector<double> data_filtered; data_filtered.Resize(data_count); double dp=0.0; bool res=data_close.L1TrendFilter(L1CoefLambda,true,data_filtered); if(res) dp = data_filtered[data_count-1] - data_filtered[data_count-2]; //--- return(dp); } //+------------------------------------------------------------------+ //| GetTradeSignal (ADX) | //+------------------------------------------------------------------+ bool GetTradeSignal(ENUM_ORDER_TYPE &signal) { signal=WRONG_VALUE; double adx[],plusdi[],minusdi[]; ArrayResize(adx,2); ArrayResize(plusdi,2); ArrayResize(minusdi,2); //--- ArraySetAsSeries(adx,true); ArraySetAsSeries(plusdi,true); ArraySetAsSeries(minusdi,true); //--- buffer0 = ADX if(CopyBuffer(ExtHandle,0,1,2,adx)!=2) return(false); //--- buffer1 = +DI if(CopyBuffer(ExtHandle,1,1,2,plusdi)!=2) return(false); //--- buffer2 = -DI if(CopyBuffer(ExtHandle,2,1,2,minusdi)!=2) return(false); double adx_last=adx[0]; double plus_prev=plusdi[1]; double plus_last=plusdi[0]; double minus_prev=minusdi[1]; double minus_last=minusdi[0]; //--- strong trend required if(adx_last<ADXTrendLevel) return(true); //--- +DI cross -DI if(plus_prev<minus_prev && plus_last>minus_last) signal=ORDER_TYPE_BUY; else if(plus_prev>minus_prev && plus_last<minus_last) signal=ORDER_TYPE_SELL; //--- return(true); } //+------------------------------------------------------------------+ //| CheckForOpen | //+------------------------------------------------------------------+ void CheckForOpen() { ENUM_ORDER_TYPE signal; //--- if(!GetTradeSignal(signal) || signal==WRONG_VALUE) return; //--- if(L1FilterOpen) { double dp=CheckTrendL1(); if(signal==ORDER_TYPE_BUY && dp<0) return; if(signal==ORDER_TYPE_SELL && dp>0) return; } //--- if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) || Bars(_Symbol,_Period)<L1TotalBars) return; //--- double price=(signal==ORDER_TYPE_BUY) ? SymbolInfoDouble(_Symbol,SYMBOL_ASK) : SymbolInfoDouble(_Symbol,SYMBOL_BID); //--- ExtTrade.PositionOpen(_Symbol,signal,TradeLot,price,0,0); } //+------------------------------------------------------------------+ //| CheckForClose | //+------------------------------------------------------------------+ void CheckForClose() { //--- check position if(!PositionSelect(_Symbol)) return; //--- check position magic if(PositionGetInteger(POSITION_MAGIC)!=ADX_MAGIC) return; //--- check trade signal ENUM_ORDER_TYPE signal; if(!GetTradeSignal(signal)) return; //--- long type=PositionGetInteger(POSITION_TYPE); bool close_signal=false; //--- if(type==POSITION_TYPE_BUY && signal==ORDER_TYPE_SELL) close_signal=true; //--- if(type==POSITION_TYPE_SELL && signal==ORDER_TYPE_BUY) close_signal=true; //--- check L1 filter //--- check L1 filter if(L1FilterClose) { double dp = CheckTrendL1(); if(type == POSITION_TYPE_BUY && dp > 0) { close_signal = false; PrintFormat("Close BUY signal cancelled by L1 trend dp=%.5f", dp); } if(type == POSITION_TYPE_SELL && dp < 0) { close_signal = false; PrintFormat("Close SELL signal cancelled by L1 trend dp=%.5f", dp); } } //--- if(close_signal) ExtTrade.PositionClose(_Symbol,3); } //+------------------------------------------------------------------+ //| SelectPosition | //+------------------------------------------------------------------+ bool SelectPosition() { bool res = false; if(ExtHedging) { uint total = PositionsTotal(); for(uint i=0; i<total; i++) { string sym = PositionGetSymbol(i); if(sym == _Symbol && PositionGetInteger(POSITION_MAGIC)==ADX_MAGIC) { res = true; break; } } } else { if(PositionSelect(_Symbol)) res = (PositionGetInteger(POSITION_MAGIC)==ADX_MAGIC); } return(res); } //+------------------------------------------------------------------+ //| Expert initialization | //+------------------------------------------------------------------+ int OnInit() { ExtHedging = (AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING); ExtTrade.SetExpertMagicNumber(ADX_MAGIC); ExtTrade.SetMarginMode(); ExtTrade.SetTypeFillingBySymbol(_Symbol); //--- prepare indicator ExtHandle=iADX(_Symbol,_Period,ADXPeriod); if(ExtHandle==INVALID_HANDLE) { Print("ADX handle error"); return(INIT_FAILED); } //--- prepare filename ExtStrategyFileName=PrepareStrategyFileName(ExtStrategyName); //--- delete old file if exists if(FileIsExist(ExtStrategyFileName)) FileDelete(ExtStrategyFileName); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| PrepareStrategyFileName | //+------------------------------------------------------------------+ string PrepareStrategyFileName(string strategy_name) { int v=0; if(L1FilterOpen) v=v | 1; //--- if(L1FilterClose) v=v | 2; //--- string filename=IntegerToString(v)+"_"+strategy_name+"_"+_Symbol+".txt"; return(filename); } //+------------------------------------------------------------------+ //| Save account statistics to file | //+------------------------------------------------------------------+ void SaveAccountStatistics() { //--- check file name if(ExtStrategyFileName=="") return; //--- int file=FileOpen(ExtStrategyFileName,FILE_WRITE|FILE_READ|FILE_TXT|FILE_SHARE_WRITE|FILE_ANSI); if(file==INVALID_HANDLE) { Print("File open error: ",GetLastError()); return; } //--- append FileSeek(file,0,SEEK_END); //--- account data double balance = AccountInfoDouble(ACCOUNT_BALANCE); double equity = AccountInfoDouble(ACCOUNT_EQUITY); double margin = AccountInfoDouble(ACCOUNT_MARGIN); double free_margin = AccountInfoDouble(ACCOUNT_MARGIN_FREE); double margin_lvl = AccountInfoDouble(ACCOUNT_MARGIN_LEVEL); //--- volume double volume=0.0; if(PositionSelect(_Symbol)) volume=PositionGetDouble(POSITION_VOLUME); //--- time datetime t[1]; if(CopyTime(_Symbol,_Period,0,1,t)<=0) { FileClose(file); return; } double current_close[1]; if(CopyClose(_Symbol,_Period,0,1,current_close)<=0) { FileClose(file); return; } string line=StringFormat("%s;%.2f;%.2f;%.2f;%.2f;%.2f;%.2f;%f",TimeToString(t[0],TIME_DATE|TIME_SECONDS), balance,equity,margin,free_margin,margin_lvl,volume,current_close[0]); //--- FileWrite(file,line); //--- FileClose(file); } //+------------------------------------------------------------------+ //| Expert OnTick function | //+------------------------------------------------------------------+ void OnTick() { //--- trade only at new bar if(!IsNewBar()) return; //--- check trade conditions if(SelectPosition()) CheckForClose(); else CheckForOpen(); //--- save account statistics if(SaveStatistics) SaveAccountStatistics(); } //+------------------------------------------------------------------+ //| Expert deinitialization | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- save account statistics if(SaveStatistics) SaveAccountStatistics(); //--- release indicator handle if(ExtHandle!=INVALID_HANDLE) IndicatorRelease(ExtHandle); } //+------------------------------------------------------------------+
Les réglages, les paramètres et les résultats des tests sont présentés sur les Fig. 46 à 48.

Fig. 46. Réglages du testeur pour l’Expert Advisor ADXFilteredL1.mq5

Fig. 47. Paramètres de test de l’Expert Advisor ADXFilteredL1.mq5

Fig. 48. Résultats du test de l’Expert Advisor ADXFilteredL1.mq5
Pour le test, vous devez effectuer 4 exécutions avec les différents réglages du filtre :

Fig. 49. Paramètres de test pour l’enregistrement des résultats dans des fichiers pour l’Expert Advisor ADXFilteredL1.mq5
Des fichiers apparaîtront ensuite dans le répertoire du testeur ; ils doivent être copiés dans C:\Data et traités avec PlotData.py.

Fig. 50. Fichiers des résultats de test de l’Expert Advisor ADXFiltered.mq5 avec les différents réglages du filtre
3.5.3.1. Résultats de l’application des filtres L1 à la stratégie ADX
Les résultats sont présentés sur les Fig. 51 à 53.

Fig. 51. Graphiques du solde de l’Expert Advisor ADXFilteredL1.mq5 avec les différents modes de filtrage

Fig. 52. Graphiques des fonds de l’Expert Advisor ADXFilteredL1.mq5 avec les différents modes de filtrage

Fig. 53. Graphiques combinés du solde et des fonds de l’Expert Advisor ADXFilteredL1.mq5 avec les différents modes de filtrage
3.5.4. Stratégie de trading basée sur le croisement d’EMA
Prenons l’Expert Advisor EMAFilteredL1.mq5, qui implémente une stratégie de trading de suivi de tendance basée sur le croisement de deux moyennes mobiles exponentielles (Exponential Moving Average, EMA).
La stratégie utilise deux moyennes mobiles :
- FastEMA — moyenne mobile exponentielle rapide ;
- SlowEMA — moyenne mobile exponentielle lente.
Les signaux de trading sont générés comme suit :
- BUY : lorsque l’EMA rapide croise l’EMA lente de bas en haut ;
- SELL : lorsque l’EMA rapide croise l’EMA lente de haut en bas.
L’analyse utilise les valeurs des indicateurs sur les deux dernières barres clôturées, ce qui élimine l’influence de la barre en cours, non terminée, et réduit le nombre de faux signaux.
En outre, des filtres d’entrée et de sortie sont appliqués conformément aux réglages du filtre de tendance L1 (L1TotalBars, L1FilterOpen, L1FilterClose, L1CoefLambda).
Le code de l’Expert Advisor EMAFilteredL1.mq5 est présenté ci-dessous.
//+------------------------------------------------------------------+ //| EMAFilteredL1.mq5 | //| Copyright 2000-2026, MetaQuotes Ltd. | //| http://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2000-2026, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" //--- best EMA parameters for EURUSD,H1,2025 input int FastEMA = 29; // Fast EMA input int SlowEMA = 101; // Slow EMA //--- trade volume input double TradeLot = 0.1; // Lot size //--- L1 filter parameters input int L1TotalBars = 1000; // Total bars for L1 filter input bool L1FilterOpen = false; // Use filter for Open input bool L1FilterClose = false; // Use filter for Close input double L1CoefLambda = 0.2; // Lambda in lambda_max units //--- save statistics input bool SaveStatistics = false; // Save statistics to file //--- #define EMA_MAGIC 1234503 #include <Trade\Trade.mqh> CTrade ExtTrade; int ExtHandle = INVALID_HANDLE; bool ExtHedging = false; int FastHandle, SlowHandle; string ExtStrategyName="EMA"; string ExtStrategyFileName=""; //+------------------------------------------------------------------+ //| Check new bar | //+------------------------------------------------------------------+ bool IsNewBar() { static datetime last_time=0; datetime t[1]; //--- if(CopyTime(_Symbol,_Period,0,1,t)>0) { if(t[0]!=last_time) { last_time=t[0]; return(true); } } return(false); } //+------------------------------------------------------------------+ //| CheckTrendL1 | //+------------------------------------------------------------------+ double CheckTrendL1() { int max_bars=L1TotalBars; MqlRates rates_data[]; ArrayResize(rates_data,max_bars); ArraySetAsSeries(rates_data,false); if(CopyRates(_Symbol,_Period,0,max_bars,rates_data) != max_bars) { Print("CopyRates failed for L1Trend"); return(0); } //--- prepare data (close prices vector) int data_count=max_bars; vector<double> data_close; data_close.Resize(data_count); for(int i=0; i<data_count; i++) data_close[i] = rates_data[i].close; //--- calculate L1 filter vector<double> data_filtered; data_filtered.Resize(data_count); double dp=0.0; bool res=data_close.L1TrendFilter(L1CoefLambda,true,data_filtered); if(res) dp = data_filtered[data_count-1] - data_filtered[data_count-2]; //--- return(dp); } //+------------------------------------------------------------------+ //| GetTradeSignal (2EMA crossover) | //+------------------------------------------------------------------+ bool GetTradeSignal(ENUM_ORDER_TYPE &signal) { signal=WRONG_VALUE; //--- double fast[],slow[]; ArrayResize(fast,2); ArrayResize(slow,2); //--- ArraySetAsSeries(fast,true); ArraySetAsSeries(slow,true); //--- if(CopyBuffer(FastHandle,0,1,2,fast)!=2) return(false); //--- if(CopyBuffer(SlowHandle,0,1,2,slow)!=2) return(false); //--- if(fast[1]<slow[1] && fast[0]>slow[0]) signal=ORDER_TYPE_BUY; if(fast[1]>slow[1] && fast[0]<slow[0]) signal=ORDER_TYPE_SELL; //--- return(true); } //+------------------------------------------------------------------+ //| CheckForOpen | //+------------------------------------------------------------------+ void CheckForOpen() { ENUM_ORDER_TYPE signal; //--- if(!GetTradeSignal(signal) || signal==WRONG_VALUE) return; //--- if(L1FilterOpen) { double dp=CheckTrendL1(); //--- if(signal==ORDER_TYPE_BUY && dp<0) return; if(signal==ORDER_TYPE_SELL && dp>0) return; } //--- if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) || Bars(_Symbol,_Period)<L1TotalBars) return; //--- double price=(signal==ORDER_TYPE_BUY) ? SymbolInfoDouble(_Symbol,SYMBOL_ASK) : SymbolInfoDouble(_Symbol,SYMBOL_BID); //--- ExtTrade.PositionOpen(_Symbol,signal,TradeLot,price,0,0); } //+------------------------------------------------------------------+ //| CheckForClose | //+------------------------------------------------------------------+ void CheckForClose() { //--- check position if(!PositionSelect(_Symbol)) return; //--- check position magic if(PositionGetInteger(POSITION_MAGIC)!=EMA_MAGIC) return; //--- check trade signal ENUM_ORDER_TYPE signal; if(!GetTradeSignal(signal)) return; //--- long type=PositionGetInteger(POSITION_TYPE); bool close_signal=false; //--- if(type==POSITION_TYPE_BUY && signal==ORDER_TYPE_SELL) close_signal=true; //--- if(type==POSITION_TYPE_SELL && signal==ORDER_TYPE_BUY) close_signal=true; //--- check L1 filter if(L1FilterClose) { double dp = CheckTrendL1(); if(type == POSITION_TYPE_BUY && dp > 0) { close_signal = false; PrintFormat("Close BUY signal cancelled by L1 trend dp=%.5f", dp); } if(type == POSITION_TYPE_SELL && dp < 0) { close_signal = false; PrintFormat("Close SELL signal cancelled by L1 trend dp=%.5f", dp); } } //--- if(close_signal) ExtTrade.PositionClose(_Symbol,3); } //+------------------------------------------------------------------+ //| SelectPosition | //+------------------------------------------------------------------+ bool SelectPosition() { bool res = false; if(ExtHedging) { uint total = PositionsTotal(); for(uint i=0; i<total; i++) { string sym = PositionGetSymbol(i); if(sym == _Symbol && PositionGetInteger(POSITION_MAGIC)==EMA_MAGIC) { res = true; break; } } } else { if(PositionSelect(_Symbol)) res = (PositionGetInteger(POSITION_MAGIC)==EMA_MAGIC); } return(res); } //+------------------------------------------------------------------+ //| Expert initialization | //+------------------------------------------------------------------+ int OnInit() { ExtHedging = (AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING); ExtTrade.SetExpertMagicNumber(EMA_MAGIC); ExtTrade.SetMarginMode(); ExtTrade.SetTypeFillingBySymbol(_Symbol); //--- prepare indicators FastHandle=iMA(_Symbol,_Period,FastEMA,0,MODE_EMA,PRICE_CLOSE); SlowHandle=iMA(_Symbol,_Period,SlowEMA,0,MODE_EMA,PRICE_CLOSE); if(FastHandle==INVALID_HANDLE||SlowHandle==INVALID_HANDLE) return(INIT_FAILED); //--- prepare filename ExtStrategyFileName=PrepareStrategyFileName(ExtStrategyName); //--- delete old file if exists if(FileIsExist(ExtStrategyFileName)) FileDelete(ExtStrategyFileName); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| PrepareStrategyFileName | //+------------------------------------------------------------------+ string PrepareStrategyFileName(string strategy_name) { int v=0; if(L1FilterOpen) v=v | 1; //--- if(L1FilterClose) v=v | 2; //--- string filename=IntegerToString(v)+"_"+strategy_name+"_"+_Symbol+".txt"; return(filename); } //+------------------------------------------------------------------+ //| Save account statistics to file | //+------------------------------------------------------------------+ void SaveAccountStatistics() { //--- check file name if(ExtStrategyFileName=="") return; //--- int file=FileOpen(ExtStrategyFileName,FILE_WRITE|FILE_READ|FILE_TXT|FILE_SHARE_WRITE|FILE_ANSI); if(file==INVALID_HANDLE) { Print("File open error: ",GetLastError()); return; } //--- append FileSeek(file,0,SEEK_END); //--- account data double balance = AccountInfoDouble(ACCOUNT_BALANCE); double equity = AccountInfoDouble(ACCOUNT_EQUITY); double margin = AccountInfoDouble(ACCOUNT_MARGIN); double free_margin = AccountInfoDouble(ACCOUNT_MARGIN_FREE); double margin_lvl = AccountInfoDouble(ACCOUNT_MARGIN_LEVEL); //--- volume double volume=0.0; if(PositionSelect(_Symbol)) volume=PositionGetDouble(POSITION_VOLUME); //--- time datetime t[1]; if(CopyTime(_Symbol,_Period,0,1,t)<=0) { FileClose(file); return; } double current_close[1]; if(CopyClose(_Symbol,_Period,0,1,current_close)<=0) { FileClose(file); return; } string line=StringFormat("%s;%.2f;%.2f;%.2f;%.2f;%.2f;%.2f;%f",TimeToString(t[0],TIME_DATE|TIME_SECONDS), balance,equity,margin,free_margin,margin_lvl,volume,current_close[0]); //--- FileWrite(file,line); //--- FileClose(file); } //+------------------------------------------------------------------+ //| Expert OnTick function | //+------------------------------------------------------------------+ void OnTick() { //--- trade only at new bar if(!IsNewBar()) return; //--- check trade conditions if(SelectPosition()) CheckForClose(); else CheckForOpen(); //--- save account statistics if(SaveStatistics) SaveAccountStatistics(); } //+------------------------------------------------------------------+ //| Expert deinitialization | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- save account statistics if(SaveStatistics) SaveAccountStatistics(); //--- release indicator handles if(FastHandle!=INVALID_HANDLE) IndicatorRelease(FastHandle); //--- if(SlowHandle!=INVALID_HANDLE) IndicatorRelease(SlowHandle); } //+------------------------------------------------------------------+
Les réglages, les paramètres et les résultats de test de l’Expert Advisor EMAFilteredL1.mq5 sont présentés sur les Fig. 54 à 56.

Fig. 54. Réglages du testeur pour l’Expert Advisor EMAFilteredL1.mq5

Fig. 55. Paramètres de test de l’Expert Advisor EMAFilteredL1.mq5

Fig. 56. Résultats du test de l’Expert Advisor EMAFilteredL1.mq5
Pour analyser l’efficacité des filtres L1, il faut exécuter successivement l’Expert Advisor dans le testeur avec les différents modes de filtrage :

Fig. 57. Paramètres de test pour l’enregistrement des résultats dans des fichiers pour l’Expert Advisor EMAFilteredL1.mq5
Des fichiers seront alors créés dans le dossier du testeur ; il faut les copier dans le répertoire indiqué dans le script Python PlotData.py, puis exécuter ce script avec les réglages : symbol = "EURUSD", name_strategy = "EMA".

Fig. 58. Fichiers des résultats de test de l’Expert Advisor EMAFilteredL1.mq5 pour les différents modes de filtrage des signaux de trading
3.5.4.1. Résultats de l’application des filtres L1 aux signaux de trading de la stratégie EMA
Les résultats sont présentés sur les Fig. 59 à 61.

Fig. 59. Graphiques du solde de l’Expert Advisor EMAFilteredL1.mq5 pour les différents modes de filtrage

Fig. 60. Graphiques des fonds de l’Expert Advisor EMAFilteredL1.mq5 pour les différents modes de filtrage

Fig. 61. Graphiques combinés du solde et des fonds de l’Expert Advisor EMAFilteredL1.mq5 pour les différents modes de filtrage
3.5.5. Synthèse de l’utilisation du filtre L1 dans les stratégies de trading MovingAverage, MACD, ADX et EMA
Dans les exemples considérés, les stratégies de trading ont été testées sur la paire de devises EURUSD, en période H1, sur l’année 2025 (Fig. 62).

Fig. 62. Graphique des cours EURUSD sur la période de test (2025, H1, cours de clôture)

Fig. 63. Graphiques du solde des stratégies Moving Average, MACD, ADX et EMA pour les différents modes de filtrage
L’analyse des stratégies Moving Average, MACD, ADX et EMA a montré que les meilleurs résultats sont obtenus en appliquant le filtrage L1 lors de la clôture des positions (en vert sur les graphiques). L’utilisation du filtre en sortie supprime efficacement les retournements dus au bruit et les faux signaux, ce qui permet de conserver les positions dans le sens d’une tendance stable. Il en résulte une hausse du profit et du Facteur de Profit, ainsi qu’une baisse du drawdown maximal.
L’utilisation du filtrage L1 lors de l’ouverture des positions (en orange) s’est révélée moins efficace : le filtrage supplémentaire limite le nombre d’entrées et fait manquer une partie des mouvements rentables, sans amélioration proportionnelle de la qualité des transactions.

Fig. 64. Graphiques du solde et des fonds des stratégies Moving Average, MACD, ADX et EMA pour les différents modes de filtrage
Ainsi, le filtrage L1 des signaux de trading lors de la clôture des positions accroît la stabilité du système de trading, réduit la sensibilité aux fluctuations de cours à court terme et améliore le rapport profit/risque. Par rapport aux moyennes mobiles classiques, le filtre L1 distingue mieux les corrections temporaires des véritables retournements de tendance, ce qui permet d’utiliser plus efficacement les stratégies de suivi de tendance.
Il convient de prêter attention au comportement des courbes de solde et de fonds lorsque l’on exécute des transactions avec des signaux alignés sur le filtre de tendance L1 (Fig. 64). Lorsque l’on exécute des transactions dans le sens de la tendance, les fonds sont souvent supérieurs au solde, ce qui améliore nettement les métriques de risque et réduit le drawdown. Par conséquent, l’alignement sur la tendance améliore aussi les caractéristiques des systèmes de trading (baisse du drawdown et des risques).
En outre, lorsque les signaux de trading sont alignés sur la tendance L1, le nombre de transactions diminue tandis que leur qualité augmente, ce qui a également un effet positif sur les caractéristiques statistiques globales des stratégies de trading.
| N° | Stratégie | Profit Total Net, USD | % Buy and Hold |
|---|---|---|---|
| 1 | Buy and Hold | 1 363,8 | 100 % |
| 2 | MovingAverage (sans filtre) | 1 001,03 | 73,4 % |
| 3 | MovingAverage (filtre L1 en entrée) | 107,65 | 7,89 % |
| 4 | MovingAverage (filtre L1 en sortie) | 1 342,5 | 98,43 % |
| 5 | MovingAverage (L1 en entrée + sortie) | 986,16 | 72,31 % |
| 6 | MACD (sans filtre) | 997,79 | 73,16 % |
| 7 | MACD (filtre L1 en entrée) | 140,13 | 10,27 % |
| 8 | MACD (filtre L1 en sortie) | 1 359,52 | 99,69 % |
| 9 | MACD (L1 en entrée + sortie) | 697,54 | 51,15 % |
| 10 | ADX (sans filtre) | 791,99 | 58,07 % |
| 11 | ADX (filtre L1 en entrée) | -50,9 | -3,73 % |
| 12 | ADX (filtre L1 en sortie) | 940,39 | 68,95 % |
| 13 | ADX (L1 en entrée + sortie) | 430,05 | 31,53 % |
| 14 | EMA (sans filtre) | 957,3 | 70,19 % |
| 15 | EMA (filtre L1 en entrée) | -173,35 | -12,71 % |
| 16 | EMA (filtre L1 en sortie) | 1 258,99 | 92,31 % |
| 17 | EMA (L1 en entrée + sortie) | -131,41 | -9,64 % |
Tableau 4. Profit total obtenu avec le filtre L1 dans les stratégies MovingAverage, MACD, ADX et EMA, comparé à la stratégie Buy and Hold
D’après le tableau 4, l’utilisation du filtre L1 à la clôture des positions a amélioré la rentabilité de toutes les stratégies.
Si l’on prend le résultat de la stratégie Buy and Hold (1 363,8 USD) comme 100 % du mouvement de tendance complet, on obtient :
- le profit de Moving Average est passé de 73,4 % à 98,43 % ;
- le profit de MACD est passé de 73,16 % à 99,69 % ;
- le profit d’ADX est passé de 58,07 % à 68,5 % ;
- le profit d’EMA est passé de 70,19 % à 92,31 %.
Comme on le voit, l’utilisation du filtre L1 a permis aux stratégies Moving Average, MACD et EMA d’augmenter leur profit de 22 à 26 %, en captant l’essentiel du mouvement de tendance (98,43 %, 99,69 % et 92,31 %) et en se rapprochant du résultat de la stratégie Buy and Hold. Le profit de la stratégie ADX a augmenté de 10 %.
Dans les exemples, nous avons considéré des stratégies avec les paramètres donnant les valeurs de solde les plus élevées (c’est-à-dire parmi les meilleures solutions possibles). Elles sont mises en évidence en bleu. Les résultats montrent que même ces solutions, les plus rentables, peuvent encore être améliorées par un filtrage supplémentaire des signaux de trading grâce à l’alignement sur la tendance L1. Certaines stratégies ne se sont que légèrement améliorées (pour ADX, les courbes vertes sont proches des bleues, ce qui indique une proximité avec la solution optimale en termes de solde). La qualité des signaux de trading d’une stratégie (le caractère optimal des paramètres choisis) peut être jugée à l’aune de l’amélioration apportée par un tel filtrage L1.
Il faut noter que, dans ce cas, le marché EURUSD considéré présentait une forte tendance (Fig. 62). Pour d’autres régimes de marché et instruments, les résultats seront différents. En outre, la tendance L1 a été construite sur la période H1 avec un paramètre de régularisation λ = 0,2·λmax. Pour d’autres instruments et périodes, les valeurs appropriées de ce coefficient peuvent être estimées à l’aide des indicateurs de tendance L1.
Conclusion
Le filtrage de tendance L1 a prouvé son utilité pratique en tant qu’outil permettant de séparer le bruit local des véritables changements de tendance.
La méthode produit une tendance linéaire par morceaux avec des points de rupture automatiques et une échelle de réglage pratique via λmax, ce qui élimine le problème de l’ajustement manuel des paramètres.
Pour l’intégration pratique, une boîte à outils complète a été fournie : des fonctions de calcul de λmax et du filtre L1, trois indicateurs (L1Trend, L1TrendSlope, L1TrendSlopeSign), sept indicateurs de volatilité basés sur la tendance L1 (L1Volatility, L1VolatilitySmoothed, L1VolatilityAbsolute, L1VolatilityNormalized, L1VolatilityNormalizedSmoothed, L1VolatilityRegime, L1VolatilityRegimeColor), des modèles d’Expert Advisors et un protocole de test reproductible (quatre modes : sans filtre, filtre en entrée, filtre en sortie, les deux filtres ; enregistrement des résultats et script Python de visualisation).
Notons également que le filtre de tendance L1 peut être utilisé pour l’étiquetage des données en apprentissage automatique (Machine Learning, ML). Ainsi, dans l’article « Développer des stratégies de trading de tendance avec l’apprentissage automatique », la tendance est déterminée à l’aide des dérivées des cours lissés par le filtre de Savitzky–Golay. Une approche similaire peut être mise en œuvre avec le filtrage L1, où la tendance est approximée par des fonctions linéaires par morceaux et où la force de la tendance sur chaque segment est naturellement liée à la pente de ce segment.
Recommandations pratiques :
- Utilisez une régularisation relative : λ = coef_lambda_max · λmax. Pour la plupart des tâches, utilisez un coef compris entre 0,04 et 0,25 ; pour plus de détail, ≈0,02–0,04 ; pour une approximation grossière et la détection de régime, ≈0,12–0,25.
- Dans la plupart des cas, le filtre L1 est le plus efficace lorsqu’il est appliqué à la clôture des positions (conservation des tendances rentables et réduction des sorties prématurées). L’appliquer aux entrées réduit souvent le nombre de transactions sans amélioration proportionnelle de leur qualité.
- Pour analyser la tendance actuelle, utilisez une règle simple : delta = x_filtered[last] − x_filtered[last−1]. Le signe de delta indique la direction de la tendance L1 dominante.
Limite : l’effet dépend de l’instrument, de la période et du régime de marché ; une validation sur des données historiques avec les métriques choisies est nécessaire.
Les modules MQL5 et le protocole de test proposés permettent de tester rapidement des hypothèses et de choisir les paramètres de travail d’un système de trading donné.
Tous les codes de l’article sont également disponibles dans le projet public « MQL5\Shared Projects\L1Trend ».
Exemples
| Type | Fichier | Description |
|---|---|---|
| Script | MQL5\Scripts\TestL1Trend.mq5 | Script de test pour le calcul de la tendance L1 sur des données simulées (marche aléatoire) |
| Script | MQL5\Scripts\TestL1TrendFloatDouble.mq5 | Script de test pour le calcul de la tendance L1 sur des données simulées (marche aléatoire) pour les vecteurs double et float |
| Script | MQL5\Scripts\TestL1TrendFilterSP500.mq5 | Script de test pour le calcul de la tendance L1 sur les cotations de l’indice SP500 |
| Fichier de données | MQL5\Files\snp500.txt | Fichier de données pour le script de test (logarithme de la série des cours de l’indice SP500) |
| Script | MQL5\Scripts\TestScalingBrownianMotion.mq5 | Script de calcul de la dépendance en loi de puissance de λmax pour le mouvement brownien |
| Script | MQL5\Scripts\TestScalingSymbol.mq5 | Script de calcul de la dépendance en loi de puissance de λmax pour la série des cours d’un symbole donné |
| Indicateur | MQL5\Indicators\L1TrendFilter.mq5 | Indicateur de calcul de la tendance L1 |
| Indicateur | MQL5\Indicators\L1TrendFilter_Slope.mq5 | Indicateur de calcul du taux de variation de la tendance L1 |
| Indicateur | MQL5\Indicators\L1TrendFilter_SlopeSign.mq5 | Indicateur de calcul du signe de la variation de la tendance L1 |
| Indicateur | MQL5\Indicators\L1Volatility.mq5 | Indicateur de calcul de la volatilité résiduelle (différence entre les cours de clôture et la valeur de la tendance L1) |
| Indicateur | MQL5\Indicators\L1VolatilitySmoothed.mq5 | Indicateur de calcul de la volatilité résiduelle lissée |
| Indicateur | MQL5\Indicators\L1VolatilityAbsolute.mq5 | Indicateur de calcul de la volatilité absolue |
| Indicateur | MQL5\Indicators\L1VolatilityNormalized.mq5 | Indicateur de calcul de la volatilité normalisée à l’aide de l’ATR (Average True Range) et de la tendance L1 |
| Indicateur | MQL5\Indicators\L1VolatilityNormalizedSmoothed.mq5 | Indicateur de calcul de la volatilité normalisée lissée |
| Indicateur | MQL5\Indicators\L1VolatilityRegime.mq5 | Indicateur de détection du régime de marché |
| Indicateur | MQL5\Indicators\L1VolatilityRegimeColor.mq5 | Version couleur de l’indicateur de détection du régime de marché |
| Expert Advisor | MQL5\Experts\MovingAverageFilteredL1.mq5 | Expert Advisor de trading basé sur la stratégie Moving Average avec filtre L1 |
| Expert Advisor | MQL5\Experts\MACDFilteredL1.mq5 | Expert Advisor de trading basé sur la stratégie MACD avec filtre L1 |
| Expert Advisor | MQL5\Experts\ADXFilteredL1.mq5 | Expert Advisor de trading basé sur la stratégie ADX avec filtre L1 |
| Expert Advisor | MQL5\Experts\EMAFilteredL1.mq5 | Expert Advisor de trading basé sur le croisement de deux EMA avec filtre L1 |
| Script Python | MQL5\Scripts\PlotData.py | Script Python d’analyse de l’efficacité de l’application du filtre L1 aux signaux de trading |
Tableau 5. Description des programmes utilisés dans l’article
Traduit du russe par MetaQuotes Ltd.
Article original : https://www.mql5.com/ru/articles/21142
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.
En gros, ça donne quelque chose comme ça :
La segmentation en tendances dépend fortement du paramètre de régularisation lambda : plus lambda est petit, plus les tendances détectées sont courtes.
Dans les exemples considérés, des valeurs fixes de lambda ont été utilisées, exprimées en unités lambda = 0,2 * lambda_max. Le calcul en unités lambda_max permet en partie de s'adapter aux données. La valeur même de lambda_max dépend de la géométrie de la série (des amplitudes relatives), c'est-à-dire de la volatilité.
Il faut garder à l’esprit qu’une tendance comporte différentes phases et possède son propre cycle de vie. C’est pourquoi il faut un mécanisme permettant de s’adapter à la tendance actuelle, c’est-à-dire de gérer lambda d’une manière ou d’une autre et de trouver la segmentation optimale de la tendance — cette tâche n’est pas encore résolue.
Si la stratégie elle-même ne génère pas de bénéfices sur la période considérée, le filtre ne pourra pas non plus y remédier.
Les meilleurs résultats devraient être obtenus sur un marché parfaitement tendanciel ; voici un exemple : EURUSD, 2025, H1 (meilleurs paramètres : MovingAverage period=61).
Filtre L1 Close
On constate ici que le filtre de sortie a permis d’augmenter les bénéfices sur la partie de tendance.
Variante de la même stratégie avec ajout de positions lors des corrections :
sans ajouts :
Avec ajouts :
Les intervalles de marché latéral comportent de petites tendances locales et, pour les prendre correctement en compte, il faut utiliser des valeurs plus faibles pour le paramètre lambda (lorsqu'il est utilisé comme filtre de sortie).
De plus, les meilleures valeurs des paramètres MovingAverage sur l'intervalle de marché latéral doivent être différentes. Autrement dit, les périodes optimales des moyennes sur le deuxième intervalle ont changé (mais lors de l’optimisation dans le testeur, les paramètres trouvés génèrent le plus grand profit parmi tous les autres sur l’ensemble de l’intervalle d’optimisation).
Vérifions les résultats sur un intervalle plat avec différentes valeurs de lambda.
Sans filtres :
Avec un filtre de sortie lambda = 0,2 * lambda_max
Avec un filtre lambda = 0,001 lambda_max (tendances plus fines)
Ainsi, sur la zone de consolidation, avec lambda = 0,001 lambda_max, on parvient à améliorer le résultat obtenu sans filtres et à prendre en compte les petites tendances locales.
Cependant, l'option avec un filtre lambda = 0,2 * lambda_max s'est ici révélée moins rentable que la stratégie sans filtres.
Stratégie consistant à ouvrir des positions (avec différents lambda) sur des tendances locales au sein d'un mouvement latéral
Sans filtres :
Avec un filtre lambda = 0,2 * lambda_max et des ouvertures de positions lors des corrections :
Avec un filtre lambda = 0,001*lambda_max et des ouvertures de positions lors des corrections :
La variante avec un filtre lambda = 0,2*lambda_max et l'ajout de positions lors des corrections a donné de meilleurs résultats que la variante sans filtre.
L'ajout, lors des corrections, de petites tendances locales (lambda = 0,001 * lambda_max) au sein d'un intervalle de consolidation a permis d'augmenter les bénéfices de la stratégie originale sans filtre (et d'améliorer les bénéfices de la variante avec lambda = 0,2 * lambda_max).
Variante consistant à ajouter des positions (avec différents lambda) sur des tendances locales au sein d'un mouvement latéral
Sans filtres :
Avec un filtre lambda = 0,2 * lambda_max et des ouvertures de positions lors des corrections :
Avec un filtre lambda = 0,001*lambda_max et l'ajout des corrections :
La variante avec un filtre lambda = 0,2 * lambda_max et l'ajout de corrections a donné un meilleur résultat que la variante sans filtre.
L'ajout, lors des corrections, de petites tendances locales (lambda = 0,001 * lambda_max) à l'intérieur de l'intervalle de consolidation a permis d'augmenter le profit de la stratégie d'origine sans filtre (et d'améliorer le profit de la variante avec lambda = 0,2 * lambda_max).
Négociez, ne serait-ce que sur un compte démo
La compréhension viendra avec l'expérience, après avoir enduré 10 mois de travail infructueux
Merci de nous avoir présenté ces deux nouvelles fonctions pour les vecteurs. Elles sont très utiles. Vous avez accompli un travail considérable, mais malheureusement, tous vos indicateurs présentent un fort redessinage : même sur l'historique, les pics s'inversent ou disparaissent. Ce n’est pas pour rien que vous les avez comparés au filtre de Hodrick-Prescott, qui se redessine : on pourrait également les comparer à la TMA centrée et à quelques autres. Sur les périodes plus longues, un renversement de tendance peut apparaître 300 bougies en arrière ; on peut alors passer une année à trader (et perdre son dépôt) à contre-courant de la tendance. Pour le trading réel, ni les indicateurs, ni les robots basés sur ceux-ci ne conviennent, car ils tentent de prédire l’avenir.
En réalité, ces fonctions permettent de créer des indicateurs qui ne se redessinent pas. Dans le cas de vos indicateurs, il suffit d’attendre 1 000 bougies et de refaire le calcul à partir de la fin de la période précédente, mais cela n’est pas utile dans la pratique ; il existe des options plus intéressantes. Bonne chance dans vos recherches.