Python + MetaTrader 5 : Сadre de recherche rapide pour les données, les variables et les prototypes
Introduction
Python est devenu l'un des outils les plus pratiques pour travailler avec des données. Il offre un large éventail de bibliothèques qui nous permettent d'effectuer rapidement des analyses statistiques, de tester des hypothèses et de présenter visuellement les résultats sans gaspiller de temps ni de ressources. Ceci est important pour résoudre les problèmes liés aux marchés financiers : dans ce contexte, ce n'est pas seulement la rapidité du traitement des données qui compte, mais aussi la capacité à passer rapidement de l'analyse aux conclusions pratiques.
MetaTrader 5 offre une intégration directe avec Python, ce qui élargit considérablement les possibilités de travail pratique avec les données de marché. Un chercheur ou un développeur peut utiliser l’écosystème Python qu’il connaît déjà pour étudier les données de prix, construire des modèles statistiques et tester des hypothèses pratiques sans quitter la plateforme de trading. Cette approche rend le flux de travail plus flexible et soutient un cycle unifié : des données à l’hypothèse, de l’hypothèse au modèle et du modèle à l’application pratique.
Dans cet article, nous allons montrer :
- comment Python est intégré à MetaTrader 5 ;
- comment l'utiliser pour analyser des données financières et tester des hypothèses ;
- comment construire et entraîner un petit modèle, puis transférer le résultat de l'entraînement vers un EA en utilisant ONNX.
Cela nous permettra de passer d'une expérience de recherche à une mise en œuvre pratique dans un système de trading.
1. Installation et connexion
Avant de procéder à l'analyse des données, il est nécessaire de préparer soigneusement l'environnement de travail pour l'utilisation conjointe de Python et de MetaTrader 5. La tâche est simple, mais elle exige de la précision. Une configuration correcte dès le départ vous évitera des dizaines de petits problèmes par la suite.
Tout d'abord, installons le terminal MetaTrader 5 en téléchargeant la distribution depuis le site officiel.
Ensuite, nous aurons besoin de la version actuelle de Python. Au moment de la rédaction de cet article, il s’agit de la version 3.14.3. Lors de l'installation, veillez à activer l'option qui ajoute Python à la variable d'environnement PATH. Cela permettra de travailler avec l'interpréteur directement depuis la ligne de commande, sans réglages manuels inutiles.

Le point clé consiste à isoler l'environnement. L'expérience montre que les projets qui travaillent avec des données et des modèles se retrouvent rapidement surchargés de dépendances. Afin de garantir l'ordre et la reproductibilité des résultats, un environnement virtuel distinct est créé pour chaque projet. En Python, on peut le faire grâce à l'outil venv intégré.
Le flux de travail est le suivant.
- Ouvrez une invite de commandes. Le moyen le plus simple est d'appuyer sur Win+R. Dans la fenêtre qui apparaît, saisissez la commande cmd et appuyez sur Entrée. Cela ouvre l'invite de commandes Windows. Vous pouvez utiliser PowerShell si vous le souhaitez — le même principe s'applique dans ce cas.
- Accédez au répertoire du projet où l'environnement sera créé. Cela se fait avec la commande standard :
- Créez un environnement virtuel :
- Installez le module Python pour interagir avec MetaTrader 5 :
- Pour une analyse complète des données financières, il est judicieux de déployer immédiatement une pile de bibliothèques de base, déjà presque complète pour un usage opérationnel. Elle couvre les tâches clés que sont le traitement des données, la modélisation et l'analyse technique.
cd /path/to/your/project
python -m venv integration
Et activez-le :
integration\Scripts\activate
Désormais, tous les packages installés seront isolés au sein du projet actuel.
pip install MetaTrader5
Tout d'abord, installez NumPy — la base du calcul numérique. C'est sur cette base que repose l'ensemble de la pile logicielle suivante.
Ensuite, nous installons Pandas — l'outil principal pour travailler avec des données tabulaires et des séries temporelles, sans lequel l'analyse des prix devient un véritable calvaire.
Pour la visualisation, nous utiliserons Matplotlib et Seaborn. Le premier vous offre un contrôle total sur vos graphiques, et le second accélère la création de visualisations statistiquement expressives. En travaillant de concert, ils nous permettent de voir le marché, au lieu de simplement le quantifier.
Pour les tâches d'apprentissage automatique, nous ajoutons Scikit-Learn — un outil éprouvé pour la construction et la validation de modèles. Il est parfaitement adapté aux prototypes initiaux et aux stratégies de base.
Pour l'analyse de marché appliquée, nous ajoutons la bibliothèque TA — une bibliothèque d'indicateurs techniques. Il s'agit d'une méthode pratique pour enrichir rapidement les données avec des signaux sans avoir à implémenter manuellement des formules classiques.
L'installation des bibliothèques s'effectue en une seule commande :
pip install numpy pandas matplotlib seaborn scikit-learn ta
Il est judicieux d'ajouter la bibliothèque pytz. À première vue, elle est auxiliaire, mais en pratique, elle est essentielle pour travailler avec les fuseaux horaires.
pip install pytz
Les données financières sont hautement sensibles au facteur temps. Les bourses fonctionnent dans des fuseaux horaires différents.
Par défaut, Python utilise l'heure locale du système lors de la création de l'objet datetime. Ce comportement est pratique pour les tâches quotidiennes, mais dans un contexte financier, il devient une source d'erreurs systémiques. MetaTrader 5 stocke les heures des ticks et des barres au format UTC, sans décalage et sans référence au fuseau horaire local.
Cela crée un décalage classique. Le modèle et les données ne partagent pas le même fuseau horaire. En pratique, cela entraîne des effets indésirables.
Par conséquent, la règle est ici stricte. Toutes les opérations liées au temps doivent être effectuées en UTC. Les objets datetime doivent être explicitement créés en fuseau horaire UTC, et toutes les valeurs locales doivent être converties en une norme unique. Cela aligne les données et le modèle dans le même système de coordonnées temporelles.
Il est recommandé d'utiliser pytz pour une gestion explicite des fuseaux horaires. Cela élimine les transformations implicites et rend le comportement du système prévisible.
Les données obtenues à partir de MetaTrader 5 sont déjà en UTC. Il ne faut pas les corriger artificiellement. Elles doivent être correctement interprétées et cohérentes avec la logique du modèle. Dans les problèmes financiers, le temps n'est pas seulement un repère, mais un axe de coordonnées. La moindre erreur en ce domaine fausse toute la géométrie de l’analyse.
Cet ensemble semble limité, mais en pratique, il couvre jusqu'à 80% des tâches typiques. Il s'agit d'une approche classique : moins de redondance, plus d'efficacité.
Si vous devez quitter l'environnement actuel, utilisez la commande standard :
deactivate
À ce stade, l'infrastructure est entièrement prête. Le terminal, l'interpréteur et les bibliothèques nécessaires sont installés. L'environnement est isolé et reproductible. C’est sur cette base qu’il est facile et sûr de construire des analyses plus avancées, de tester des hypothèses et de passer progressivement au développement de modèles de trading.
2. Chargement des données
Une fois l'infrastructure mise en place, nous passons à la première étape pratique : l'écriture du programme. Il n'y a pas de limitations strictes : n'importe quel éditeur familier fera l'affaire. Toutefois, dans le contexte de l'intégration, il est logique d'utiliser l'éditeur intégré de MetaTrader 5 appelé MetaEditor, qui prend déjà en charge le travail avec Python et permet de conserver l'ensemble du processus dans un seul cadre.
Pour lancer des scripts Python directement depuis MetaEditor ou le terminal, il suffit de spécifier une seule fois le chemin d'accès à l'interpréteur dans les paramètres de la plateforme. Il s'agit d'une opération de base, mais il y a un détail important à noter. Si vous utilisez l’environnement virtuel créé précédemment, le chemin d'accès doit pointer spécifiquement vers son interpréteur, et non vers une installation Python globale.

Cette approche préserve l'isolation du projet et garantit que toutes les dépendances restent maîtrisées. Sinon, vous risquez de rencontrer des bugs subtils où le même code se comporte différemment selon l'environnement d'exécution. En matière financière, c'est un luxe inacceptable.
Dans un premier temps, nous établissons une connexion de base : Python → MetaTrader 5 → données historiques. La tâche est simple dans sa formulation et critique par essence : se connecter au terminal à partir d'un script et obtenir les données de prix pour un instrument et une unité de temps spécifiés. C’est à partir de ce moment que commence toute analyse significative.
Il est logique de structurer le script en blocs. Tout d'abord, nous importons les bibliothèques nécessaires – cela constitue l'ensemble d'outils de travail :
from datetime import datetime import MetaTrader5 as mt5 import pandas as pd import numpy as np import pytz import seaborn as sns import matplotlib.pyplot as plt import ta
Ensuite, la connexion au terminal est initialisée via le module MetaTrader5. Il s'agit du point d'entrée du système. Si la connexion n'est pas établie, il est inutile de continuer. Par conséquent, le contrôle d'état est effectué immédiatement et sans compromis.
# Display data on the MetaTrader 5 package print("MetaTrader5 package author: ", mt5.__author__) print("MetaTrader5 package version: ", mt5.__version__) # Connection to MetaTrader 5 terminal if not mt5.initialize(): print("initialize() failed, error code =", mt5.last_error()) quit()
L'étape suivante consiste à définir l'intervalle de temps. Nous utilisons ici pytz pour indiquer explicitement le fuseau horaire UTC. Il ne s'agit pas d'un détail technique, mais d'une exigence obligatoire. Le terminal enregistre les données de prix en UTC, et toute divergence du côté Python entraîne un biais dans les données. L'erreur est silencieuse, mais ses conséquences sont systémiques.
# Set time zone to UTC timezone = pytz.timezone("Etc/UTC") # Create 'datetime' objects in UTC time zone to avoid the implementation of a local time zone offset utc_from = datetime(2020, 1, 1, tzinfo=timezone) utc_to = datetime.now(timezone) # Set to the current date and time
Ensuite, une demande de données historiques est effectuée. L'exemple utilise des barres horaires pour l’EURUSD. Il convient ici de prêter attention au nom de l'instrument. Il doit correspondre strictement à l'orthographe du terminal, y compris les suffixes et les préfixes.
# Get bars from EURUSD H1 (hourly timeframe) within the specified interval rates = mt5.copy_rates_range("EURUSD", mt5.TIMEFRAME_H1, utc_from, utc_to)
Le résultat est un ensemble structuré de données contenant des prix et des horodatages — le marché à l’état brut sans filtres ni interprétations. C’est précisément le type de données nécessaires dans un premier temps.
Après cela, la connexion au terminal est correctement fermée. Il s'agit d’une discipline d'exécution : un arrêt propre empêche les défaillances cachées lors des démarrages suivants et rend le comportement du système prévisible.
# Shut down connection to the MetaTrader 5 terminal
mt5.shutdown()
S'ensuit une vérification basique du résultat. Si des données sont reçues, les premiers enregistrements sont affichés pour une validation rapide. Sinon, le script s'arrête.
# Check if data was retrieved if rates is None or len(rates) == 0: print("No data retrieved. Please check the symbol or date range.") quit() # Print the first 10 raw records for a quick data sanity check print("Display obtained data 'as is'") for rate in rates[:10]: print(rate)
Une visualisation est également effectuée : un graphique des prix de clôture et des volumes est construit. Dans ce cas, il est conseillé de placer les volumes sur un axe séparé. Nous fixerons la valeur maximale le long de l'axe des volumes avec une marge cinq fois supérieure à la valeur maximale observée. Cette méthode paraît simple, mais elle fonctionne parfaitement. L'histogramme est plaqué contre le bas du graphique et cesse de rivaliser avec le prix pour attirer l'attention.
De ce fait, la ligne du cours de clôture reste claire et lisible, et les volumes restent informatifs sans surcharger la présentation visuelle. Il s'agit d'un équilibre classique entre exhaustivité et compréhensibilité des données. Un graphique ne doit pas seulement contenir des informations, mais aussi permettre leur interprétation rapide et sans stress inutile.
# Create a DataFrame from the retrieved tick data rates_frame = pd.DataFrame(rates) # Convert the timestamp column from seconds since epoch to datetime rates_frame['time'] = pd.to_datetime(rates_frame['time'], unit='s') # Use datetime as the DataFrame index for time series plotting and analysis rates_frame.set_index('time', inplace=True) # Plot closing price and tick volume fig, ax1 = plt.subplots(figsize=(12, 6)) # Close price on primary y-axis ax1.set_xlabel('Date') ax1.set_ylabel('Close Price', color='tab:blue') ax1.plot(rates_frame.index, rates_frame['close'], color='tab:blue', label='Close Price') ax1.tick_params(axis='y', labelcolor='tab:blue') # Tick volume on secondary y-axis ax2 = ax1.twinx() ax2.set_ylabel('Tick Volume', color='tab:green') max_tick = rates_frame['tick_volume'].max() ax2.set_ylim(0, max_tick * 5) ax2.plot(rates_frame.index, rates_frame['tick_volume'], color='tab:green', label='Tick Volume') ax2.tick_params(axis='y', labelcolor='tab:green') # Show the plot plt.title('Close Price and Tick Volume Over Time') fig.tight_layout() plt.show() fig.savefig('close_price.png')

D'un point de vue pratique, la procédure semble élémentaire. Mais il s'agit en réalité d'une étape clé du contrôle de la qualité des données. Vous comprenez parfaitement ce qui est intégré au modèle : format, horodatage, valeurs. Ce type d'audit initial est un exemple classique d'approche d'ingénierie. Cela permet d'économiser du temps, des nerfs et, ce qui est particulièrement important dans les systèmes de trading, de l'argent.
3. Tester les hypothèses et sélectionner les variables
Maintenant que nous disposons de données historiques sur les prix, passons à l'analyse initiale. Commençons par une hypothèse extrêmement simple, presque classique : le marché tend à prolonger le mouvement de la barre précédente. Vérifions s'il existe une inertie dans la dynamique à court terme.
La boîte à outils Python vous permet de tester une telle hypothèse en quelques lignes seulement. La logique est la suivante. Prenons une série de cours de clôture et passons ensuite aux différences de prix d'une barre à l'autre. On obtient ainsi la dynamique des prix, ce qui est fondamental pour l'analyse.
# Correlation analysis between adjacent bar moves close = rates_frame['close'].to_numpy(dtype=float) # last and next price move differences diff = close[1:] - close[:-1]
Ensuite, formons deux séries chronologiques : les changements précédents et les changements suivants. Techniquement, cela se fait en décalant le tableau d'un élément. Le résultat est constitué de paires de valeurs où chaque observation répond à une question simple : si le marché montait (ou descendait), que se passe-t-il sur la barre suivante ?
diff = np.column_stack((diff[:-1], diff[1:])) data_matrix = pd.DataFrame(diff, columns=['last', 'next'])
Ensuite, le coefficient de corrélation de Pearson entre ces deux séries est calculé. Une corrélation positive indique une forte inertie, et une corrélation négative indique un repli prédominant.
Pour plus de clarté, le résultat est visualisé à l'aide de Seaborn — une carte thermique de corrélation est construite. C'est un moyen rapide de visualiser la structure de la dépendance sans entrer dans les détails numériques.
correlation_matrix = data_matrix.corr('pearson') plt.subplots(figsize=(3, 2)) sns.heatmap(correlation_matrix, annot=True, cmap='coolwarm') plt.title('Correlation Bar to Bar') plt.savefig('bar_to_bar.png') plt.show()
Le point essentiel ici n'est pas la stratégie elle-même – elle est intrinsèquement primitive. La valeur se trouve ailleurs. Nous présentons un cycle de test d'hypothèses de base. Nous avons formulé une hypothèse, transformé les données, effectué un calcul et visualisé le résultat. Cette approche structure l'analyse et nous empêche de tirer des conclusions à l'œil.

Mais évaluons tout de même les résultats obtenus. La corrélation observée est de −0,018 — une valeur proche de zéro, mais avec un signe négatif.
Cela signifie qu'il n'existe pas de relation claire entre les barres adjacentes. De plus, le faible signe négatif indique un subtil effet de retour à la moyenne. Après un mouvement dans une direction, la barre suivante est susceptible de se déplacer dans la direction opposée. Cependant, l'ampleur de cet effet est si faible que, d'un point de vue pratique, il se rapproche du bruit statistique.
L'hypothèse de la poursuite du mouvement n'est pas confirmée. Sur la période H1, le marché se comporte davantage comme un processus aléatoire que comme un système inertiel. Il s'agit d'une observation importante. Elle élimine d'emblée toute une catégorie de stratégies naïves et établit un point de départ plus réaliste pour une analyse plus approfondie.
Une seule bougie, c'est une échelle trop petite. Ces données sont dominées par le bruit plutôt que par la structure. Nous passons donc à l'étape logique suivante : nous élargissons les observations et vérifions non pas les changements individuels, mais les mouvements moyens.
Au lieu d'une seule barre, nous prenons la variation de prix moyenne sur un intervalle historique de 1 à 23 barres. Cela lisse les fluctuations aléatoires et nous permet d'isoler la composante la plus stable du mouvement. De même, nous établissons la variation moyenne future du prix sur l'horizon de 1 à 9 barres. Nous passons ainsi d'observations ponctuelles à des signaux agrégés.
L'implémentation est clairement divisée en deux blocs. Le premier calcule les moyennes mobiles des variations passées.
# Add rolling mean features for the previous and future moves for period in range(2, 24, 1): data_matrix[f'last_mean_{period:02d}'] = data_matrix['last'].rolling(window=period).mean()
Le second – pour les changements futurs, avec un décalage obligatoire afin d'empêcher les fuites d'informations du futur vers le passé. C'est un point crucial : sans lui, l'analyse perd tout son sens.
for period in range(2, 10, 1): data_matrix[f'next_mean_{period}'] = data_matrix['next'].rolling(window=period).mean().shift(-(period-1))
Après calcul, les lignes comportant des valeurs manquantes sont supprimées – une conséquence inévitable des opérations de fenêtrage.
# Remove rows with missing values created by rolling calculations data_matrix.dropna(inplace=True)
Ensuite, la matrice de corrélation de Pearson est construite et la sous-matrice nécessaire en est extraite : la dépendance du futur par rapport au passé. Voici la réponse à la question principale : le mouvement moyen a-t-il un pouvoir prédictif ?
correlation_matrix = data_matrix.corr('pearson') # Match columns that begin with "next" reg = r'^next.*$' selected_cols = correlation_matrix.filter(regex=reg).columns remaining_rows = correlation_matrix.index.difference(selected_cols) correlation_matrix = correlation_matrix.loc[remaining_rows, selected_cols]
La visualisation au moyen de Seaborn sous forme de carte thermique est particulièrement appropriée ici. Cela nous permet d'évaluer rapidement la structure des dépendances sur l'ensemble de la grille de paramètres.
plt.figure(figsize=(12, 7)) plt.subplots_adjust(left=0.15, right=1, bottom=0.16, top=0.95) sns.heatmap(correlation_matrix, annot=True, cmap='coolwarm') plt.title('Correlation Means Last to Next Bars') plt.savefig('mean_to_bar.png') plt.show()
D'un point de vue méthodologique, il s'agit déjà d'une expérience plus aboutie. Nous nous éloignons du test naïf bar-to-bar pour passer à l'analyse des effets agrégés. Si le marché présente effectivement de faibles régularités, c'est à ce niveau qu'elles commencent à apparaître.

Le résultat obtenu semble plus intéressant, mais la conclusion générale reste réservée. Nous observons toujours des valeurs de corrélation négatives. Mais elles sont désormais structurées. Dans la région centrale de la matrice (fenêtres de 8 à 14 barres dans l'historique et de 5 à 8 barres dans l'horizon), l'effet s'intensifie et atteint les valeurs de −0,02…−0,03. Il s'agit d'un signal de retour à la moyenne faible mais cohérent.
La logique est assez claire. Si le marché évolue dans une direction depuis un certain temps, il est plus probable que nous observions une correction partielle dans les barres suivantes. Cependant, l'effet n'est pas linéaire :
- sur des fenêtres courtes, le signal se noie dans le bruit ;
- sur les fenêtres trop longues, il devient flou et perd de sa force ;
- le maximum se manifeste dans les intervalles moyens.
Le coin inférieur droit de la matrice mérite d'être mentionné séparément. Dans ce coin, la corrélation tend vers zéro et devient même positive par endroits. C’est un signe classique de perte de pouvoir prédictif en cas de lissage excessif : le signal disparaît avec la variabilité des données.
La visualisation au moyen de Seaborn met assez bien en évidence ici la structure de la dépendance. L'image s'avère être assez significative.
La conclusion est simple : le marché ne présente pas une forte inertie, mais il montre un faible effet de retour à la moyenne. Cela ne fournit pas une stratégie toute faite, mais cela définit la direction à suivre.
Étant donné la faible valeur des corrélations obtenues, un tel signal ne peut être directement transposé en trading dans un cadre linéaire. Mais c'est là que commence la partie la plus intéressante du travail. Si une relation simple est faible, nous pouvons essayer de la renforcer par une combinaison de variables, c'est-à-dire passer à un modèle.
La tâche principale consiste à constituer un espace de variables informatif. Une seule série de prix ne suffit pas. Par conséquent, la prochaine étape consiste à rechercher des variables supplémentaires permettant de saisir la structure cachée du marché. Il est logique d'utiliser les indicateurs techniques classiques comme ensemble de base. Il s'agit d'un outil éprouvé qui, malgré sa simplicité, fournit souvent des signaux utiles.
Ensuite, nous appliquons la même approche rigoureuse : la vérification par corrélation. Nous évaluons la relation entre l'évolution future des prix et les valeurs de divers indicateurs. Dans ce cas, les paramètres des indicateurs varient dans la boucle. Cela nous permet de couvrir immédiatement un large éventail de configurations et de voir où le signal est le plus fort.
D'un point de vue pratique, le processus ressemble à l'énumération des paramètres suivie d'un filtrage. Mais en substance, il s'agit de la formation et de la sélection d'un espace de variables. La corrélation sert ici d'outil de diagnostic. Nous mesurons quelles transformations de données sont associées à un mouvement futur. Il s'agit d'un niveau de réflexion fondamentalement différent : d'abord, comprendre la structure, puis en extraire le profit.
Dans le code, cette approche est mise en œuvre de manière assez systématique. D'une part, on utilise des dérivés simples du prix : les moyennes mobiles, leurs premières et secondes différences, les écarts par rapport à la valeur actuelle. Il s'agit d'une tentative pour saisir la dynamique locale et l'accélération du marché.
# Recreate the base matrix for indicator engineering data_matrix = pd.DataFrame(diff, columns=['last', 'next']) # Add 11-period previous move averages and derived momentum features data_matrix[f'last_mean_11'] = data_matrix['last'].rolling(window=11).mean() data_matrix[f'Dlast_mean_11'] = data_matrix[f'last_mean_11'].diff() data_matrix[f'DDlast_mean_11'] = data_matrix[f'Dlast_mean_11'].diff() # Feature representing the gap between the rolling mean and the current move data_matrix[f'last_last_11'] = data_matrix[f'last_mean_11'] - data_matrix['last'] data_matrix[f'Dlast_last_11'] = data_matrix[f'last_last_11'].diff() data_matrix[f'DDlast_last_11'] = data_matrix[f'Dlast_last_11'].diff() # Add short-term future sum targets for the next bars for period in range(2, 10, 1): data_matrix[f'next_{period}'] = data_matrix['next'].rolling(window=period).sum().shift(-(period-1))
Par ailleurs, des indicateurs classiques de la bibliothèque TA sont ajoutés : SMA, RSI, MACD, et ce, dans plusieurs paramétrages simultanément. Cette couverture nous empêche de choisir intuitivement la bonne période, mais permet plutôt d’observer le comportement sur toute la plage de paramètres.
# Build additional technical indicators using the close price series close = pd.DataFrame(close[:-1], columns=['close']) indicator_cols = {} for period in [4, 8, 12, 24, 36, 48]: sma = ta.trend.sma_indicator(close['close'], window=period, fillna=True) dsma = sma.diff() ddsma = dsma.diff() rsi = ta.momentum.rsi(close['close'], window=period, fillna=True) drsi = rsi.diff() ddrsi = drsi.diff() macd = ta.trend.MACD( close['close'], window_slow=2 * period, window_fast=period, window_sign=period * 3 // 4, fillna=True, ) macd_main = macd.macd() dmacd = macd_main.diff() ddmacd = dmacd.diff() macd_diff = macd.macd_diff() dmacd_diff = macd_diff.diff() ddmacd_diff = dmacd_diff.diff() macd_signal = macd.macd_signal() dmacd_signal = macd_signal.diff() ddmacd_signal = dmacd_signal.diff() macd_sig_main = macd_signal - macd_main dmacd_sig_main = macd_sig_main.diff() ddmacd_sig_main = dmacd_sig_main.diff() indicator_cols[f'SMA_{period:02d}'] = sma indicator_cols[f'DSMA_{period:02d}'] = dsma indicator_cols[f'DDSMA_{period:02d}'] = ddsma indicator_cols[f'RSI_{period:02d}'] = rsi indicator_cols[f'DRSI_{period:02d}'] = drsi indicator_cols[f'DDRSI_{period:02d}'] = ddrsi indicator_cols[f'MACD_{period:02d},{2*period:02d},{period*3//4:02d}'] = macd_main indicator_cols[f'DMACD_{period:02d},{2*period:02d},{period*3//4:02d}'] = dmacd indicator_cols[f'DDMACD_{period:02d},{2*period:02d},{period*3//4:02d}'] = ddmacd indicator_cols[f'MACD_DIFF_{period:02d},{2*period:02d},{period*3//4:02d}'] = macd_diff indicator_cols[f'DMACD_DIFF_{period:02d},{2*period:02d},{period*3//4:02d}'] = dmacd_diff indicator_cols[f'DDMACD_DIFF_{period:02d},{2*period:02d},{period*3//4:02d}'] = ddmacd_diff indicator_cols[f'MACD_SIGNAL_{period:02d},{2*period:02d},{period*3//4:02d}'] = macd_signal indicator_cols[f'DMACD_SIGNAL_{period:02d},{2*period:02d},{period*3//4:02d}'] = dmacd_signal indicator_cols[f'DDMACD_SIGNAL_{period:02d},{2*period:02d},{period*3//4:02d}'] = ddmacd_signal indicator_cols[f'MACD_Sig_Main{period:02d},{2*period:02d},{period*3//4:02d}'] = macd_sig_main indicator_cols[f'DMACD_Sig_Main{period:02d},{2*period:02d},{period*3//4:02d}'] = dmacd_sig_main indicator_cols[f'DDMACD_Sig_Main{period:02d},{2*period:02d},{period*3//4:02d}'] = ddmacd_sig_main # Append all indicator columns to the feature matrix in one operation # This avoids repeated DataFrame assignment and keeps the DataFrame compact data_matrix = pd.concat([data_matrix, pd.DataFrame(indicator_cols)], axis=1) # Remove any rows with NaN values created by indicator calculations data_matrix.dropna(inplace=True)
L'ajout de dérivées du second ordre est particulièrement révélateur (diff et diff de diff). Il s'agit déjà d'une tentative de saisir le changement dans le signal lui-même – une transition vers l'analyse de l'accélération et du ralentissement des mouvements du marché. Dans les séries financières, ces effets s'avèrent souvent plus informatifs que les niveaux eux-mêmes.
Ensuite, un filtrage est appliqué. De la matrice de corrélation complète, seules les variables dont la relation maximale avec la variable cible dépasse le seuil spécifié (dans ce cas, 0,02) restent. C'est un point important. Nous éliminons consciemment les dépendances faibles et instables, ne conservant que celles qui restent présentes dans les données, au moins dans la mesure minimale possible.
correlation_matrix = data_matrix.corr('pearson') selected_cols = correlation_matrix.filter(regex=reg).columns remaining_rows = correlation_matrix.index.difference(selected_cols) correlation_matrix = correlation_matrix.loc[remaining_rows, selected_cols] # Delete rows with low correlations correlation_matrix = correlation_matrix[correlation_matrix.abs().max(axis=1) >= 0.02]
La visualisation via Seaborn achève le processus :
plt.figure(figsize=(12, 7)) plt.subplots_adjust(left=0.2, right=1, bottom=0.05, top=0.95) sns.heatmap(correlation_matrix, annot=True, cmap='coolwarm') plt.title('Correlation Indicators to Next Bars') plt.savefig('trend_to_bar.png') plt.show()
La carte thermique ne ressemble plus à un bruit chaotique, mais se transforme en une cartographie des signaux. Il est clair quels groupes d'indicateurs commencent à réagir aux mouvements futurs, à quels paramètres cette réaction s'amplifie et où elle disparaît complètement.

C’est à ce stade que l’ensemble des hypothèses commence à se structurer. Nous passons d'une sélection aléatoire d'indicateurs à une formation consciente des variables. Il ne s'agit pas encore d'un modèle, mais plutôt de sa structure. La prochaine étape consiste à assembler ces signaux faibles et disparates en un système unique capable d'extraire une dépendance stable là où, individuellement, elle est presque invisible.
4. Construction et entraînement du modèle
Dans l'étape précédente, nous avons obtenu une carte de corrélation et sélectionné les variables présentant les valeurs maximales de corrélation directe et inverse. C'est un point fondamental : une corrélation négative correspond au même signal, mais avec un signe opposé. En ce qui concerne le modèle, il ne s'agit pas d'une limitation, mais d'une information supplémentaire.
Nous passerons ensuite à l’entraînement. L'écosystème Scikit-Learn offre un large éventail d'algorithmes, allant des modèles linéaires aux ensembles. L'article « Modèles de régression de la bibliothèque Scikit-learn et leur export vers ONNX » fournit une comparaison de 55 modèles de régression. Mais pour résoudre un problème pratique, il est judicieux de se concentrer sur des algorithmes robustes et éprouvés. Dans ce cas, nous utiliserons RandomForestRegressor.
L’algorithme « Classic Forest » est un compromis entre simplicité et expressivité. Il fonctionne bien avec les dépendances non linéaires, est robuste au bruit et ne nécessite pas de normalisation agressive des données. C’est exactement ce dont on a besoin lors de la première étape de la modélisation.
La prochaine étape clé consiste à sélectionner les hyper-paramètres. Utilisons l'énumération directe par deux paramètres : le nombre d'arbres (n_estimators) et la profondeur de l'arbre (max_depth). Il s'agit d'un choix raisonnable : le premier paramètre est responsable de la force de l'ensemble, et le second se concentre sur le degré d'immersion d'un arbre individuel dans les données et sur le contrôle du sur-apprentissage.
À cette fin, créons un nouveau script. L'algorithme de connexion à MetaTrader 5 et de chargement des données de prix historiques reste le même, nous omettrons donc sa description.
Après la réception des données historiques, la formation de l'espace des variables commence. Une matrice de base est construite : différences de prix, leurs versions lissées et les variables dérivées.
macd_settings = [(8,16,6),(12,24,9),(36,72,27),(48,96,36)] features = [] # Build the base feature matrix from close price changes close = pd.DataFrame(rates_frame['close'][:-1].to_numpy(dtype=float), columns=['close']) diff = rates_frame['close'].diff().to_numpy(dtype=float) # Pair consecutive differences into 'last' and 'next' columns diff = np.column_stack((diff[:-1], diff[1:])) data_matrix = pd.DataFrame(diff, columns=['last', 'next']) features.append('last') # Add to features list for later use # Add a 11-period rolling mean of the previous bar move data_matrix['last_11'] = data_matrix['last'].rolling(window=11).mean() features.append('last_11') # Add to features list for later use # Add the difference between the rolling mean and current bar move data_matrix['last_last_11'] = data_matrix['last_11'] - data_matrix['last'] features.append('last_last_11') # Add to features list for later use
Ensuite, on ajoute les indicateurs techniques : SMA et un ensemble de configurations MACD.
# Add a 12-period simple moving average as a technical feature data_matrix['SMA_12'] = ta.trend.sma_indicator(close['close'], window=12, fillna=True) features.append('SMA_12') # Add to features list for later use # Add MACD-based technical indicators for the selected parameter sets for fast, slow, sign in macd_settings: macd = ta.trend.MACD( close['close'], window_slow=slow, window_fast=fast, window_sign=sign, fillna=True, ) macd_main = macd.macd() dmacd = macd_main.diff() macd_signal = macd.macd_signal() dmacd_signal = macd_signal.diff() macd_sig_main = macd_signal - macd_main sufix = f"{fast:02d},{slow:02d},{sign:02d}" data_matrix[f'MACD_MAIN_{sufix}'] = macd_main features.append(f'MACD_MAIN_{sufix}') # Add to features list for later use data_matrix[f'DMACD_MAIN_{sufix}'] = dmacd features.append(f'DMACD_MAIN_{sufix}') # Add to features list for later use data_matrix[f'MACD_SIGNAL_{sufix}'] = macd_signal features.append(f'MACD_SIGNAL_{sufix}') # Add to features list for later use data_matrix[f'DMACD_SIGNAL_{sufix}'] = dmacd_signal features.append(f'DMACD_SIGNAL_{sufix}') # Add to features list for later use data_matrix[f'MACD_Sig_Main_{sufix}'] = macd_sig_main features.append(f'MACD_Sig_Main_{sufix}') # Add to features list for later use data_matrix[f'DMACD_Sig_Main_{sufix}'] = macd_sig_main.diff() features.append(f'DMACD_Sig_Main_{sufix}') # Add to features list for later use
Toutes les variables sont regroupées dans une structure de données unique, et leurs noms sont définis dans une liste distincte de variables. Cela simplifie les travaux ultérieurs et élimine la perte accidentelle de variables.
À la même étape, la variable cible est formée - le mouvement total du prix sur un horizon donné (next_9).
# Add a 9-period future return target for the next bars data_matrix['next_9'] = data_matrix['next'].rolling(window=9).sum().shift(-8)
Le problème est donc formalisé comme une régression : prédire, à partir des indicateurs actuels, les variations de prix futures.
L'étape suivante consiste à nettoyer les données. Après application de fenêtres glissantes et de différentiation, des valeurs manquantes apparaissent inévitablement. Elles sont supprimées afin de garantir un entraînement correct du modèle.
data_matrix.dropna(inplace=True)
Ensuite, les données sont divisées en fonction du temps : les 90% premières sont utilisées pour l’entraînement, les 10% restantes pour les tests. C'est un point fondamental. Contrairement aux problèmes d'apprentissage automatique classiques, le mélange aléatoire des données n'est pas autorisé ici. Nous respectons scrupuleusement la chronologie, simulant un processus réel : d'abord le passé, puis le futur.
# ===== 1) Data preparation ===== # Copy the raw feature matrix (preserves original data for later reference) df = data_matrix.copy() # Keep only features that are actually present in the DataFrame features = [c for c in features if c in data_matrix.columns] df = df[features + ["next_9"]] X = df[features] y = df["next_9"] # ===== 2) Time-based split ===== split_idx = int(len(X) * 0.9) X_train = X.iloc[:split_idx] X_test = X.iloc[split_idx:] y_train = y.iloc[:split_idx] y_test = y.iloc[split_idx:]
Ensuite, un cycle d'énumération des hyper-paramètres du modèle RandomForestRegressor est lancé. Pour chaque combinaison de paramètres, un cycle complet est effectué :
- le modèle est entraîné sur un échantillon d'entraînement :
# ===== 3) Model ===== results = [] for est in range(60, 111, 5): for dep in range(2, 14, 1): print(f"\n=== Estimators: {est}, Max Depth: {dep} ===") model = RandomForestRegressor( n_estimators = est, max_depth = dep, max_leaf_nodes = None, min_samples_split = 6, min_samples_leaf = 3, bootstrap = True, random_state = 42, n_jobs = -1 ) model.fit(X_train, y_train)
# ===== 4) Evaluation ===== pred_train=np.nan_to_num(model.predict(X_train), nan=0.0, posinf=0.0, neginf=0.0) pred_test = np.nan_to_num(model.predict(X_test), nan=0.0, posinf=0.0, neginf=0.0)
pt_corr = np.corrcoef(pred_test, y_test)[0, 1] results.append((est, dep, pt_corr)) print("Train R2:", round(r2_score(y_train, pred_train), 6)) print("Test R2:", round(r2_score(y_test, pred_test), 6)) print("Test MAE:", round(mean_absolute_error(y_test, pred_test), 8)) print("Pred/Target corr:", round(pt_corr, 6))
L'indicateur clé est la corrélation entre la prévision et la valeur réelle sur l'échantillon de test. C’est cela qui reflète la capacité du modèle à capter la direction du mouvement. Le R² et le MAE sont également calculés pour contrôler la qualité globale de l'approximation et le niveau d'erreur.
Tous les résultats sont stockés dans un tableau, dont chaque ligne correspond à une combinaison spécifique d'hyper-paramètres. Ensuite, ce tableau est converti sous forme matricielle, ce qui nous permet de visualiser la dépendance de la qualité du modèle aux paramètres.
# --- results -> DataFrame --- df_results = pd.DataFrame( results, columns=["Estimators", "Max Depth", "Test Correlation"] ) # --- pivot table --- heatmap_data = df_results.pivot( index="Estimators", columns="Max Depth", values="Test Correlation" ).sort_index()
La dernière étape consiste en la visualisation au moyen de Seaborn. La carte thermique indique clairement dans quelle zone de paramètres les meilleurs résultats sont obtenus. Cela nous permet non seulement de sélectionner la configuration optimale, mais aussi d'évaluer la stabilité du modèle — c'est-à-dire dans quelle mesure la qualité change avec de petites modifications des paramètres.
# --- heatmap --- plt.figure(figsize=(14, 10)) sns.heatmap( heatmap_data, annot=True, fmt=".4f", cmap="coolwarm", linewidths=0.5, cbar_kws={"label": "Test Correlation"} ) plt.title("Heatmap of Test Correlation by n_estimators and max_leaf_nodes") plt.xlabel("Max Nodes") plt.ylabel("Estimators") plt.tight_layout() plt.show()

Un point important ressort ici : le modèle ne crée pas un signal à partir de rien. Il ne fait qu'agréger les dépendances faibles identifiées précédemment. S’il n’y a pas même un soupçon de structure au stade de l’analyse, aucun modèle ne pourra sauver la situation. Mais s'il y a un signal, même faible, l'ensemble est capable de l'amplifier et de le rendre exploitable.
Sur le graphique de la première itération de sélection des hyper-paramètres, la région avec max_depth = 10 est clairement visible. Cette valeur démontre l'équilibre le plus stable entre la capacité du modèle à saisir les dépendances et à contrôler le sur-apprentissage. En fait, c'est dans ce mode de fonctionnement que le modèle n'est plus primitif, mais sans encore commencer à sur-ajuster au bruit.
La logique se développe alors naturellement. Après avoir défini max_depth, nous passons au réglage de la structure des arbres via max_leaf_nodes. En parallèle, nous réduisons la plage en fonction du nombre d'arbres (n_estimators), ne laissant que la zone où des résultats stables ont été observés précédemment. Cela nous permet d'augmenter la résolution de la recherche : le nombre d'étapes de recherche est réduit et l'attention se concentre sur la zone des paramètres réellement significative.
Cette approche rappelle la procédure classique d'optimisation locale. On détermine d'abord une zone approximative du maximum, puis un réglage fin à l'intérieur de celle-ci. De ce fait, nous évitons de gaspiller des ressources de calcul sur des configurations manifestement faibles et parvenons rapidement à une combinaison stable de paramètres.
Les modifications apportées au code sont ciblées : la plage de recherche est spécifiée et le deuxième paramètre est modifié. L'architecture du script reste inchangée.

Après la sélection, nous fixons les hyper-paramètres optimaux et procédons à l'entraînement final du modèle. Structurellement, rien ne change : les boucles d’itération sont supprimées du script et les valeurs des paramètres sont spécifiées directement lors de l’initialisation de RandomForestRegressor. Toute autre logique — préparation des données, division des échantillons, entraînement et validation de base — reste inchangée.
Ensuite, un point plus subtil mais fondamental apparaît. Le modèle n'est pas aussi fiable pour toutes ses prévisions. Dans certains cas, il donne un signal fort, dans d'autres les valeurs sont proches de zéro et se situent en fait dans la zone d'incertitude. Si toutes les prévisions sont traitées de la même manière, la stratégie finit inévitablement par trader du bruit.
Ceci conduit à une hypothèse naturelle : ignorer les signaux faibles et ne travailler que là où le modèle démontre une confiance suffisante ou lorsque le mouvement attendu couvre les coûts. Il s'agit déjà d'une transition du modèle de prévision au modèle de filtre de trading.
Le code met en œuvre cette idée de manière élégante et sans complexité inutile. Sur la base de l'échantillon d'entraînement, des seuils fondés sur la valeur absolue des prévisions sont calculés.
# ===== 5) Simple PnL prototype ===== # Calculate strategy metrics for a vector of thresholds without an explicit loop percentiles = np.arange(10, 100, 5) thresholds = np.percentile(np.abs(pred_train), percentiles)
C’est un point important : les seuils sont déterminés lors de l’entraînement et appliqués aux tests, ce qui préserve la validité de l’expérience.
Ensuite, une matrice de prévisions et de seuils correspondants est formée. Pour chaque niveau, un masque est calculé, indiquant quels signaux passent le filtre. La position est déterminée par le signe de la prévision, en tenant compte de la corrélation générale, ce qui permet à la direction des positions d'être cohérente avec la nature du modèle.
# Build a matrix where each column repeats the test predictions pred_matrix = np.tile(pred_test[:, None], (1, thresholds.size)) threshold_matrix = thresholds[None, :] # Generate a mask per threshold and compute sign positions mask = np.abs(pred_matrix) >= threshold_matrix position = np.sign(pred_matrix) * np.sign(pt_corr) * mask.astype(float)
La rentabilité se calcule ensuite comme le produit de la position et du mouvement effectif. Les coûts fixes (spread / commission) sont soustraits et la courbe de capital cumulée est construite.
# Broadcast y_check to match the threshold matrix shape y_check_matrix = np.tile(y_check.values[:, None], (1, thresholds.size)) # Subtracting swap cost from the target to get a more realistic PnL estimate strategy_ret = position * y_check_matrix - np.abs(position)*(0.00021) # Compute equity curves for each threshold column equity = np.cumsum(strategy_ret, axis=0)
Les résultats sont regroupés dans un tableau :
- final_equity — rentabilité finale ;
- mean_return — résultat moyen des transactions ;
- win_rate — proportion de positions gagnantes.
# Aggregate results into a DataFrame results = pd.DataFrame({ 'percentile': percentiles, 'threshold': thresholds, 'final_equity': equity[-1, :], 'mean_return': np.sum(strategy_ret, axis=0)/(np.sum(strategy_ret != 0, axis=0)+1e-9), 'win_rate': np.sum(strategy_ret > 0, axis=0)/(np.sum(strategy_ret != 0, axis=0)+1e-9) }) print(results.to_string(index=False, float_format='%.8f'))
D'un point de vue pratique, il ne s'agit plus seulement d'une évaluation de modèle, mais des prémices d'un système de trading. Nous vérifions non seulement la précision des prévisions, mais aussi leur traduction en performance selon différents seuils de sélection.

5. Transition vers ONNX
Nous avons entraîné le modèle. L'étape logique suivante consiste à retirer la personne du processus décisionnel. Le trading manuel basé sur des signaux de modèles est presque toujours inférieur au trading automatisé : il n’y a pas de continuité, la vitesse de réaction est perdue et le facteur psychologique s’ajoute. Dans la réalité professionnelle, cela se traduit par des distorsions systémiques : entrées manquées, sorties prématurées, méfiance envers son propre modèle.
La plateforme MetaTrader 5 propose deux voies d'automatisation. La première consiste à lancer le script Python qui envoie directement les ordres de trading. La seconde consiste à transférer le modèle au format ONNX en vue de son utilisation ultérieure dans l'EA MQL5. En pratique, la seconde option semble plus aboutie.
Le format ONNX résout plusieurs problèmes à la fois. Le modèle est fixé sous une forme compacte et indépendante. Il peut être facilement transféré d'un ordinateur à l'autre – tout ce dont vous avez besoin, c'est du terminal lui-même. Il devient possible d'effectuer des tests complets dans le Testeur de Stratégie. Et il n'y a aucune perte de performance : le terminal prend en charge l'accélération matérielle, notamment l’accélération GPU via CUDA, ce qui est particulièrement important lors de l'utilisation de modèles d'ensemble.
La conversion est relativement simple. Tout d'abord, on décrit l'entrée du modèle : la dimension de l'espace des variables.
# Number of features used for model input n_features = X_train.shape[1] # Describe the model input shape for ONNX conversion initial_type = [("float_input", FloatTensorType([None, n_features]))]
Ensuite, le modèle entraîné à partir de Scikit-Learn est transformé en ONNX via le convertisseur correspondant et enregistré sur le disque.
# Convert the trained sklearn model to ONNX format onnx_model = convert_sklearn(model, initial_types=initial_type) # Save the ONNX model to disk with open(onnx_model_path, "wb") as f: f.write(onnx_model.SerializeToString())
Vient ensuite l'étape de validation obligatoire. Le modèle est chargé via ONNX Runtime, et les mêmes données sont utilisées pour calculer les valeurs prédites.
# Load the ONNX model for inference sess = rt.InferenceSession(onnx_model_path) input_name = sess.get_inputs()[0].name # ONNX runtime expects float32 input arrays X_test_np = X_test.astype(np.float32).values onnx_preds = sess.run(None, { input_name: X_test_np })[0].ravel()
Ils sont ensuite comparés aux résultats produits par le modèle Sklearn.
# Compare ONNX predictions with sklearn predictions sk_preds = model.predict(X_test) print("Correlation:", np.corrcoef(sk_preds, onnx_preds)[0, 1]) print("Max diff:", np.max(np.abs(sk_preds - onnx_preds)))
Il y a deux critères clés ici :
- la corrélation entre les prévisions devrait tendre vers 1 ;
- l'écart maximal doit être négligeable.
Si ces conditions sont remplies, nous pouvons supposer que le transfert a été réussi et que le modèle est prêt à être intégré au système de trading.
D'un point de vue pratique, il s'agit de la transition finale entre l'environnement de recherche Python et son utilisation pratique. Le modèle cesse d'être une expérience et devient partie intégrante de l'infrastructure – autonome, reproductible et adapté aux tests et au trading réel.
6. Tests dans le Testeur de Stratégie
Une fois le travail effectué côté Python, la logique est transférée vers l'environnement d'exécution MetaTrader 5. Ici, le modèle cesse d'être un outil de recherche et devient une partie intégrante d'un algorithme de trading. Il est important que la structure du code suive le flux d'opérations déjà familier : initialisation → préparation des données → prévision → décision de trading.
L'initialisation est effectuée dans la méthode OnInit. À ce stade, le modèle ONNX de la ressource est chargé et l'environnement d'exécution est créé via OnnxCreateFromBuffer.
int OnInit() { //--- if(!Symb.Name("EURUSD_i")) return INIT_FAILED; Symb.Refresh(); //--- if(!Trade.SetTypeFillingBySymbol(Symb.Name())) return INIT_FAILED; //--- load models onnx = OnnxCreateFromBuffer(model, ONNX_DEFAULT); if(onnx == INVALID_HANDLE) { Print("OnnxCreateFromBuffer error ", GetLastError()); return INIT_FAILED; } const ulong input_state[] = {1, Inputs.Size()}; if(!OnnxSetInputShape(onnx, 0, input_state)) { Print("OnnxSetInputShape error ", GetLastError()); OnnxRelease(onnx); return INIT_FAILED; } const ulong output_forecast[] = {1, Forecast.Size()}; if(!OnnxSetOutputShape(onnx, 0, output_forecast)) { Print("OnnxSetOutputShape error ", GetLastError()); OnnxRelease(onnx); return INIT_FAILED; }
Ensuite, les formes d'entrée et de sortie sont explicitement définies — ceci est crucial, car le modèle attend un nombre de variables strictement fixe. Une erreur à ce stade entraînera des prédictions incorrectes.
L'indicateur SMA et le MACD, configurés avec les mêmes paramètres que ceux utilisés lors de l'entraînement, sont initialisés en parallèle.
//--- Indicators if(!ciSMA.Create(Symb.Name(), TimeFrame, 12, 0, MODE_SMA, PRICE_CLOSE)) { Print("SMA create error ", GetLastError()); OnnxRelease(onnx); return INIT_FAILED; } ciSMA.BufferResize(2); for(uint i = 0; i < ciMACD.Size(); i++) { if(!ciMACD[i].Create(Symb.Name(), TimeFrame, int(macd_set[i, 0]), int(macd_set[i, 1]), int(macd_set[i, 2]), PRICE_CLOSE)) { PrintFormat("MACD %d create error %d", i, GetLastError()); OnnxRelease(onnx); return INIT_FAILED; } ciMACD[i].BufferResize(4); } //--- return(INIT_SUCCEEDED); }
Il s'agit d'un point fondamental : les variables de MQL5 doivent être identiques à celles qui ont été fournies au modèle lors de l'entraînement. Toute divergence anéantit la capacité de prédiction.
La logique principale est concentrée dans la méthode OnTick, mais avec le filtre IsNewBar pour l'événement d'ouverture d'une nouvelle barre. Cela empêche le modèle d'être recalculé à chaque tick et synchronise les calculs avec la période.
void OnTick() { //--- if(!IsNewBar()) return;
Vient ensuite le bloc consacré à la comptabilisation des positions actuelles – une simple agrégation des volumes et des profits par direction. Cela est nécessaire pour contrôler les transactions déjà ouvertes.
double buy_value = 0, sell_value = 0, buy_profit = 0, sell_profit = 0; int total = PositionsTotal(); for(int i = 0; i < total; i++) { if(PositionGetSymbol(i) != Symb.Name()) continue; double profit = PositionGetDouble(POSITION_PROFIT); switch((int)PositionGetInteger(POSITION_TYPE)) { case POSITION_TYPE_BUY: buy_value += PositionGetDouble(POSITION_VOLUME); buy_profit += profit; break; case POSITION_TYPE_SELL: sell_value += PositionGetDouble(POSITION_VOLUME); sell_profit += profit; break; } }
Le vecteur des entrées est ensuite formé. En résumé, l’ingénierie des variables en Python est reproduite manuellement ici :
- variables de base :
//--- prepare input data ciSMA.Refresh(); for(uint i = 0; i < ciMACD.Size(); i++) ciMACD[i].Refresh(); if(!Rates.CopyRates(Symb.Name(), TimeFrame, COPY_RATES_CLOSE, 1, 12)) { Print("CopyRates error ", GetLastError()); return; } Inputs[0] = float(Rates[11] - Rates[10]); Inputs[1] = float(Rates[11] - Rates[0]) / 11; Inputs[2] = float(Inputs[1] - Inputs[0]);
Inputs[3] = float(ciSMA.Main(1));
for(uint i = 0; i < ciMACD.Size(); i++) { Inputs[4 + i * 6] = float(ciMACD[i].Main(1)); Inputs[5 + i * 6] = float(Inputs[4 + i * 6] - ciMACD[i].Main(2)); Inputs[6 + i * 6] = float(ciMACD[i].Signal(1)); Inputs[7 + i * 6] = float(Inputs[6 + i * 6] - ciMACD[i].Signal(2)); Inputs[8 + i * 6] = Inputs[6 + i * 6] - Inputs[4 + i * 6]; Inputs[9 + i * 6] = Inputs[7 + i * 6] - Inputs[5 + i * 6]; }
Veuillez noter l'indexation : chaque variable occupe une place strictement définie. Il s'agit d'un contrat entre le modèle et son exécution. Si l'ordre n'est pas respecté, le modèle commence à fonctionner avec des entrées déformées.
Une fois les variables préparées, OnnxRun est appelé :
//--- run the inference if(!OnnxRun(onnx, ONNX_LOGLEVEL_INFO, Inputs, Forecast)) { Print("OnnxRun error ", GetLastError()); return; }
Le résultat est une prévision — l'évolution attendue des prix. Vient ensuite la partie pratique : l'interprétation du signal.
Le code utilise une logique simple mais efficace :
- Une valeur seuil est introduite pour éliminer les signaux faibles. Nous nous basons sur les résultats de l’entraînement.
- On tient compte du sens de la corrélation, ce qui permet d'inverser le modèle si nécessaire.
- Si les prévisions dépassent le seuil, une position est ouverte. Si le signal disparaît, la position est fermée.
Symb.Refresh(); Symb.RefreshRates(); double min_lot = Symb.LotsMin(); double step_lot = Symb.LotsStep(); double stops = (MathMax(Symb.StopsLevel(), 1) + Symb.Spread()) * Symb.Point(); //--- buy control if(Forecast[0]*direction >= threshold) { double buy_lot = min_lot; if(buy_value <= 0) Trade.Buy(buy_lot, Symb.Name(), Symb.Ask(), 0, 0); } else { if(buy_value > 0) CloseByDirection(POSITION_TYPE_BUY); } //--- sell control if(Forecast[0]*direction <= -threshold) { double sell_lot = min_lot; if(sell_value <= 0) Trade.Sell(sell_lot, Symb.Name(), Symb.Bid(), 0, 0); } else { if(sell_value > 0) CloseByDirection(POSITION_TYPE_SELL); } }
Le modèle est donc utilisé comme filtre de mouvement directionnel. Il s'agit d'une distinction importante : nous ne traitons pas toutes les prévisions comme des signaux de trading, mais seulement celles qui dépassent le seuil de force du signal.
L’EA obtenu fait ensuite l’objet d’une vérification clé : un test dans le Testeur de Stratégie MetaTrader 5 sur des données historiques du premier trimestre 2026. Il ne s'agit plus d'une évaluation abstraite du modèle, mais d'un scénario de mise en œuvre proche de la réalité.
Ce format de test est fondamental. Alors qu'au stade Python nous évaluions le modèle à l'aide de métriques, ici c'est l'ensemble du système qui est testé, de la génération des variables à la logique d'ouverture et de fermeture des positions. En fait, la stratégie est testée pour la première fois dans des conditions ressemblant le plus possible aux conditions réelles.

La phase de test boucle la boucle de développement : de l’hypothèse et de l’analyse des données à un modèle, puis à l’automatisation et à la vérification sur des données historiques. C’est à ce moment-là que l’on voit clairement si de faibles relations statistiques ont été transformées avec succès en un outil de trading pratique.
Cependant, l'intégration du modèle ONNX dans un EA n'est pas le seul cas d'utilisation. Pour les adeptes du trading manuel, MetaTrader 5 offre la possibilité d'intégrer un modèle directement dans un indicateur personnalisé. Dans ce cas, le modèle ne prend pas de décisions à la place du trader, mais agit comme un outil d’analyse, générant des signaux que l'utilisateur interprète lui-même.
D'un point de vue technique, il n'y a pratiquement aucune différence. Les mécanismes de connexion du modèle ONNX, la préparation des données initiales et l'appel d'inférence sont complètement identiques à l'implémentation dans l'EA. Seul le point d'application change : au lieu d'ouvrir automatiquement des positions, le résultat du modèle est visualisé sur un graphique ou utilisé comme filtre supplémentaire lors de la prise de décision.
Cette approche présente plusieurs avantages concrets. Il permet une combinaison flexible des signaux du modèle avec l'analyse classique et réduit les exigences en matière de fiabilité de l'algorithme — le modèle devient un assistant, et non la seule source de décision.
Conclusion
L'intégration de Python et de MetaTrader 5 crée un cadre complet et éprouvé par l'ingénierie pour le développement de solutions de trading, du concept à la mise en œuvre pratique. Dans cet article, nous avons parcouru ce chemin de manière séquentielle : de la collecte et de l’analyse des données, en passant par les tests d’hypothèses et la construction du modèle, jusqu’à sa mise en œuvre et son test dans un environnement d’exécution réel.
Le principal avantage de cette approche réside dans la séparation des rôles. Python prend en charge la partie recherche : traitement des données, génération de variables, analyse statistique et entraînement du modèle. MetaTrader 5, quant à lui, assure l'exécution : accès aux données de marché, tests de stratégies et infrastructure de trading. Il s'agit d'une combinaison classique de circuit laboratoire - production, où chaque environnement est utilisé conformément à sa finalité.
L'utilisation du format ONNX apporte une valeur ajoutée. Le modèle devient portable, indépendant de l'environnement de développement et prêt à être exécuté sur n'importe quel appareil doté d'un terminal installé. Cela simplifie la mise à l'échelle, accélère les tests et réduit les risques liés à l'incompatibilité entre les environnements.
Programmes utilisés dans l'article
| # | Nom | Type | Description |
|---|---|---|---|
| 1 | Experts\Integration\Integration.mq5 | Expert Advisor | EA de test du modèle dans MetaTrader 5 |
| 2 | Indicators\Integration\Integration.mq5 | Indicateur | Indicateur permettant d'afficher des signaux sur un graphique |
| 3 | Scripts\Integration\load_data.py | Script | Script de chargement des données |
| 4 | Scripts\Integration\look_model_param_rf.py | Script | Script d'énumération des hyper-paramètres |
| 5 | Scripts\Integration\create_model_rf.py | Script | Script pour l'entraînement du modèle et son export au format ONNX |
Traduit du russe par MetaQuotes Ltd.
Article original : https://www.mql5.com/ru/articles/22020
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.
Trading algorithmique sans tâches routinières : analyse rapide des opérations dans MetaTrader 5 avec SQLite
Comment nous avons construit la plateforme de trading la plus puissante propulsée par le machine learning : l'évolution de MQL et de MetaTrader à travers les archives, les forums et les versions
L'Histogramme des prix (Profile du Marché) et son implémentation en MQL5
Optimisation de portefeuille sur le Forex : Intégration de la VaR et de la théorie de Markowitz
- Applications de trading gratuites
- Plus de 8 000 signaux à copier
- Actualités économiques pour explorer les marchés financiers
Vous acceptez la politique du site Web et les conditions d'utilisation
Dans le fichier de script load_data.py, qui se trouve dans l'archive, on trouve les lignes suivantes :
alors que dans l'article lui-même :
C'est un détail, mais je ne l'ai pas remarqué tout de suite lors des tests...
J'ai ensuite dû abandonner la version Python 3.14.3. Je travaille avec Python dans VS. Là-bas, le débogage n'est possible qu'avec la version 3.11.