Python + MetaTrader 5: Schnelles Forschungs-Framework für Daten, Merkmale und Prototypen
Einführung
Python hat sich zu einem der praktischsten Werkzeuge für die Arbeit mit Daten entwickelt. Es bietet eine breite Palette von Bibliotheken, die es uns ermöglichen, schnell statistische Analysen durchzuführen, Hypothesen zu testen und Ergebnisse visuell darzustellen, ohne Zeit und Ressourcen zu verschwenden. Dies ist wichtig für die Lösung von Problemen im Zusammenhang mit den Finanzmärkten: Hier wird nicht nur die Geschwindigkeit der Datenverarbeitung geschätzt, sondern auch die Fähigkeit, schnell von der Analyse zu praktischen Schlussfolgerungen zu gelangen.
MetaTrader 5 bietet eine direkte Integration mit Python, wodurch die Möglichkeiten der praktischen Arbeit mit Marktdaten erheblich erweitert werden. Ein Forscher oder Entwickler kann das vertraute Python-Toolkit verwenden, um Preisdaten zu untersuchen, statistische Modelle zu erstellen und praktische Hypothesen zu testen, ohne die Verbindung zur Handelsplattform zu unterbrechen. Dieser Ansatz macht den Ablauf flexibler und unterstützt einen einheitlichen Zyklus: von den Daten zur Hypothese, von der Hypothese zum Modell und vom Modell zur praktischen Anwendung.

In diesem Artikel werden wir das zeigen:
- wie Python in den MetaTrader 5 integriert wird;
- wie man sie zur Analyse von Finanzdaten und zur Prüfung von Hypothesen einsetzt;
- wie man ein kleines Modell erstellt und trainiert und dann das trainierte Ergebnis mit ONNX auf einen EA überträgt.
Damit können wir von einem Forschungsversuch zur praktischen Umsetzung in einem Handelssystem übergehen.
1. Installation und Verbindung
Bevor Sie mit der Datenanalyse beginnen, ist es notwendig, die Arbeitsumgebung für die gemeinsame Nutzung von Python und MetaTrader 5 konsequent vorzubereiten. Die Aufgabe ist einfach, aber sie erfordert Genauigkeit. Eine korrekte Einrichtung zu Beginn erspart Ihnen später Dutzende von kleineren Problemen.
Zunächst installieren wir das Terminal MetaTrader 5, indem wir das Installationspaket von der offiziellen Website herunterladen.
Als Nächstes benötigen wir die aktuelle Python-Version. Zum Zeitpunkt der Erstellung dieses Artikels ist es die Version 3.14.3. Stellen Sie bei der Installation sicher, dass Sie die Option aktivieren, die Python zur Umgebungsvariablen PATH hinzufügt. Dies ermöglicht das Arbeiten mit dem Interpreter direkt von der Kommandozeile aus, ohne unnötige manuelle Einstellungen.

Der wichtigste Punkt ist die Isolierung der Umgebung. Die Erfahrung zeigt, dass Projekte, die mit Daten und Modellen arbeiten, schnell mit Abhängigkeiten überfrachtet werden. Um Ordnung und Reproduzierbarkeit der Ergebnisse zu gewährleisten, wird für jedes Projekt eine eigene virtuelle Umgebung geschaffen. In Python kann dies mit einem integrierten Tool venv gelöst werden.
Der Ablauf ist wie folgt.
- Öffnen Sie die Befehlsausführungsumgebung. Am einfachsten ist es, wenn Sie Win+R drücken. Geben Sie in dem nun erscheinenden Fenster den Befehl cmd ein und drücken Sie die Eingabetaste. Dadurch wird die Windows-Eingabeaufforderung geöffnet. Sie können PowerShell verwenden, wenn Sie möchten – in diesem Fall gilt das gleiche Prinzip.
- Navigieren Sie zu dem Projektverzeichnis, in dem die Umgebung erstellt werden soll. Dies wird mit dem Standardbefehl durchgeführt:
- Erstellen Sie eine virtuelle Umgebung.
- Installieren Sie das Python-Modul für die Interaktion mit MetaTrader 5.
- Für eine umfassende Analyse von Finanzdaten ist es sinnvoll, sofort einen grundlegenden, aber fast vollwertigen Satz an Bibliotheken einzusetzen. Er umfasst die wichtigsten Aufgaben der Datenverarbeitung, der Modellbildung und der technischen Analyse.
cd /path/to/your/project
python -m venv integration
Und aktivieren Sie es.
integration\Scripts\activate
Von nun an werden alle installierten Pakete innerhalb des aktuellen Projekts isoliert.
pip install MetaTrader5
Installieren Sie zunächst NumPy – die Grundlage für numerische Berechnungen. Dies ist die Grundlage, auf der der gesamte nachfolgende Stapel basiert.
Als Nächstes erfolgt die Einbindung von Pandas – das wichtigste Werkzeug für die Arbeit mit tabellarischen Daten und Zeitreihen, ohne das die Preisanalyse zu einer Qual wird.
Zur Visualisierung verwenden wir eine Kombination aus Matplotlib und Seaborn. Die erste gibt Ihnen die volle Kontrolle über Ihre Diagramme, während die zweite die Erstellung statistisch aussagekräftiger Visualisierungen beschleunigt. Indem sie zusammenarbeiten, ermöglichen sie uns, den Markt zu sehen, anstatt ihn nur zu zählen.
Für Aufgaben des maschinellen Lernens fügen wir Scikit-Learn hinzu – ein bewährtes Tool zur Erstellung und Validierung von Modellen. Es eignet sich gut für frühe Prototypen und grundlegende Strategien.
Für angewandte Marktanalysen binden wir TA ein – eine Bibliothek mit technischen Indikatoren. Dies ist eine bequeme Möglichkeit, Daten schnell mit Signalen anzureichern, ohne klassische Formeln manuell zu implementieren.
Die Installation der Bibliotheken wird mit einem einzigen Befehl durchgeführt.
pip install numpy pandas matplotlib seaborn scikit-learn ta
Es lohnt sich, die Bibliothek pytz zu installieren. Auf den ersten Blick ist es ein Hilfsmittel, aber in der Praxis ist es entscheidend für die Arbeit mit Zeitzonen.
pip install pytz
Finanzdaten sind streng zeitabhängig. Die Börsen arbeiten in verschiedenen Zeitzonen.
Standardmäßig stützt sich Python bei der Erstellung des Objekts vom Typ datetime auf die lokale Zeit des Systems. Dieses Verhalten ist für alltägliche Aufgaben praktisch, aber in einem finanziellen Kontext wird es zu einer Quelle von systemischen Fehlern. MetaTrader 5 speichert die Zeiten der Ticks und Bars im UTC-Format – ohne Zeitverschiebung und ohne Bezug auf die lokale Zeitzone.
Dadurch entsteht eine klassische Inkonsistenz: Das Modell arbeitet mit einer anderen Zeitbasis als die Daten. In der Praxis führt dies zu unangenehmen Auswirkungen.
Daher gilt hier eine strenge Regel. Alle Operationen, die sich auf die Zeit beziehen, müssen in UTC durchgeführt werden. Die Objekte vom Typ datetime sollten explizit in der UTC-Zeitzone erstellt werden, während alle lokalen Werte auf einen einheitlichen Standard gebracht werden sollten. Dadurch werden die Daten und das Modell im gleichen Zeitkoordinatensystem ausgerichtet.
Es ist eine gute Praxis, pytz für die explizite Verwaltung von Zeitzonen zu verwenden. Dadurch werden implizite Transformationen vermieden und das Systemverhalten wird vorhersehbar.
Die vom MetaTrader 5 erhaltenen Daten sind bereits in UTC. Sie sollten nicht verändert werden. Sie sollten korrekt interpretiert werden und mit der Modelllogik übereinstimmen. Bei finanziellen Problemen ist die Zeit nicht nur eine Markierung, sondern eine Koordinatenachse. Jeder Fehler in ihr macht die gesamte Analysegeometrie zunichte.
Dieser Satz sieht begrenzt aus, deckt aber in der Praxis bis zu 80 % der typischen Aufgaben ab. Dies ist ein klassischer Ansatz: weniger Redundanz, mehr Effizienz.
Wenn wir die aktuelle Umgebung verlassen müssen, verwenden wir den Standardbefehl:
deactivate
In diesem Stadium ist die Infrastruktur vollständig fertig. Das Terminal, der Interpreter und die erforderlichen Bibliotheken sind installiert. Die Umgebung ist isoliert und reproduzierbar. Dies ist die Grundlage, auf der man bequem und sicher weitere Analysen durchführen, Hypothesen testen und schrittweise Handelsmodelle entwickeln kann.
2. Laden von Daten
Nachdem wir die Infrastruktur eingerichtet haben, gehen wir zum ersten praktischen Schritt über – dem Schreiben des Programms. Hier gibt es keine strengen Einschränkungen: Jeder bekannte Editor ist geeignet. Im Zusammenhang mit der Integration ist es jedoch logisch, den integrierten Editor des MetaTrader 5 namens MetaEditor zu verwenden, der bereits die Arbeit mit Python unterstützt und es Ihnen ermöglicht, den gesamten Prozess in einem einzigen Rahmen zu halten.
Um Python-Skripte direkt aus MetaEditor oder dem Terminal zu starten, genügt es, den Pfad zum Interpreter einmal in den Plattformeinstellungen anzugeben. Dies ist ein grundlegender Vorgang, aber es gibt hier ein wichtiges Detail. Wenn Sie eine zuvor erstellte virtuelle Umgebung verwenden, sollte der Pfad speziell auf deren Interpreter verweisen, nicht auf eine globale Python-Installation.

Mit diesem Ansatz wird die Projektisolierung aufrechterhalten und sichergestellt, dass alle Abhängigkeiten kontrolliert werden. Andernfalls riskieren Sie subtile Fehler, bei denen sich derselbe Code je nach Laufzeitumgebung unterschiedlich verhält. In finanziellen Angelegenheiten ist dies ein inakzeptabler Luxus.
In der ersten Phase stellen wir eine grundlegende Verbindung her: Python → MetaTrader 5 → Historische Daten. Die Aufgabe ist einfach formuliert und inhaltlich entscheidend: Verbindung zum Terminal über ein Skript und Abruf von Kursdaten für ein bestimmtes Instrument und einen bestimmten Zeitrahmen. Von diesem Moment an beginnt jede sinnvolle Analyse.
Es ist logisch, die Skriptstruktur in Blöcken aufzubauen. Zunächst binden wir die erforderlichen Bibliotheken ein – dies bildet das Arbeitsinstrumentarium.
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
Anschließend wird die Verbindung zum Terminal über das Modul MetaTrader5 initialisiert. Dies ist der Einstiegspunkt in das System. Wenn die Verbindung nicht zustande kommt, ist es sinnlos, weiterzumachen. Daher wird die Statusprüfung sofort und ohne Kompromisse durchgeführt.
# 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()
Der nächste Schritt ist die Einstellung des Zeitintervalls. Hier verwenden wir pytz für die explizite Angabe der UTC-Zone. Dies ist kein technisches Detail, sondern eine zwingende Voraussetzung. Das Terminal speichert die Preisdaten in UTC, und jede Abweichung auf der Python-Seite führt zu Datenverzerrungen. Der Fehler ist unauffällig, aber die Folgen sind systemisch.
# 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
Danach wird eine Anfrage nach historischen Daten gestellt. Das Beispiel verwendet stündliche Bars für EURUSD. Hier lohnt es sich, auf den Namen des Instruments zu achten. Sie sollte genau mit der Schreibweise im Terminal übereinstimmen, einschließlich der Suffixe und Präfixe.
# Get bars from EURUSD H1 (hourly timeframe) within the specified interval rates = mt5.copy_rates_range("EURUSD", mt5.TIMEFRAME_H1, utc_from, utc_to)
Die Ausgabe ist ein Array von Strukturen mit Preisen und Zeitstempeln – rohe Marktdaten ohne Filter und Interpretationen. Dies ist genau die Art von Daten, die in der ersten Phase benötigt wird.
Danach ist die Verbindung zum Terminal korrekt geschlossen. Das ist Ausführungsdisziplin: Ein geordnetes Herunterfahren verhindert versteckte Fehler beim nächsten Start und macht das Systemverhalten vorhersehbar.
# Shut down connection to the MetaTrader 5 terminal
mt5.shutdown()
Anschließend erfolgt eine grundlegende Überprüfung des Ergebnisses. Wenn Daten empfangen werden, werden die ersten Datensätze zur schnellen Validierung angezeigt. Wenn nicht, wird das Skript beendet.
# 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)
Zusätzlich wird eine Visualisierung durchgeführt: Es wird ein Chart der Schlusskurse und des Volumens erstellt. In diesem Fall ist es ratsam, die Volumina auf einer separaten Achse zu platzieren. Wir setzen den Höchstwert auf der Volumenachse mit einer Marge fest, die fünfmal größer ist als der beobachtete Höchstwert. Diese Methode sieht einfach aus, aber sie funktioniert einwandfrei. Das Histogramm wird an den unteren Rand des Charts gedrückt und konkurriert nicht mehr mit dem Preis um Aufmerksamkeit.
Dadurch bleibt die Schlusskurslinie sauber und lesbar, und die Volumina bleiben informativ, ohne die visuelle Darstellung zu überladen. Dies ist ein klassisches Gleichgewicht zwischen der Vollständigkeit der Daten und ihrer Verständlichkeit. Ein Chart sollte nicht nur Informationen enthalten, sondern auch eine schnelle Interpretation ohne unnötigen Stress ermöglichen.
# 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')

Aus praktischer Sicht scheint das Verfahren einfach zu sein. Tatsächlich ist dies aber eine wichtige Phase der Datenqualitätskontrolle. Sie wissen genau, was in das Modell eingespeist wird: Format, Zeitstempel, Werte. Diese Art der Erstprüfung ist ein klassisches Beispiel für einen technischen Ansatz. Das spart Zeit, Nerven und, was bei Handelssystemen besonders wichtig ist, Geld.
3. Testen von Hypothesen und Auswahl von Merkmalen
Da uns nun historische Kursdaten zur Verfügung stehen, können wir mit der ersten Analyse beginnen. Beginnen wir mit einer sehr einfachen, fast schon lehrbuchhaften Hypothese: Der Markt tendiert dazu, die Bewegung der letzten Bar fortzusetzen. Lassen Sie uns prüfen, ob es eine Trägheit in der kurzfristigen Dynamik gibt.
Mit dem Python-Toolkit können Sie eine solche Hypothese in nur wenigen Zeilen testen. Die Logik ist wie folgt. Nehmen Sie eine Reihe von Schlusskursen und gehen Sie zu den Kursdifferenzen von Bar zu Bar über. Erhalten Sie die Preisdynamik, die für die Analyse von grundlegender Bedeutung ist.
# 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]
Als Nächstes bilden wir zwei Zeitreihen: die letzte und die nächste Änderung. Technisch wird dies durch Verschieben des Arrays um ein Element realisiert. Das Ergebnis sind Wertepaare, bei denen jede Beobachtung eine einfache Frage beantwortet: Wenn sich der Markt nach oben (oder unten) bewegt hat, was passiert dann in der nächsten Bar?
diff = np.column_stack((diff[:-1], diff[1:])) data_matrix = pd.DataFrame(diff, columns=['last', 'next'])
Anschließend wird die Pearson-Korrelation zwischen diesen beiden Reihen berechnet. Eine positive Korrelation deutet auf eine starke Trägheit hin, während eine negative Korrelation einen vorherrschenden Rücksetzer bedeutet.
Zur Verdeutlichung wird das Ergebnis mit Seaborn visualisiert – es wird eine Korrelations-Heatmap erstellt. Auf diese Weise kann man sich schnell einen Überblick über die Struktur der Abhängigkeit verschaffen, ohne sich mit numerischen Details zu befassen.
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()
Der springende Punkt ist hier nicht die Strategie selbst – sie ist von Natur aus primitiv. Der Wert liegt woanders. Wir demonstrieren einen grundlegenden Hypothesentestzyklus. Wir haben eine Hypothese formuliert, die Daten transformiert, eine Berechnung durchgeführt und das Ergebnis visualisiert. Dieser Ansatz diszipliniert die Analyse und verhindert, dass wir aus dem Stegreif Schlussfolgerungen ziehen.

Aber lassen Sie uns dennoch die erzielten Ergebnisse bewerten. Die beobachtete Korrelation beträgt -0,018 – ein Wert nahe Null, aber mit einem negativen Vorzeichen.
Dies bedeutet, dass es keine eindeutige Beziehung zwischen benachbarten Bars gibt. Außerdem deutet das schwache negative Vorzeichen auf einen subtilen Mean-Reversion-Effekt (Rückkehr zum Mittelwert) hin. Nach einer Bewegung in eine Richtung wird sich die nächste Bar wahrscheinlich in die entgegengesetzte Richtung bewegen. Das Ausmaß des Effekts ist jedoch so gering, dass er aus praktischer Sicht an statistisches Rauschen grenzt.
Die Hypothese der Fortsetzung der Bewegung wird nicht bestätigt. Der Markt im H1-Zeitrahmen verhält sich eher wie ein Zufallsprozess als wie ein Inertialsystem. Dies ist eine wichtige Feststellung. Sie eliminiert sofort eine ganze Klasse naiver Strategien und schafft eine nüchterne Ausgangsbasis für die weitere Analyse.
Eine Kerze ist ein zu kleiner Maßstab. Solche Daten sind eher von Rauschen als von Struktur geprägt. Daher gehen wir den nächsten logischen Schritt: Wir erweitern die Beobachtungen und überprüfen nicht einzelne Veränderungen, sondern durchschnittliche Bewegungen.
Anstelle einer einzelnen Bar nehmen wir die durchschnittliche Preisänderung über ein historisches Intervall von 1 bis 23 Bars. Dadurch werden zufällige Schwankungen geglättet und die stabilere Komponente der Bewegung isoliert. In ähnlicher Weise bilden wir die zukünftige durchschnittliche Preisveränderung auf dem Horizont von 1 bis 9 Bars. Wir gehen also von punktuellen Beobachtungen zu aggregierten Signalen über.
Die Umsetzung ist übersichtlich in zwei Blöcke unterteilt. Der erste Block berechnet gleitende Durchschnitte für vergangene Veränderungen.
# 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()
Der zweite Block berechnet die zukünftigen Änderungen, mit einer obligatorischen Verschiebung, um ein Durchsickern von Informationen aus der Zukunft in die Vergangenheit zu verhindern. Dies ist ein entscheidender Punkt: Ohne ihn wird die Analyse sinnlos.
for period in range(2, 10, 1): data_matrix[f'next_mean_{period}'] = data_matrix['next'].rolling(window=period).mean().shift(-(period-1))
Nach der Berechnung werden Zeilen mit fehlenden Werten entfernt – eine unvermeidliche Folge von Fensteroperationen.
# Remove rows with missing values created by rolling calculations data_matrix.dropna(inplace=True)
Anschließend wird die Pearson-Korrelationsmatrix konstruiert und daraus die notwendige Untermatrix abgeleitet: die Abhängigkeit der Zukunft von der Vergangenheit. Hier ist die Antwort auf die Hauptfrage: Hat die durchschnittliche Bewegung eine Vorhersagekraft?
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]
Die Visualisierung mittels Seaborn in Form einer Heatmap ist hier besonders geeignet. Sie ermöglicht es uns, die Struktur der Abhängigkeiten über das gesamte Parameterraster hinweg schnell zu bewerten.
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()
Aus methodischer Sicht handelt es sich bereits um ein ausgereifteres Experiment. Wir bewegen uns weg von dem naiven Test Bar-zu-Bar und gehen zur Analyse der aggregierten Effekte über. Wenn es auf dem Markt schwache Muster gibt, beginnen sie auf diesem Niveau zu entstehen.

Das erzielte Ergebnis sieht interessanter aus, aber die allgemeine Schlussfolgerung bleibt zurückhaltend. Wir sehen immer noch negative Korrelationswerte. Jetzt sind sie jedoch strukturiert. In der zentralen Region der Matrix (Fenster von 8-14 Bars in der Historie und 5-8 Bars im Horizont) verstärkt sich der Effekt und erreicht Werte von -0,02...-0,03. Dies ist ein schwaches, aber beständiges Signal für eine Rückkehr zum Mittelwert.
Die Logik liest sich ganz klar. Wenn sich der Markt eine Zeit lang in eine Richtung bewegt hat, ist die Wahrscheinlichkeit höher, dass wir in der folgenden Bar eine Teilkorrektur sehen werden. Der Effekt ist jedoch nicht linear:
- bei kurzen Fenstern geht das im Rauschen unter;
- bei zu langen Fenstern verschwimmt es und verliert an Kraft;
- Das Maximum wird in den mittleren Bereichen erreicht.
Die untere rechte Ecke der Matrix ist gesondert zu erwähnen. Dort tendiert die Korrelation gegen Null und wird an einigen Stellen sogar positiv. Dies ist ein klassisches Zeichen für den Verlust der Vorhersagekraft bei übermäßiger Glättung: Das Signal verschwindet zusammen mit der Datenvariabilität.
Die Visualisierung mittels Seaborn hebt die Struktur der Abhängigkeiten klar hervor. Die Darstellung ist insgesamt recht aussagekräftig.
Die Schlussfolgerung ist einfach: Der Markt weist keine starke Trägheit auf, aber er zeigt einen schwachen Mean-Reversion-Effekt. Dies liefert keine fertige Strategie, aber es gibt die Richtung vor.
Da die erhaltenen Korrelationswerte niedrig sind, kann ein solches Signal nicht direkt in den Handel in einer linearen Umgebung übertragen werden. Aber hier beginnt der interessantere Teil der Arbeit. Wenn eine einfache Beziehung schwach ist, können wir versuchen, sie durch eine Kombination von Merkmalen zu verstärken, d. h. wir gehen zu einem Modell über.
Die Hauptaufgabe besteht darin, einen informativen Merkmalsraum zu bilden. Eine einzige Preisreihe ist nicht ausreichend. Der nächste Schritt ist daher die Suche nach zusätzlichen Merkmalen, die die verborgene Struktur des Marktes erfassen können. Es ist sinnvoll, die klassischen technischen Indikatoren als Grundstock zu verwenden. Dies ist ein bewährtes Instrument, das trotz seiner Einfachheit oft nützliche Signale liefert.
Als Nächstes wenden wir den gleichen disziplinierten Ansatz an – Überprüfung durch Korrelation. Wir bewerten die Beziehung zwischen der künftigen Kursentwicklung und den Werten verschiedener Indikatoren. In diesem Fall variieren die Parameter der Indikatoren in der Schleife. Auf diese Weise können wir sofort eine breite Palette von Konfigurationen abdecken und sehen, wo das Signal am stärksten ist.
Aus praktischer Sicht ähnelt der Prozess dem systematischen Parameterdurchlauf mit anschließender Filterung. Im Wesentlichen geht es jedoch um die Bildung und Auswahl eines Merkmalsraums. Die Korrelation dient hier als Diagnoseinstrument. Wir messen, welche Datentransformationen mit zukünftigen Bewegungen verbunden sind. Dies ist eine grundlegend andere Denkweise: erst die Struktur verstehen, dann den Gewinn herausholen.
Im Code ist dieser Ansatz recht systematisch umgesetzt. Zum einen werden einfache Ableitungen des Preises verwendet – gleitende Durchschnitte, ihre erste und zweite Differenz, Abweichungen vom aktuellen Wert. Dies ist ein Versuch, die lokale Dynamik und die Marktbeschleunigung zu erfassen.
# 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))
Zum anderen werden klassische Indikatoren aus der TA-Bibliothek hinzugefügt: SMA, RSI, MACD, und zwar in mehreren Parametrisierungen auf einmal. Diese Abdeckung verhindert, dass wir den richtigen Zeitraum erraten und stattdessen das Verhalten über den gesamten Bereich sehen.
# 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)
Besonders aufschlussreich ist die Hinzufügung von Ableitungen zweiter Ordnung (diff und diff von diff). Dies ist bereits ein Versuch, die Veränderung des Signals selbst zu erfassen – ein Übergang zur Analyse der Beschleunigung und Verlangsamung der Marktbewegung. Bei Finanzreihen erweisen sich solche Effekte oft als aussagekräftiger als die Werte selbst.
Anschließend wird eine Filterung vorgenommen. Von der gesamten Korrelationsmatrix bleiben nur die Merkmale übrig, deren maximaler Zusammenhang mit der Zielvariablen den festgelegten Schwellenwert (in diesem Fall 0,02) überschreitet. Dies ist ein wichtiger Punkt. Schwache und instabile Abhängigkeiten werden bewusst ausgeblendet, sodass nur solche übrig bleiben, die zumindest in geringem Umfang in den Daten enthalten sind.
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]
Die Visualisierung über Seaborn vervollständigt den Prozess.
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()
Die Heatmap sieht nicht mehr wie chaotisches Rauschen aus, sondern wird zu einer Signalkarte. Es wird deutlich, welche Gruppen von Indikatoren beginnen, auf künftige Bewegungen zu reagieren, bei welchen Parametern diese Reaktion zunimmt und wo sie ganz verschwindet.

An dieser Stelle beginnt das Set an Hypothesen eine klarere Struktur anzunehmen. Wir gehen von der zufälligen Auswahl von Indikatoren zur bewussten Bildung von Merkmalen über. Es handelt sich noch nicht um ein Modell, sondern um dessen Rahmen. Die nächste Aufgabe besteht darin, diese schwachen, disparaten Signale zu einem einzigen System zusammenzufügen, das in der Lage ist, eine stabile Abhängigkeit zu erkennen, die für sich genommen fast unsichtbar ist.
4. Aufbau und Training des Modells
Im vorangegangenen Schritt haben wir eine Korrelationskarte erstellt und die Merkmale mit den höchsten Werten der direkten und inversen Korrelation ausgewählt. Dies ist ein grundlegender Punkt: Negative Korrelation ist das gleiche Signal, nur mit umgekehrtem Vorzeichen. In Bezug auf das Modell ist dies keine Einschränkung, sondern eine zusätzliche Information.
Als Nächstes gehen wir zum Training über. Das Ökosystem von Scikit-Learn bietet eine breite Palette von Algorithmen – von linearen Modellen bis hin zu Ensembles. Der Artikel „Regression models of the Scikit-learn Library and their export to ONNX" bietet einen Vergleich von 55 Regressionsmodellen. Für die Lösung eines praktischen Problems ist es jedoch sinnvoll, sich auf robuste und bewährte Algorithmen zu konzentrieren. In diesem Fall werden wir RandomForestRegressor verwenden.
Der klassische Random Forest ist ein Kompromiss zwischen Einfachheit und Ausdruckskraft. Er funktioniert gut mit nichtlinearen Abhängigkeiten, ist robust gegenüber Rauschen und erfordert keine aggressive Datennormalisierung. Dies ist genau das, was in der ersten Phase der Modellierung benötigt wird.
Der nächste wichtige Schritt ist die Auswahl von Hyperparametern. Wir verwenden eine direkte Suche über zwei Parameter: die Anzahl der Bäume n_estimators) und die Baumtiefe (max_depth). Dies ist eine vernünftige Wahl: Der erste Parameter ist für die Stärke des Ensembles verantwortlich, während der zweite sich auf den Grad des Eintauchens eines einzelnen Baums in die Daten und die Kontrolle der Überanpassung konzentriert.
Zu diesem Zweck erstellen wir ein neues Skript. Der Algorithmus für die Verbindung zum MetaTrader 5 und das Laden historischer Kursdaten bleibt derselbe, sodass wir seine Beschreibung weglassen.
Nach Erhalt der historischen Daten beginnt die Bildung des Merkmalsraums. Es wird eine Grundmatrix erstellt: Preisunterschiede, ihre geglätteten Versionen und abgeleitete Merkmale.
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
Anschließend werden technische Indikatoren hinzugefügt: SMA und eine Reihe von MACD-Konfigurationen.
# 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
Alle Merkmale werden nacheinander in einer einzigen Datenstruktur gesammelt, während ihre Namen in einer separaten Merkmalsliste aufgeführt sind. Dies vereinfacht die weitere Arbeit und verhindert den versehentlichen Verlust von Variablen.
Im gleichen Schritt wird die Zielvariable gebildet – die gesamte Kursbewegung über einen bestimmten Horizont (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)
Das Problem wird also als Regression formuliert: Auf der Grundlage aktueller Indikatoren sollen zukünftige Preisänderungen vorhergesagt werden.
Der nächste Schritt ist die Datenbereinigung. Nach Anwendung von gleitenden Fenstern und Differenzierung treten zwangsläufig fehlende Werte auf. Sie werden entfernt, um ein korrektes Modelltraining zu gewährleisten.
data_matrix.dropna(inplace=True)
Dann werden die Daten nach Zeit aufgeteilt: Die ersten 90 % werden zum Training verwendet, die restlichen 10 % zum Testen. Dies ist ein grundlegend wichtiger Punkt. Im Gegensatz zu den klassischen Problemen des maschinellen Lernens ist hier keine Datenvermischung erlaubt. Wir halten die Chronologie strikt ein und simulieren einen realen Prozess: erst die Vergangenheit, dann die Zukunft.
# ===== 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:]
Danach wird ein Zyklus zum Durchlauf der Hyperparameter des RandomForestRegressor-Modells eingeleitet. Für jede Kombination von Parametern wird ein vollständiger Zyklus durchgeführt:
- das Modell wird anhand einer Trainingsstichprobe trainiert;
# ===== 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))
Die wichtigste Kennzahl ist die Korrelation zwischen der Prognose und dem tatsächlichen Wert der Stichprobe. Darin spiegelt sich die Fähigkeit des Modells wider, die Bewegungsrichtung zu erfassen. R² und MAE werden zusätzlich berechnet, um die Gesamtqualität der Annäherung und das Fehlerniveau zu kontrollieren.
Alle Ergebnisse werden in einer Tabelle gespeichert, in der jede Zeile einer bestimmten Kombination von Hyperparametern entspricht. Anschließend wird diese Tabelle in eine Matrixform umgewandelt, die es uns ermöglicht, die Abhängigkeit der Modellqualität von den Parametern zu veranschaulichen.
# --- 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()
Der letzte Schritt ist die Visualisierung mit Hilfe von Seaborn. Die Heatmap zeigt deutlich, in welchem Parameterbereich das beste Ergebnis erzielt wird. Auf diese Weise können wir nicht nur die optimale Konfiguration auswählen, sondern auch die Stabilität des Modells bewerten, d. h. wie stark sich die Qualität bei kleinen Änderungen der Parameter ändert.
# --- 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()

Hier zeigt sich ein wichtiger Punkt: Das Modell erzeugt nicht aus dem Nichts ein Signal. Es bündelt lediglich die zuvor gefundenen schwachen Abhängigkeiten. Wenn in der Analysephase nicht einmal ein Hauch von Struktur vorhanden war, kann kein Modell die Situation retten. Aber wenn es ein Signal gibt, auch wenn es schwach ist, kann das Ensemble es verstärken und für den praktischen Gebrauch nutzbar machen.
Im Diagramm der ersten Iteration der Hyperparameterauswahl ist die Region mit max_depth = 10 deutlich sichtbar. Dieser Wert zeigt das stabilste Gleichgewicht zwischen der Fähigkeit des Modells, Abhängigkeiten zu erfassen und die Überanpassung zu kontrollieren. Dies ist der Zeitpunkt, an dem die Betriebsart erreicht ist, wenn das Modell nicht mehr primitiv ist, aber noch nicht anfängt, sich dem Rauschen anzupassen.
Dann entwickelt sich die Logik von selbst. Nach der Festlegung von max_depth geht es an die zweite Stufe – den Aufbau der Baumstruktur über max_leaf_nodes. Parallel dazu grenzen wir den Bereich nach der Anzahl der Bäume (n_estimators) ein, sodass nur der Bereich übrig bleibt, in dem zuvor stabile Ergebnisse beobachtet wurden. Auf diese Weise können wir die Suchauflösung erhöhen: Der Suchschritt wird reduziert, und die Aufmerksamkeit wird auf den wirklich wichtigen Bereich der Parameter konzentriert.
Dieser Ansatz erinnert an das klassische Verfahren der lokalen Optimierung. Zunächst wird ein grober Bereich des Maximums bestimmt, dann eine saubere Feinabstimmung innerhalb dieses Bereichs. Dadurch vermeiden wir die Verschwendung von Rechenressourcen für offensichtlich schwache Konfigurationen und gelangen schnell zu einer stabilen Kombination von Parametern.
Die Änderungen im Code sind gezielter Natur: Der Bereich der Suche wird spezifiziert und der zweite Parameter wird geändert. Die Architektur des Skripts bleibt unverändert.

Nach der Auswahl legen wir die optimalen Hyperparameter fest und fahren mit dem endgültigen Training des Modells fort. Strukturell ändert sich nichts: Die Iterationsschleifen werden aus dem Skript entfernt und die Parameterwerte werden direkt bei der Initialisierung von RandomForestRegressor angegeben. Alle anderen Logiken – Datenaufbereitung, Stichprobenaufteilung, Training und grundlegende Validierung – bleiben unverändert.
Als Nächstes kommt ein subtilerer, aber grundlegenderer Punkt. Die Zuverlässigkeit des Modells ist bei den einzelnen Prognosen nicht in gleichem Maße gegeben. In einigen Fällen gibt es ein starkes Signal, in anderen liegen die Werte nahe bei Null und damit in der Zone der Unsicherheit. Wenn alle Prognosen gleich behandelt werden, beginnt die Strategie unweigerlich mit Rauschen zu handeln.
Daraus ergibt sich eine natürliche Hypothese: schwache Signale ignorieren und nur dort arbeiten, wo das Modell ein ausreichendes Vertrauen zeigt oder die erwartete Bewegung die Kosten deckt. Dies ist bereits ein Übergang vom Funktionsmodell zum Handelsfiltermodell.
Der Code setzt diese Idee sauber und ohne unnötige Komplexität um. Auf der Grundlage der Trainingsstichprobe werden Schwellenwerte für absolute Prognosen berechnet.
# ===== 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)
Dies ist ein wichtiger Punkt: Die Schwellenwerte werden in train bestimmt und im test angewendet, wodurch die Gültigkeit des Experiments erhalten bleibt.
Anschließend wird eine Matrix aus Prognosen und entsprechenden Schwellenwerten gebildet. Für jede Stufe wird eine Maske berechnet – welche Signale den Filter passieren. Die Position wird durch das Vorzeichen der Prognose bestimmt, wobei die allgemeine Korrelation berücksichtigt wird, die es ermöglicht, dass die Handelsrichtung mit der Natur des Modells übereinstimmt.
# 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)
Anschließend wird die Rentabilität als Produkt aus der Position und der tatsächlichen Bewegung berechnet. Danach werden feste Kosten (Spread/Kommission) abgezogen und die kumulative Kapitalkurve wird erstellt.
# 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)
Die Ergebnisse werden in einer Tabelle aggregiert:
- final_equity – endgültige Rentabilität;
- mean_return – durchschnittliches Handelsergebnis;
- win_rate – Anteil der gewinnbringenden Trades.
# 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'))
Aus praktischer Sicht handelt es sich nicht mehr nur um die Bewertung eines Modells, sondern um die Anfänge eines Handelssystems. Wir prüfen nicht nur die Prognosegenauigkeit, sondern bewerten auch sofort, wie sie auf verschiedenen Sortierungsebenen monetarisiert wird.

5. Umwandlung in ONNX
Wir haben das Modell trainiert. Der nächste logische Schritt besteht darin, die Person aus dem Entscheidungsprozess herauszunehmen. Der manuelle Handel auf der Grundlage von Modellsignalen ist dem automatisierten Handel fast immer unterlegen: Es gibt keine Kontinuität, die Reaktionsgeschwindigkeit geht verloren, und der psychologische Faktor kommt hinzu. In der Praxis führt dies zu systemischen Verzerrungen – verpasste Einstiege, verfrühte Ausstiege, Misstrauen gegenüber dem eigenen Modell.
Die Plattform MetaTrader 5 bietet zwei Automatisierungswege. Die erste ist der Start des Python-Skripts mit direkter Ausführung der Trades. Das zweite ist die Übertragung des Modells in das ONNX-Format mit anschließender Verwendung im MQL5 EA. In der Praxis scheint die zweite Option ausgereifter zu sein.
Das ONNX-Format löst gleich mehrere Probleme auf einmal. Das Modell ist in einer kompakten und unabhängigen Form festgelegt. Es kann leicht zwischen Computern übertragen werden – Sie brauchen nur das Terminal selbst. Mit dem Strategietester ist es möglich, vollwertige Tests durchzuführen. Und das ohne Leistungseinbußen: Das Terminal unterstützt Hardware-Beschleunigung, einschließlich der Arbeit mit GPU (CUDA), was besonders bei der Verwendung von Ensemble-Modellen wichtig ist.
Die Konvertierung ist vergleichsweise einfach. Zunächst wird der Input des Modells beschrieben – die Dimension des Merkmalsraums.
# 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]))]
Anschließend wird das trainierte Modell aus Scikit-Learn über den entsprechenden Konverter in ONNX umgewandelt und auf der Festplatte gespeichert.
# 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())
Danach folgt die obligatorische Validierungsphase. Das Modell wird über ONNX Runtime geladen, und dieselben Daten werden zur Berechnung der vorhergesagten Werte verwendet.
# 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()
Diese werden dann mit den ursprünglichen Ergebnissen des Sklearn-Modells verglichen.
# 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)))
Hier gibt es zwei wichtige Kriterien:
- Die Korrelation zwischen den Prognosen sollte gegen 1 tendieren;
- die maximale Abweichung sollte vernachlässigbar gering sein.
Wenn diese Bedingungen erfüllt sind, können wir davon ausgehen, dass die Übertragung erfolgreich war und das Modell für die Integration in das Handelssystem bereit ist.
Aus praktischer Sicht ist dies der endgültige Übergang von der Python-Forschungsumgebung zur praktischen Anwendung. Das Modell hört auf, ein Experiment zu sein und wird Teil der Infrastruktur – autonom, reproduzierbar und geeignet für Tests und realen Handel.
6. Testen mit dem Strategietester
Nach Abschluss der Arbeiten auf der Python-Seite wird die Logik in die Laufzeitumgebung MetaTrader 5 übertragen. Hier hört das Modell auf, ein Forschungsinstrument zu sein und wird Teil eines Handelsalgorithmus. Es ist wichtig, dass die Codestruktur dem bereits bekannten Ablauf folgt: Initialisierung → Datenaufbereitung → Prognose → Handelsentscheidung.
Die Initialisierung wird in der Methode OnInit durchgeführt. In diesem Stadium wird das ONNX-Modell aus der Ressource geladen und die Laufzeitumgebung über OnnxCreateFromBuffer erstellt.
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; }
Als Nächstes werden die Eingabe- und Ausgabedimensionen explizit festgelegt – dies ist entscheidend, da das Modell eine streng festgelegte Anzahl von Merkmalen erwartet. Ein Fehler in diesem Stadium führt zu einer falschen Vorhersagen.
Die Indikatoren SMA und der MACD werden mit denselben Parametern, die beim Training verwendet wurden, parallel initialisiert.
//--- 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); }
Dies ist ein grundlegender Punkt: Die Merkmale in MQL5 sollten mit denen identisch sein, die beim Training in das Modell eingegeben wurden. Jede Abweichung macht die Vorhersagefähigkeit zunichte.
Die Hauptlogik ist in der Methode OnTick konzentriert, aber mit dem Filter IsNewBar für das Eröffnungsereignis einer neuen Bar. Dadurch wird verhindert, dass das Modell bei jedem Tick neu berechnet wird, und die Berechnungen werden mit dem Zeitrahmen synchronisiert.
void OnTick() { //--- if(!IsNewBar()) return;
Als Nächstes folgt der Block für die Abrechnung der aktuellen Positionen – eine einfache Aggregation von Volumen und Gewinnen nach Richtung. Dies ist notwendig, um bereits offene Trades zu kontrollieren.
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; } }
Anschließend wird der Vektor Inputs mit den Eingabedaten gebildet. Im Wesentlichen wird hier die Merkmalsgenerierung aus Python manuell reproduziert:
- Basismerkmale;
//--- 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]; }
Bitte beachten Sie die Indexierung: Jedes Merkmal nimmt einen genau festgelegten Platz ein. Dies ist ein Vertrag zwischen Modell und Ausführung. Wird die Reihenfolge verletzt, beginnt das Modell mit verzerrten Eingaben zu arbeiten.
Nach der Vorbereitung der Merkmale wird OnnxRun aufgerufen.
//--- run the inference if(!OnnxRun(onnx, ONNX_LOGLEVEL_INFO, Inputs, Forecast)) { Print("OnnxRun error ", GetLastError()); return; }
Das Ergebnis ist eine Prognose – die erwartete Preisentwicklung. Als Nächstes kommt der praktische Teil – die Signalinterpretation.
Der Code verwendet eine einfache, aber effiziente Logik:
- Ein Schwellenwert wird eingeführt, um schwache Signale abzuschneiden. Wir gehen von den Trainingsergebnissen aus.
- Die Richtung der Korrelation wird berücksichtigt, sodass das Modell bei Bedarf invertiert werden kann.
- Wenn die Prognose den Schwellenwert überschreitet, wird eine Position eröffnet. Wenn das Signal verschwindet, wird die Position geschlossen.
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); } }
Das Modell wird also als gerichteter Bewegungsfilter verwendet. Dies ist ein wichtiger Unterschied: Wir handeln nicht jeden Prognosewert, sondern nur diejenigen, die die Signalstärke übertreffen.
Der daraus resultierende EA wird dann einer wichtigen Prüfung unterzogen – dem Test im Strategietester des MetaTrader 5 mit historischen Daten für das erste Quartal 2026. Es handelt sich nicht mehr um eine abstrakte Bewertung des Modells, sondern um ein realitätsnahes Umsetzungsszenario.
Dieses Testformat ist von grundlegender Bedeutung. Während wir in der Python-Phase das Modell anhand von Metriken bewertet haben, wird hier das gesamte System getestet – von der Merkmalsgenerierung bis zur Logik der Öffnungs- und Schließpositionen. Der erste Test der Strategie findet nämlich unter Bedingungen statt, die den realen Bedingungen so weit wie möglich ähneln.

Die Testphase schließt die Entwicklungsschleife ab: von der Hypothese und der Datenanalyse zum Modell, dann zur Automatisierung und Überprüfung anhand historischer Daten. Hier zeigt sich, ob es gelungen ist, schwache statistische Zusammenhänge in ein praktisches Handelsinstrument zu verwandeln.
Die Integration des ONNX-Modells in einen EA ist jedoch nicht der einzige Anwendungsfall. Für Fans des manuellen Handels bietet der MetaTrader 5 die Möglichkeit, ein Modell direkt in einen benutzerdefinierten Indikator einzubetten. In diesem Fall trifft das Modell keine Entscheidungen für den Händler, sondern fungiert als Analyseinstrument, das Signale generiert, die der Nutzer selbständig interpretiert.
Vom technischen Standpunkt aus gesehen gibt es praktisch keine Unterschiede. Die Mechanismen für die Verbindung mit dem ONNX-Modell, die Vorbereitung der Ausgangsdaten und der Aufruf der Inferenz sind völlig identisch mit der Implementierung im EA. Lediglich der Einsatzort ändert sich: Anstatt automatisch Positionen zu eröffnen, wird das Modellergebnis auf einem Chart visualisiert oder als zusätzlicher Filter bei der Entscheidungsfindung verwendet.

Dieser Ansatz hat seine Vorteile. Er ermöglicht eine flexible Kombination von Modellsignalen mit klassischer Analyse und reduziert die Anforderungen an die Zuverlässigkeit des Algorithmus – das Modell fungiert als Unterstützung und nicht als alleinige Entscheidungsgrundlage.
Schlussfolgerung
Die Integration von Python und MetaTrader 5 schafft ein komplettes, technisch erprobtes Framework für die Entwicklung von Handelslösungen – vom Konzept bis zur praktischen Umsetzung. In diesem Artikel haben wir diesen Weg sequentiell beschritten: von der Datenerfassung und -analyse über die Hypothesenprüfung und Modellkonstruktion bis hin zur Implementierung und Prüfung in einer realen Ausführungsumgebung.
Der entscheidende Vorteil dieses Ansatzes ist die Trennung der Rollen. Python übernimmt den Forschungsteil: Datenverarbeitung, Merkmalsgenerierung, statistische Analyse und Modelltraining. MetaTrader 5 wiederum sorgt für die Ausführung: Zugang zu Marktdaten, Strategietests und Handelsinfrastruktur. Hierbei handelt es sich um eine klassische Kombination des Kreislaufs aus Labor und Produktion, bei der jede Umgebung ihrem eigentlichen Zweck dient.
Die Verwendung des ONNX-Formats bietet einen zusätzlichen Nutzen. Das Modell wird portabel, unabhängig von der Entwicklungsumgebung und kann auf jedem Gerät mit einem installierten Terminal ausgeführt werden. Dies vereinfacht die Skalierung, beschleunigt die Tests und verringert die Risiken, die mit der Inkompatibilität zwischen verschiedenen Umgebungen verbunden sind.
Programme, die in diesem Artikel verwendet werden
| # | Name | Typ | Beschreibung |
|---|---|---|---|
| 1 | Experts\Integration\Integration.mq5 | Expert Advisor | EA zum Testen des Modells im Terminal |
| 2 | Indicators\Integration\Integration.mq5 | Indikator | Indikator für die Anzeige von Signalen in einem Chart |
| 3 | Scripts\Integration\load_data.py | Skript | Skript zum Laden von Daten |
| 4 | Scripts\Integration\look_model_param_rf.py | Skript | Skript mit der Aufzählung der Hyperparameter |
| 5 | Scripts\Integration\create_model_rf.py | Skript | Skript zum Trainieren des Modells und zum Exportieren in ONNX |
Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/22020
Warnung: Alle Rechte sind von MetaQuotes Ltd. vorbehalten. Kopieren oder Vervielfältigen untersagt.
Einsatz spieltheoretischer Ansätze in Handelsalgorithmen
Chaos-Optimierungsalgorithmus (COA): Fortsetzung
Eine alternative Log-datei mit der Verwendung der HTML und CSS
Forex-Arbitragehandel: Panel zur Bewertung von Wechselkursbeziehungen
- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.
In der Skriptdatei load_data.py, die sich im Archiv befindet, gibt es solche Zeilen:
dann wie in dem Artikel selbst:
Eine Kleinigkeit, die ich aber beim Testen nicht sofort gesehen habe.....
Dann musste ich die Python-Version 3.14.3 aufgeben. Ich arbeite mit Python in VS. Debugging kann man dort nur in 3.11 machen.