Pipelines in MQL5
Einleitung: Warum die Datenvorverarbeitung wichtig ist
Bei der Entwicklung und dem Aufbau von Prognosesystemen und KI-gestützten Modellen ist die Versuchung groß, sich nur auf die Architektur des Deep-Learning-Modells oder die Komplexität der Handelsstrategie zu konzentrieren. Eine der wichtigsten Determinanten der Modellleistung liegt jedoch oft nicht in den neuronalen Netzen selbst, sondern in der Qualität und Konsistenz der Daten, die dem Modell zugeführt werden. In der Praxis sind rohe Finanzdaten wie OHLC-Balken, Tick-Volumen oder sogar Spreads oft alles andere als „modellfähig“. Diese „Roh“-Werte können in verschiedenen Maßstäben vorliegen, sie können Ausreißer enthalten, die durch plötzliche Marktschocks verursacht werden, oder sie können kategorische Merkmale wie Handelssitzungen enthalten; all dies kann nicht ohne Weiteres durch mathematische Modelle verarbeitet werden.
Wenn wir beispielsweise ein einfaches neuronales Netz auf normalisierte Preisrenditen und Volatilitätsindikatoren trainieren, dann wäre der Standard-Scaler wahrscheinlich die geeignete Wahl, da er die Vergleichbarkeit der Merkmale gewährleistet. Wenn es sich bei dem Modell beispielsweise um ein in Python trainiertes ONNX-LSTM mit sigmoiden Aktivierungen handelt, könnte der Min-Max-Skalierer besser geeignet sein, da er dazu neigt, sich an den begrenzten Bereich der Aktivierung anzupassen. Auf der anderen Seite, wenn wir das Handelsverhalten während eines aufsehenerregenden Nachrichtenereignisses modellieren, bei dem die Spreads und Volumina dramatisch ansteigen können, dann kann der robuste Skalierer dazu beitragen, die zentrale Struktur der Daten zu erhalten und gleichzeitig den Einfluss dieser Anomalie zu minimieren.
Die wichtigste Erkenntnis hier ist, dass die Vorverarbeitung nicht als nachträglicher Gedanke behandelt werden sollte. Die Wahl des Skalierers wirkt sich direkt darauf aus, wie die Merkmale in den Lernschritten interagieren, und kann den Unterschied zwischen einem Modell, das gut verallgemeinert, und einem, das verzerrt oder instabil ist, ausmachen.
Ohne eine angemessene Vorverarbeitung wird auch das ausgefeilteste Netz Schwierigkeiten haben, Muster effektiv zu lernen. Beispielsweise kann ein Merkmal wie die Streuung, wenn es in Bruchteilen von pp gemessen wird, vom Volumen überschattet werden, das in die Tausende gehen kann. Ebenso birgt die Zuweisung ganzer Zahlen zu den Marktsitzungen, wie z. B. Asien 0, Europa 1 und USA 2, die Gefahr, dass kategorische Daten in das Modell einfließen, die irreführend sein können.
Aus diesem Grund kann die Vorverarbeitung als Grundlage für robuste Workflows für maschinelles Lernen betrachtet werden. Im Python-Ökosystem ist die SCIKIT-LEARN-Bibliothek für ihre Werkzeuge zur Standardisierung, Normalisierung, robusten Skalierung sowie für die One-Hot-Codierung bekannt, die alle dazu dienen, Daten für „nachgelagerte“ Modelle vorzubereiten. Jeder Transformationsschritt bedeutet, dass die Eingangsmerkmale am Ende einen fairen Beitrag zum Lernprozess leisten. Im Handel ist die Arbeit mit MQL5-Bibliotheken jedoch mit diesem Mangel an Vorverarbeitung verbunden. Um dies zu überbrücken, können wir MQL5-Klassen implementieren, die bis zu einem gewissen Grad die Funktionalität von SCIKIT-LEARN widerspiegeln, und so zu Pipelines gelangen, die auf die Vorhersage von Finanzzeitreihen zugeschnitten sind.
Überbrückung der Ökosystemlücke
Die SCIKIT-LEARN-Bibliothek von Python hat sich in jeder Hinsicht zum Industriestandard für die Vorverarbeitung von maschinellem Lernen entwickelt. Mit minimalem Codeaufwand können Entwickler einen Standard-Scaler zur Zentrierung von Merkmalen, einen Min-Max-Scaler zur Komprimierung von Merkmalen auf einen festen Bereich, einen Robust-Scaler zur Reduzierung von Ausreißern oder einen One-Hot-Encoder zur Erweiterung von Merkmalen in binäre Darstellungen anwenden. Darüber hinaus ermöglicht die Pipeline-Klasse von SCIKIT-LEARN eine nahtlose Verkettung dieser Schritte, was bedeutet, dass alle Datensätze, die an das Modell übergeben werden, die gleichen Transformationssequenzen aufweisen. Dieser modulare Plug-and-Play-Mechanismus hat zu einer raschen Einführung des maschinellen Lernens in einer Vielzahl von Branchen geführt.
Im Gegensatz dazu werden MQL5-Entwickler von einer anderen Realität eingeschüchtert. MQL5 ist zwar relativ effizient im Umgang mit Handelsdaten, bietet aber noch keine mit SCIKIT-LEARN vergleichbaren Vorverarbeitungsmethoden. Für jede Umwandlung – ob Skalierung, Kodierung oder sogar Imputation fehlender Werte – muss die Kodierung manuell und oft auf fragmentierte Weise vorgenommen werden. Dies erhöht nicht nur die Wahrscheinlichkeit, dass Fehler auftreten, sondern erschwert auch die Reproduzierbarkeit von Testergebnissen oder die Konsistenz zwischen Trainings- und Testdaten.
Die Lösung könnte meiner Meinung nach in der Entwicklung einer Vorverarbeitungs-Pipeline-Klasse in MQL5 liegen, die diese SCIKIT-LEARN-Philosophie nachahmt. Wenn wir wiederverwendbare Module wie CStandardScaler, CMinMaxScaler, CRobustScaler und COneHotEncoder implementieren können, können wir eine Vorverarbeitungspipeline in einem Container verketten. Diese Struktur würde sicherstellen, dass die rohen Finanzzeitreihendaten systematisch aufbereitet werden, bevor sie in Deep-Learning-Modelle eingehen. Dies gilt unabhängig davon, ob die Modelle nativ in MQL5 kodiert oder über ONNX importiert werden. Dadurch können MQL5-Entwickler einen vertrauten Python-Workflow mit MQL5 übernehmen, was sauberere Experimente, schnellere Entwicklung und vermutlich zuverlässigere KI-Systeme ermöglicht.
Anatomie einer Vorverarbeitungspipeline
Bei der Definition einer Vorverarbeitungspipeline kann man sich diese wie ein Förderband für Daten vorstellen. Die Rohdaten werden auf der einen Seite eingegeben und auf der anderen Seite in ein für das Modell geeignetes Format umgewandelt. Jede Stufe des Förderbandes erfüllt eine bestimmte Aufgabe, wie das Auffüllen fehlender Werte, die Kodierung von Kategorien oder die Neuskalierung numerischer Merkmale. In Python ist dies in der Regel im Pipeline-Objekt von SCIKIT-LEARN gekapselt. In MQL5 müssen wir eine ähnliche Struktur mit nutzerdefinierten Klassen entwerfen. Unsere solche Klasse wird CPreprocessingPipeline heißen. Wir implementieren sie in MQL5 wie folgt:
// Preprocessing Pipeline class CPreprocessingPipeline { private: SPreprocessorStep m_steps[]; IPreprocessor *m_preprocessors[]; int m_step_count; public: CPreprocessingPipeline() : m_step_count(0) {} ~CPreprocessingPipeline() { for(int i = 0; i < m_step_count; i++) delete m_preprocessors[i]; } void AddImputeMedian(int column) { ArrayResize(m_steps, m_step_count + 1); ArrayResize(m_preprocessors, m_step_count + 1); m_steps[m_step_count].type = PREPROCESSOR_IMPUTE_MEDIAN; m_steps[m_step_count].column = column; m_preprocessors[m_step_count] = new CImputeMedian(column); m_step_count++; } void AddImputeMode(int column) { ArrayResize(m_steps, m_step_count + 1); ArrayResize(m_preprocessors, m_step_count + 1); m_steps[m_step_count].type = PREPROCESSOR_IMPUTE_MODE; m_steps[m_step_count].column = column; m_preprocessors[m_step_count] = new CImputeMode(column); m_step_count++; } void AddStandardScaler() { ArrayResize(m_steps, m_step_count + 1); ArrayResize(m_preprocessors, m_step_count + 1); m_steps[m_step_count].type = PREPROCESSOR_STANDARD_SCALER; m_steps[m_step_count].column = -1; m_preprocessors[m_step_count] = new CStandardScaler(); m_step_count++; } void AddRobustScaler() { ArrayResize(m_steps, m_step_count + 1); ArrayResize(m_preprocessors, m_step_count + 1); m_steps[m_step_count].type = PREPROCESSOR_ROBUST_SCALER; m_steps[m_step_count].column = -1; m_preprocessors[m_step_count] = new CRobustScaler(); m_step_count++; } void AddMinMaxScaler(double new_min = 0.0, double new_max = 1.0) { ArrayResize(m_steps, m_step_count + 1); ArrayResize(m_preprocessors, m_step_count + 1); m_steps[m_step_count].type = PREPROCESSOR_MINMAX_SCALER; m_steps[m_step_count].column = -1; m_preprocessors[m_step_count] = new CMinMaxScaler(new_min, new_max); m_step_count++; } void AddOneHotEncoder(int column) { ArrayResize(m_steps, m_step_count + 1); ArrayResize(m_preprocessors, m_step_count + 1); m_steps[m_step_count].type = PREPROCESSOR_ONEHOT_ENCODER; m_steps[m_step_count].column = column; m_preprocessors[m_step_count] = new COneHotEncoder(column); m_step_count++; } bool FitPipeline(matrix &data) { matrix temp; temp.Copy(data); for(int i = 0; i < m_step_count; i++) { matrix out; if(!m_preprocessors[i].Fit(temp)) return false; if(!m_preprocessors[i].Transform(temp, out)) return false; temp.Copy(out); } return true; } bool TransformPipeline(matrix &data, matrix &out) { out.Copy(data); for(int i = 0; i < m_step_count; i++) { matrix temp; if(!m_preprocessors[i].Transform(out, temp)) return false; out.Copy(temp); } return true; } bool FitTransformPipeline(matrix &data, matrix &out) { if(!FitPipeline(data)) return false; return TransformPipeline(data, out); } };
Diese Klasse dient im Grunde als Container für die Transformationsschritte. Entwickler können Schritte mit Methoden wie AddStandardScaler(), AddRobustScaler(), AddMinMaxScaler() oder AddOneHotEncoder() hinzufügen. Jeder dieser Schritte wird durch eine eigene Klasse repräsentiert. Wir haben zum Beispiel die Klasse CStandardScaler. Diese Klassen implementieren eine einheitliche Schnittstelle, indem sie Funktionen/Methoden wie Fit(), Transform() und FitTransform() einsetzen. Sobald die Pipeline zusammengestellt ist, wird sie an einen Trainingsdatensatz „angepasst“, wo sie Parameter wie Mittelwerte, Mediane, Modi oder sogar Kategoriezuordnungen lernt. Sobald die Anpassung zu einer zufriedenstellenden Trainings- und Testleistung geführt hat, kann sie auf einen neuen Datensatz mit ähnlichen Transformationsschritten angewendet werden. Diese Konsistenz stellt sicher, dass wir einige häufige Fallstricke beim Testen vermeiden, wie z. B. dem Leakage (machine learning).
Diese modulare Bauweise hat jedoch einige Vorteile. Erstens fördert es die Wiederverwendbarkeit, da dasselbe Pipeline-Objekt in vielen Experimenten verwendet werden kann. Zweitens verbessert es die Wartbarkeit, da jede Transformationsklasse weniger abhängig und daher leichter zu debuggen ist. Schließlich fördert es die Konsistenz in dem Sinne, dass sowohl das Training als auch die Validierung und sogar das Testen dem exakt gleichen Transformationspfad folgen. Mit anderen Worten: Die Preprocessing-Pipeline führt ein Maß an Disziplin in die MQL5-Workflows für maschinelles Lernen ein, das wohl die Robustheit des Python-Ökosystems widerspiegelt.
Standard-Skalierer
Der Standard-Skalierer ist eines der wohl am häufigsten verwendeten Vorverarbeitungswerkzeuge beim maschinellen Lernen. Sie dient dazu, jedes Merkmal um den Nullpunkt zu zentrieren und es um seine Standardabweichung zu skalieren. In der Mathematik definieren wir diese Transformation mit der folgenden Formel:

wobei:
- μ oder mu ist der Mittelwert des Merkmals, und
- σ oder sigma ist seine Standardabweichung.
- x ist ein Datenpunkt im Datensatz.
Das Endergebnis ist, dass jedes Merkmal einen Mittelwert von Null und eine Standardabweichung von Eins hat. Dadurch wird der Datensatz einheitlicher, da das Risiko, dass bestimmte Merkmale den Lernprozess dominieren, verringert wird. Wir codieren unsere Klasse CStandardScaler in MQL5 wie folgt:
// Standard Scaler class CStandardScaler : public IPreprocessor { private: double m_means[]; double m_stds[]; bool m_is_fitted; public: CStandardScaler() : m_is_fitted(false) {} bool Fit(matrix &data) { int rows = int(data.Rows()); int cols = int(data.Cols()); ArrayResize(m_means, cols); ArrayResize(m_stds, cols); for(int j = 0; j < cols; j++) { vector column(rows); for(int i = 0; i < rows; i++) column[i] = data[i][j]; m_means[j] = column.Mean(); m_stds[j] = column.Std(); if(m_stds[j] == 0.0) m_stds[j] = EPSILON; } m_is_fitted = true; return true; } bool Transform(matrix &data, matrix &out) { if(!m_is_fitted) return false; int rows = int(data.Rows()); int cols = int(data.Cols()); out.Init(rows, cols); for(int j = 0; j < cols; j++) for(int i = 0; i < rows; i++) out[i][j] = (!MathIsValidNumber(data[i][j]) ? DBL_MIN : (data[i][j] - m_means[j]) / m_stds[j]); return true; } bool FitTransform(matrix &data, matrix &out) { if(!Fit(data)) return false; return Transform(data, out); } };
In unserer obigen MQL5-Klasse erreichen wir die Standard-Skalierungsziele, indem wir spaltenweise Mittelwerte und Standardabweichungen innerhalb der Phase Fit() berechnen. Diese Werte werden intern gespeichert, um dann in der Phase Transform() angewendet zu werden, sodass jeder Datenpunkt entsprechend angepasst wird. Sobald ein Merkmal eine Varianz von Null hat, wird ein kleiner Wert ungleich Null, ein Epsilon, hinzugefügt, um eine Division durch Null zu vermeiden. Dies gewährleistet numerische Stabilität.
Bei Handelsanwendungen ist der Standard-Scaler sehr nützlich, wenn es um die Verarbeitung von Datenmerkmalen geht, die in unterschiedlichen Maßstäben oder Zeitrahmen vorliegen. Nehmen Sie zum Beispiel die Spreads. Diese werden in der Regel als Pip-Anteil aufgezeichnet, während Merkmalsdaten wie das Tick-Volumen in Tausendstel-Schritten erfasst werden. Ohne jegliche „Standardisierung“ könnte das Modell der größeren Variable allein aufgrund ihrer Größe unverhältnismäßig viel Aufmerksamkeit schenken. Wenn wir beides „standardisieren“, bewertet das Modell die Merkmale relativ gleichwertig.
Ein einfacher Anwendungsfall kann auch darin bestehen, Eingaben wie logarithmische Renditen, gleitende Durchschnitte und Volatilitätsmaße für ein neuronales Netz vorzubereiten. Durch die Anwendung dieses Skalierers werden die Merkmale normalisiert, was die Konvergenz der Modelle, ob MLPs oder SVMs, beim Training verbessert.
Min-Max-Skalierer
Während der Standard-Skalierer die Merkmale auf einen Mittelwert von Null und eine Einheitsvarianz normiert, skaliert der Min-Max-Skalierer die Merkmale auf einen bestimmten Bereich um, der normalerweise zwischen 0 und 1 liegt. Die Formel für diese Transformation lautet wie folgt:

Diese Transformation sorgt dafür, dass alle Werte innerhalb eines gewünschten Bereichs liegen, was bei Modellen, die empfindlich auf Eingangsgrößen reagieren, sehr nützlich sein kann. In der Regel handelt es sich dabei um neuronale Netze mit sigmoidalen oder tanh-Aktivierungen. Unsere Klasse CMinMaxScaler ist wie folgt in MQL5 implementiert:
// MinMax Scaler class CMinMaxScaler : public IPreprocessor { private: double m_mins[]; double m_maxs[]; double m_new_min; double m_new_max; bool m_is_fitted; public: CMinMaxScaler(double new_min = 0.0, double new_max = 1.0) : m_new_min(new_min), m_new_max(new_max), m_is_fitted(false) {} bool Fit(matrix &data) { int rows = int(data.Rows()); int cols = int(data.Cols()); ArrayResize(m_mins, cols); ArrayResize(m_maxs, cols); for(int j = 0; j < cols; j++) { vector column(rows); for(int i = 0; i < rows; i++) column[i] = data[i][j]; m_mins[j] = column.Min(); m_maxs[j] = column.Max(); if(m_maxs[j] - m_mins[j] == 0.0) m_maxs[j] += EPSILON; } m_is_fitted = true; return true; } bool Transform(matrix &data, matrix &out) { if(!m_is_fitted) return false; int rows = int(data.Rows()); int cols = int(data.Cols()); out.Init(rows, cols); for(int j = 0; j < cols; j++) for(int i = 0; i < rows; i++) { if(!MathIsValidNumber(data[i][j])) out[i][j] = DBL_MIN; else { double scale = (m_new_max - m_new_min) / (m_maxs[j] - m_mins[j]); out[i][j] = (data[i][j] - m_mins[j]) * scale + m_new_min; } } return true; } bool FitTransform(matrix &data, matrix &out) { if(!Fit(data)) return false; return Transform(data, out); } };
Diese Klasse implementiert das Verhalten zunächst durch die Bestimmung des Minimums und Maximums jeder Spalte während der Phase Fit(). Diese ermittelten Werte werden dann während der Phase Transform() zur Neuskalierung der Daten herangezogen. Die Entwickler können nutzerdefinierte Grenzen angeben, z. B. -1 bis +1 anstelle des typischen [0,1], um die Erwartungen an das verwendete Modell zu erfüllen. Wie beim Standard-Skalierer sorgt ein Epsilon dafür, dass es nicht zu Null-Divisionen kommt, selbst wenn alle Werte identisch sind.
Ein praktisches Handelsbeispiel kann die Neuskalierung von Schlusskursen beinhalten, bevor sie in ein ONNX-basiertes LSTM-Modell eingespeist werden. In Anbetracht der Tatsache, dass neuronale Netze am besten funktionieren, wenn die Eingaben in einem engen Bereich begrenzt sind, sorgt die Min-Max-Normalisierung für glatte Gradienten und eine schnellere Konvergenz. Auch bei der Arbeit mit Momentum-Indikatoren oder Oszillatoren mit großen absoluten Werten bringt der Min-Max-Skalierer diese in einen konsistenten und vorhersehbaren Bereich.
Der Hauptvorteil des Min-Max-Skalierers liegt jedoch wohl in seiner Einfachheit und der Fähigkeit, die Form der Prioritätsverteilung zu erhalten. Im Gegensatz zur Standardisierung, die die Varianz des Datensatzes verändert, komprimiert der Min-Max-Skalierer die Werte einfach in einem festen Intervall. Dennoch kann diese Methode empfindlich auf Ausreißer reagieren, da ein einzelner Extremwert die Skalierung des gesamten eingegebenen Merkmalsdatensatzes verzerren kann. Bei stabilen Datensätzen oder bei Verwendung dieses Skalierers in Kombination mit der Entfernung von wilden Ausreißern kann er die beste Wahl bei der Vorbereitung von Merkmalen/Eingabedaten für Deep-Learning-Modelle sein.
Robuster Scaler
Die Märkte sind bekannt für ihre Unberechenbarkeit und das Auftreten von Ausreißern. Plötzliche Nachrichten führen oft dazu, dass sich die Spannen von Bid und Ask dramatisch ausweiten oder das Volumen weit über die historischen Durchschnittswerte ansteigt. In solchen Fällen können Vorverarbeitungsmethoden wie der Standard-Skalierer oder der Min-Max-Skalierer zu Verzerrungen führen, da sich beide unverhältnismäßig stark auf die Mittel- und Extremwerte der Daten stützen. In diesen Situationen erweist sich der robuste Scaler als unschätzbar wertvoll.
Der Robust Scaler zentriert die Daten nach ihrem Median und skaliert sie dann nach ihrem Interquartilsbereich, auch IQR genannt. Dieser IQR ist definiert als die Differenz zwischen dem 75. Perzentil, Q3, und dem 25. Perzentil – Q1. Diese Transformation wird also wie folgt ausgedrückt:

wobei:
- IQR ist der Abstand zwischen dem 75. Perzentil Q3 und dem 25. Q1;
- Median ist der Median des Datensatzes und;
- x ist ein Datenpunkt im Datensatz.
Da dieser Skalierer den Einfluss von Extremwerten ignoriert, ist er tendenziell resistent gegen Ausreißer. Bei Handelsdaten bedeutet dies, dass selbst bei einem seltenen Marktschock die meisten Merkmale für die Modellierung angemessen skaliert bleiben. Unsere MQL5-Implementierung dieser Klasse sieht daher folgendermaßen aus:
// Robust Scaler class CRobustScaler : public IPreprocessor { private: double m_medians[]; double m_iqrs[]; bool m_is_fitted; public: CRobustScaler() : m_is_fitted(false) {} bool Fit(matrix &data) { int rows = int(data.Rows()); int cols = int(data.Cols()); ArrayResize(m_medians, cols); ArrayResize(m_iqrs, cols); for(int j = 0; j < cols; j++) { vector column(rows); for(int i = 0; i < rows; i++) column[i] = data[i][j]; m_medians[j] = column.Median(); double q25 = column.Quantile(0.25); double q75 = column.Quantile(0.75); m_iqrs[j] = q75 - q25; if(m_iqrs[j] == 0.0) m_iqrs[j] = EPSILON; } m_is_fitted = true; return true; } bool Transform(matrix &data, matrix &out) { if(!m_is_fitted) return false; int rows = int(data.Rows()); int cols = int(data.Cols()); out.Init(rows, cols); for(int j = 0; j < cols; j++) for(int i = 0; i < rows; i++) out[i][j] = (!MathIsValidNumber(data[i][j]) ? DBL_MIN : (data[i][j] - m_medians[j]) / m_iqrs[j]); return true; } bool FitTransform(matrix &data, matrix &out) { if(!Fit(data)) return false; return Transform(data, out); } };
Unsere obige MQL5-Klasse CRobustScaler berechnet Mediane und Quartile während der Phase Fit() und wendet dann die Skalierung innerhalb des Transform()-Schrittes an. Eine Epsilon-Sicherung wird auch verwendet, um die Stabilität für den Fall zu gewährleisten, dass der IQR Null ist. Unsere Implementierung ermöglicht die Verarbeitung von Datensätzen, die das Potenzial haben, Modelle aufgrund von unregelmäßigen Marktspitzen zu täuschen.
Zur Veranschaulichung: Stellen Sie sich vor, Sie trainieren ein Modell unter Verwendung des Tick-Volumens. In „normalen“ Börsensitzungen bewegen sich die Volumina in einem stabilen Bereich, doch wenn eine Pressemitteilung die Runde macht, können sie sich leicht verzehnfachen. Eine Standardisierung oder Min-Max-Skalierung würde die Merkmalsverteilung strecken und die „normalen“ Werte auf eine sehr enge und fast unbedeutende Bandbreite komprimieren. Der Robust Scaler konzentriert sich jedoch auf die mittleren 50 Prozent der Daten, wodurch sichergestellt wird, dass die Mehrzahl der in ein Modell eingegebenen Merkmalsmuster intakt bleibt. Dies macht es daher für Deep Learning geeignet, wo Modelle mit volatilen, stark schwankenden Verteilungen, wie sie häufig auf Devisen- oder einigen Kryptomärkten zu finden sind, arbeiten können.
One-Hot-Codierung
Im Handel sind nicht alle Merkmale numerisch. Einige sind oft diskreter oder kategorischer Natur, da sie sich auf bestimmte Formen des Marktes beziehen. Wir könnten zum Beispiel die Zeit in Handelssitzungen in Asien, Europa und den USA einteilen. Oder wir können die Marktregime in steigende, fallende Trends und Seitwärtsbewegung einteilen. Diese Beispiele sind nicht erschöpfend, aber der Punkt ist, dass Modelle des maschinellen Lernens solche kategorischen, nicht-numerischen Werte nicht von Haus aus interpretieren können. Und um dies noch zu unterstreichen: Die einfache Zuweisung ganzer Zahlen zu jeder Kategorie, z. B. Asien = 0, Europa = 1 und USA = 2, führt zu einem falschen Ordnungssinn und kann das Modell verfälschen.
Die Lösung für dieses Problem ist häufig die One-Hot-Codierung. Hier wird jede Kategorie in einen binären Vektor umgewandelt – zum Beispiel werden unsere Marktsitzungsklassifizierungen für Asien, Europa und die USA zu [1,0,0], [0,1,0] bzw. [0,0,1]. Dies ermöglicht es den Modellen, Kategorien zu unterscheiden, ohne notwendigerweise eine ordinale Beziehung anzunehmen. Unsere Implementierung in MQL5 sieht folgendermaßen aus:
// One-Hot Encoder class COneHotEncoder : public IPreprocessor { private: int m_column; double m_categories[]; bool m_is_fitted; public: COneHotEncoder(int column) : m_column(column), m_is_fitted(false) {} bool Fit(matrix &data) { int rows = int(data.Rows()); vector values; int unique = 0; for(int i = 0; i < rows; i++) { if(!MathIsValidNumber(data[i][m_column])) continue; int idx = CVectorUtils::BinarySearch(values, data[i][m_column]); if(idx == -1) { values.Resize(unique + 1); values[unique] = data[i][m_column]; unique++; } } values.Swap(m_categories); //ArrayCopy(m_categories, values); m_is_fitted = true; return true; } bool Transform(matrix &data, matrix &out) { if(!m_is_fitted) return false; int rows = int(data.Rows()); int cols = int(data.Cols()); int cat_count = ArraySize(m_categories); if(data.Cols() == cols - 1 + cat_count) return false; out.Resize( rows, cols - 1 + cat_count); out.Fill(0.0); for(int i = 0; i < rows; i++) { int out_col = 0; for(int j = 0; j < cols; j++) { if(j == m_column) continue; out[i][out_col] = data[i][j]; out_col++; } for(int k = 0; k < cat_count; k++) if(data[i][m_column] == m_categories[k]) { out[i][out_col + k] = 1.0; break; } } m_is_fitted = true; return true; } bool FitTransform(matrix &data, matrix &out) { if(!Fit(data)) return false; return Transform(data, out); } };
Die obige Klasse COneHotEncoder implementiert diese Transformation. In der Phase Fit() werden eindeutige Kategorien in der ausgewählten Merkmalsspalte identifiziert. In der Phase Transform() wird die kategoriale Spalte dann durch mehrere binäre Spalten ersetzt, deren Anzahl die Anzahl der Kategorien darstellt. Das Ergebnis ist eine erweiterte Merkmalsmatrix, in der kategoriale Informationen in ein modellfreundliches Format eingebettet werden.
Um zu zeigen, wie einfallsreich dies sein kann, können wir bei den obigen Beispielen für die Verschlüsselung von Handelssitzungen bleiben. Wenn wir rohe Sitzungszahlen von 0 für Asien, 1 für Europa und 2 für die USA verwenden, könnte ein neuronales Netz den Unterschied zwischen Asien und den USA als größer interpretieren als den zwischen Asien und Europa in Bezug auf Metriken, die nicht unbedingt zeitbezogen sind. Durch die Einführung einer Hot-Codierung erhält jede Sitzung eine unabhängige Repräsentation, und das Modell wird freier, um für jede Kategorie unterschiedliche Verhaltensweisen zu lernen. Dies ist für Handelsmodelle von entscheidender Bedeutung, da verschiedene Sitzungen beispielsweise eine einzigartige Liquidität, Volatilität und Richtungssignale aufweisen können.
Alles zusammenfügen
Scaler und Encoder sind für sich genommen sehr leistungsfähig, doch ihr wahres Potenzial liegt darin, dass sie gemeinsam in einem Workflow genutzt werden. Dies ist die Vorverarbeitungspipeline. Indem wir mehrere Transformationen zusammenfügen, stellen wir sicher, dass jeder Datensatz genau die gleiche Behandlung erfährt, bevor er in das Modell gelangt.
Stellen Sie sich folgendes Szenario vor. Angenommen, wir bereiten einen Datensatz vor, für den wir 4 Schlüsselmerkmale ermittelt haben:
- Schlusskurs
- Tick-Volumen
- Spread
- Handelssitzungs-Bucket (kategorisch)
Der erste Schritt besteht in der Regel darin, fehlende Werte zu behandeln. Dies ist etwas, das einige MQL5-Entwickler übergehen könnten, da die Daten oft unverändert vom Broker übernommen werden. Man sollte jedoch nie davon ausgehen, dass alle erforderlichen Daten vorhanden sind. Wenn beispielsweise die Tick-Volumen-Daten unvollständig sind, können wir die Funktion AddImputeMedian(1) anwenden, wobei 1 den Index des Tick-Volumen-Merkmals innerhalb unserer 4 verwendeten Merkmale darstellt. Dadurch würden fehlende Einträge durch den Median der Spalte ersetzt. Wenn im Laufe der Handelssitzung Daten fehlen, können wir die Funktion AddImputeMode(3) anwenden, um die häufigste Handelssitzung als Kompromiss einzutragen. Diese Optionen können vom Entwickler je nach System/Modell angepasst werden, was hier nur zur Veranschaulichung dargestellt wird.
Nach der Beseitigung der fehlenden Daten besteht der nächste Schritt darin, die kategorialen Daten in ein „unparteiisches“ binäres Format umzuwandeln. Um dies zu erreichen, würden wir die Funktion AddOneHotEncoder(3) anwenden, um die Sitzungsspalte in binäre Vektoren zu erweitern und gleichzeitig sicherzustellen, dass jede Sitzung eindeutig dargestellt wird.
Der nächste Schritt, oder Schritt 3, könnte die Anwendung eines Scalers sein. Bei unserem Datensatz können wir zwischen den Funktionen AddStandardScaler(), AddRobustScaler() oder AddminMaxScaler() wählen. Letztlich soll dieser Schritt sicherstellen, dass alle numerischen Merkmale so angepasst werden, dass sie in ihrem Maßstab vergleichbar sind. Sobald diese Schritte zur Pipeline hinzugefügt sind, müssen wir die Funktion FitPipeline() für den Trainingsdatensatz aufrufen. Diese Funktion würde alle notwendigen Parameter wie Mittelwert, Median, Modus und Kategoriezuordnungen lernen. Später können wir dann die Funktion TransformPipeline() sowohl für die Trainings- als auch für die Testdatensätze (oder für die Optimierung und den Forward Walk) aufrufen, wodurch die Konsistenz gewährleistet wird, ohne dass zukünftige Informationen in den Trainingsprozess einfließen.
Am Ende ist unsere Ausgabe eine saubere, skalierte und kodierte Matrix von Merkmalen, die sofort für die Verwendung in einem Deep-Learning-Modell bereit ist – egal, ob sie mit MQL5 kodiert oder mit ONNX importiert wurde. Diese Pipeline macht die Vorverarbeitung transparent, reproduzierbar und modular, was es den Entwicklern ermöglicht, sich mehr auf Signale oder Strategien und weniger auf die Datenverarbeitung zu konzentrieren. Eine Demonstration unserer Pipeline-Klassen zur Vorbereitung von Daten für ein Modell kann mit dem Skript durchgeführt werden, das am Ende des Artikels angehängt ist. Ein Testlauf mit dem Symbol USD JPY zeigt uns die folgenden Protokolle:
2025.09.12 17:05:50.150 Pipeline_Illustration (USDJPY,H4) RAW (first 6 rows) [rows=2999, cols=4] 2025.09.12 17:05:50.150 Pipeline_Illustration (USDJPY,H4) 147.625000, 6894.000000, 20.000000, 2.000000 2025.09.12 17:05:50.150 Pipeline_Illustration (USDJPY,H4) 147.837000, 14153.000000, 20.000000, 1.000000 2025.09.12 17:05:50.150 Pipeline_Illustration (USDJPY,H4) 147.885000, 16794.000000, 20.000000, 1.000000 2025.09.12 17:05:50.150 Pipeline_Illustration (USDJPY,H4) 147.489000, 8010.000000, 20.000000, 0.000000 2025.09.12 17:05:50.150 Pipeline_Illustration (USDJPY,H4) 147.219000, 6710.000000, 20.000000, 0.000000 2025.09.12 17:05:50.150 Pipeline_Illustration (USDJPY,H4) 147.194000, 13686.000000, 20.000000, 3.000000 2025.09.12 17:05:50.163 Pipeline_Illustration (USDJPY,H4) TRANSFORMED (FitTransform on all) [rows=2999, cols=32] 2025.09.12 17:05:50.163 Pipeline_Illustration (USDJPY,H4) 0.353976, 0.081606, 0.052632, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, ... 2025.09.12 17:05:50.163 Pipeline_Illustration (USDJPY,H4) 0.363616, 0.167533, 0.052632, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, ... 2025.09.12 17:05:50.163 Pipeline_Illustration (USDJPY,H4) 0.365798, 0.198795, 0.052632, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, ... 2025.09.12 17:05:50.163 Pipeline_Illustration (USDJPY,H4) 0.347792, 0.094816, 0.052632, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, ... 2025.09.12 17:05:50.163 Pipeline_Illustration (USDJPY,H4) 0.335516, 0.079428, 0.052632, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, ... 2025.09.12 17:05:50.163 Pipeline_Illustration (USDJPY,H4) 0.334379, 0.162005, 0.052632, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, ... 2025.09.12 17:05:50.170 Pipeline_Illustration (USDJPY,H4) TRAIN (after TransformPipeline) [rows=2249, cols=32] 2025.09.12 17:05:50.170 Pipeline_Illustration (USDJPY,H4) 0.353976, 0.081606, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, ... 2025.09.12 17:05:50.170 Pipeline_Illustration (USDJPY,H4) 0.363616, 0.167533, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, ... 2025.09.12 17:05:50.170 Pipeline_Illustration (USDJPY,H4) 0.365798, 0.198795, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, ... 2025.09.12 17:05:50.170 Pipeline_Illustration (USDJPY,H4) 0.347792, 0.094816, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, ... 2025.09.12 17:05:50.170 Pipeline_Illustration (USDJPY,H4) 0.335516, 0.079428, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, ... 2025.09.12 17:05:50.170 Pipeline_Illustration (USDJPY,H4) 0.334379, 0.162005, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, ... 2025.09.12 17:05:50.170 Pipeline_Illustration (USDJPY,H4) TEST (after TransformPipeline) [rows=750, cols=32] 2025.09.12 17:05:50.170 Pipeline_Illustration (USDJPY,H4) 0.538217, 0.098806, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, ... 2025.09.12 17:05:50.170 Pipeline_Illustration (USDJPY,H4) 0.536307, 0.280804, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, ... 2025.09.12 17:05:50.170 Pipeline_Illustration (USDJPY,H4) 0.545628, 0.163082, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, ... 2025.09.12 17:05:50.170 Pipeline_Illustration (USDJPY,H4) 0.540217, 0.121817, -0.028571, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, ... 2025.09.12 17:05:50.170 Pipeline_Illustration (USDJPY,H4) 0.533852, 0.093858, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, ... 2025.09.12 17:05:50.170 Pipeline_Illustration (USDJPY,H4) 0.532215, 0.071675, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, ... 2025.09.12 17:05:50.170 Pipeline_Illustration (USDJPY,H4) Preprocessing pipeline demo complete.
Vergleich von Scalern im Handel
Die Auswahl des richtigen Scalers ist seltener eine Einheitslösung. Jeder Scaler hat seine Stärken und Schwächen, und die optimale Wahl hängt in der Regel von der Art der Daten und dem Verhalten des Zielmarktes ab. Der nachstehende tabellarische Vergleich kann dazu beitragen, zu verdeutlichen, wo jeder Scaler seine Stärken hat:
| Scaler | Stärken | Schwächen | Am besten für |
|---|---|---|---|
| Standard-Scaler | Geeignet für Gauß-ähnliche Daten. Erzeugt Merkmale mit Mittelwert Null und Varianzeinheit. | Empfindlich gegenüber Ausreißern. | Gut im Umgang mit technischen Indikatoren oder volatilitätsbasierten Lösungen. |
| Min-Max-Skalierer | Geeignet für begrenzte Aktivierungen. Erzeugt Daten in einem festen Bereich, normalerweise [0,1]. | Sie ist sehr empfindlich gegenüber Ausreißern, da extreme Werte die Skalierung verzerren. | Ideal für Preiswerte und Merkmale, die ein Sigmoid/Tanh-Netzwerk speisen |
| Robuster Skalierer | Die Verwendung von Medianen und IQR macht sie weniger unempfindlich gegenüber Ausreißern. | Hat das Potenzial, oft die absolute Skalierung zu verlieren. | Geeignet für stark schwankende Daten, Tick-Volumen und Spreads in volatilen Marktsituationen. |
Wenn wir beispielsweise ein einfaches neuronales Netz auf normalisierte Preisrenditen und Volatilitätsindikatoren trainieren, dann wäre der Standard-Scaler wahrscheinlich die geeignete Wahl, da er die Vergleichbarkeit der Merkmale gewährleistet. Wenn es sich bei dem Modell beispielsweise um ein in Python trainiertes ONNX-LSTM mit sigmoiden Aktivierungen handelt, könnte der Min-Max-Skalierer besser geeignet sein, da er dazu neigt, sich an den begrenzten Bereich der Aktivierung anzupassen. Auf der anderen Seite, wenn wir das Handelsverhalten während eines aufsehenerregenden Nachrichtenereignisses modellieren, bei dem die Spreads und Volumina dramatisch ansteigen können, dann kann der robuste Skalierer dazu beitragen, die zentrale Struktur der Daten zu erhalten und gleichzeitig den Einfluss dieser Anomalie zu minimieren.
Die wichtigste Erkenntnis hier ist, dass die Vorverarbeitung nicht als nachträglicher Gedanke behandelt werden sollte. Die Wahl des Skalierers wirkt sich direkt darauf aus, wie die Merkmale in den Lernschritten interagieren, und kann den Unterschied zwischen einem Modell, das gut verallgemeinert, und einem, das verzerrt oder instabil ist, ausmachen.
Aufbereitung von Daten für Deep Learning
Was wir als Deep-Learning-Modelle oder neuronale Netze bezeichnen, ob in MQL5 kodiert oder über ONNX importiert, neigt dazu, strenge Erwartungen an ihre Eingaben zu stellen. Ein in Python auf normalisierten Daten trainiertes Modell wird beispielsweise unterdurchschnittlich abschneiden oder ganz versagen, wenn es mit rohen und nicht skalierten Merkmalen von MQL5-Daten präsentiert wird. Dies unterstreicht die Bedeutung von Vorverarbeitungspipelines als etwas, das streng genommen nicht optional ist, sondern für seriöse Handels- und Arbeitsabläufe unerlässlich ist.
Betrachten Sie ein ONNX-Modell, das in TensorFlow auf min-max normalisierten Preisen trainiert wurde. Wenn dasselbe Modell später in MQL5 eingesetzt wird, aber mit OHLC-Rohdaten, ist es selbstverständlich, dass die Gewichte nicht mit der Skalierung dieser Eingaben übereinstimmen und dies zu schlechten Prognosen führt. Die Anwendung des Pipelineschrittes CMinMaxScaler() mit identischen Min/Max-Einstellungen wie beim Python-Training gewährleistet die Konsistenz zwischen den beiden Umgebungen.
Neben der Skalierung erfüllt die kategoriale Kodierung einen weiteren wichtigen Zweck. Ein neuronales Netz, das mit einer heißen Kodierung trainiert wurde, wie z. B. das oben betrachtete Beispiel der Handelssitzungen, wird bei der Inferenz dasselbe Binärformat erwarten. Wenn die Kodierung inkonsistent ist, d. h. wenn z. B. Asien beim Training auf [0,1,0] abgebildet wurde, jetzt aber auf [1,0,0] liegt, werden die Vorhersagen des Modells an Bedeutung verlieren. Durch den Einsatz einer Pipeline, die gelernte Kategorien protokolliert, können wir solche Risiken abmildern.
Ausdauer ist ein weiterer wichtiger Aspekt. Parameter wie Mittelwerte, Minima, Maxima sowie Kategoriezuordnungen müssen nach dem Training gespeichert und dann während der Inferenz wieder angewendet werden. Ohne diese Konstanz besteht die Gefahr, dass die Umschulung oder die Schlussfolgerung auseinanderdriften. In Python bietet SCIKIT-LEARN eine Serialisierung mit Modulen wie joblib oder pickle. In MQL5 konnten Entwickler einen ähnlichen Effekt erzielen, indem sie die Pipeline-Zustandsvariablen aus ihrem Array-Format in bin- oder csv-Dateien speicherten und sie während der Initialisierung des Expert Advisors neu laden ließen.
Herausforderungen und bewährte Praktiken
Dank dieser Vorverarbeitungspipelines sind die MQL5-Arbeitsabläufe zwar rigoros, doch bringt dies auch einige Herausforderungen mit sich, die zur Gewährleistung der Zuverlässigkeit angegangen werden müssen. Wir betrachten fünf davon. Zunächst geht es um die Behandlung von NaNs und den Umgang mit fehlenden Daten. Finanzdatensätze enthalten in der Regel fehlende Werte aufgrund von Marktschließungen, unvollständigen Tickdaten oder unregelmäßigen Brokerdateneinspeisungen. In MQL5-Pipelines werden oft Platzhalter wie DBL_MIN eingesetzt, um diese fehlenden Einträge zu kennzeichnen. Daher ist es wichtig, diese Werte konsistent zu imputieren, indem Alternativen wie Mediane verwendet werden, wenn es sich um numerische Werte handelt, oder der Modus, wenn es sich um diskrete/kategoriale Daten handelt. Dadurch wird verhindert, dass ungültige Daten in das Modell eingegeben werden.
Das zweite Problem bei diesen Pipelines in MQL5 könnte darin bestehen, Datenverluste zu verhindern. Ein häufiger Fehler ist die Anpassung von Skalierern oder Kodierern an den gesamten Datensatz, bevor eine Aufteilung in Trainings-, Validierungs- und Testdatensätze vorgenommen wird. Diese Praxis kann dazu führen, dass zukünftige Informationen in den Ausbildungsprozess einfließen und die Leistungskennzahlen künstlich aufgebläht werden. Der Strategietester von MQL5 ist von Haus aus so aufgebaut, dass er ausschließlich aktuelle oder alte Preisinformationen liest. Wenn jedoch zusätzliche Daten verwendet und in ein Modell eingespeist werden sollen, müssen zusätzliche Prüfungen durchgeführt werden, um sicherzustellen, dass solche Lecks nicht auftreten. Am besten ist es, die Pipeline immer nur auf die Trainingsdaten anzupassen() und dann die Trainings- und Testdaten getrennt zu transformieren().
Drittens kann die Skalierung gemischter Datentypen eine Herausforderung darstellen. Wenn Datensätze sowohl numerische als auch kategoriale Merkmale enthalten, müssen Transformationen mit Bedacht vorgenommen werden. Kodierer müssen vor Skalierern verwendet werden, um sicherzustellen, dass neu erstellte Binärspalten ordnungsgemäß in die neue numerische Matrix integriert werden.
Viertens muss die Fehlersuche bei Transformationen beachtet werden. Um die Korrektheit der Transformationsausgaben zu überprüfen, ist es sinnvoll, die ersten Zeilen der transformierten Daten zu drucken. Werkzeuge zur Fehlersuche wie Print() können schnell zeigen, ob sich Kodierung und Skalierung wie erwartet verhalten.
Schließlich kann nicht immer davon ausgegangen werden, dass die Reproduzierbarkeit gewährleistet ist. Um die Konsistenz zwischen den Experimenten zu gewährleisten, müssen Pipeline-Parameter, Minimalwerte, Mediane und Kategoriezuordnungen zusammen mit dem trainierten Modell gespeichert werden. Damit ist gewährleistet, dass genau dieselbe Vorverarbeitung beim Backtesting, beim Live-Handel oder sogar beim Umlernen angewendet werden kann.
Die Beachtung dieser fünf Hinweise kann Entwicklern helfen, häufige Fallstricke zu vermeiden, indem sie dafür sorgen, dass Pipelines in MQL5 genauso robust bleiben wie ihre Python-Gegenstücke.
Schlussfolgerung
Die Vorverarbeitung ist sicherlich kein glamouröser Teil des maschinellen Lernens, aber sie ist wohl einer der Grundpfeiler. Ohne sorgfältige Vorbereitung wird selbst das fortschrittlichste Deep-Learning-Modell straucheln, wenn es mit rohen, nicht skalierten oder inkonsistent kodierten Handelsdaten konfrontiert wird. Für Entwickler, die mit MetaTrader 5 vertraut sind, war diese Herausforderung traditionell ein Hindernis für die vollständige Nutzung von Workflows für maschinelles Lernen. Im Gegensatz zu Python, das das robuste Toolkit von SCIKIT-LEARN bietet, stellt MQL5 keine integrierten Vorverarbeitungspipelines zur Verfügung. Die Lösung könnte, wie gezeigt, im Aufbau modularer, wiederverwendbarer Vorverarbeitungspipelines innerhalb von MQL5 liegen.
Wenn diese nicht als optionale Hilfsmittel, sondern als wesentliche Komponenten des KI-Workflows behandelt werden, werden die Entwickler ihre Handelssysteme mit der Strenge der Praxis des maschinellen Lernens in Einklang bringen, sodass die meisten Modelle – ob nativ oder ONNX-basiert – die bestmögliche Grundlage für den Erfolg haben.
| Name | Beschreibung |
|---|---|
| PipeLine.mqh | Basisklasse für Pipeline-Funktionalität |
| Pipeline_Illustration.mq5 | Skript, das auf die Basisklasse verweist und deren Verwendung zeigt |
Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/19544
Warnung: Alle Rechte sind von MetaQuotes Ltd. vorbehalten. Kopieren oder Vervielfältigen untersagt.
Dieser Artikel wurde von einem Nutzer der Website verfasst und gibt dessen persönliche Meinung wieder. MetaQuotes Ltd übernimmt keine Verantwortung für die Richtigkeit der dargestellten Informationen oder für Folgen, die sich aus der Anwendung der beschriebenen Lösungen, Strategien oder Empfehlungen ergeben.
Die Übertragung der Trading-Signale in einem universalen Expert Advisor.
Die Grenzen des maschinellen Lernens überwinden (Teil 4): Überwindung des irreduziblen Fehlers durch mehrere Prognosehorizonte
Eine alternative Log-datei mit der Verwendung der HTML und CSS
Entwicklung des Price Action Analysis Toolkit (Teil 41): Aufbau eines statistischen Preis-Level EA in MQL5
- 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.