Neuronale Netze im Handel: Ein multimodaler, werkzeuggestützter Agent für Finanzmärkte (letzter Teil)
Einführung
Im vorangegangenen Artikel haben wir uns mit dem FinAgent-Framework beschäftigt – einem fortschrittlichen Tool für die Datenanalyse und Entscheidungsunterstützung auf den Finanzmärkten. Seine Entwicklung konzentriert sich auf die Schaffung eines effizienten Mechanismus für die Entwicklung von Handelsstrategien und die Minimierung von Risiken in einem komplexen und sich schnell verändernden Marktumfeld. Die Architektur von FinAgent besteht aus fünf miteinander verbundenen Modulen, die jeweils spezielle Funktionen ausführen, um die allgemeine Anpassungsfähigkeit des Systems zu gewährleisten.
Das Marktanalysemodul ist für die Extraktion und Verarbeitung von Daten aus verschiedenen Quellen zuständig, darunter Kurscharts, Marktnachrichten und Berichte. Innerhalb dieses Moduls identifiziert das System stabile Muster, die zur Vorhersage der Preisdynamik verwendet werden können.
Die Reflexionsmodule spielen eine entscheidende Rolle für den Anpassungs- und Lernprozess des Modells. Das Low-Level-Reflexionsmodul analysiert Interdependenzen zwischen den aktuellen Marktsignalen und verbessert so die Genauigkeit der kurzfristigen Prognosen. Das High-Level-Reflexionsmodul hingegen arbeitet mit langfristigen Trends – unter Einbeziehung historischer Daten und der Ergebnisse vergangener Handelsentscheidungen – um die Strategie auf der Grundlage der gesammelten Erfahrungen anzupassen.
Das Speichermodul ermöglicht die langfristige Speicherung großer Mengen von Marktdaten. Durch den Einsatz moderner Vektorähnlichkeitstechnologien wird das Rauschen minimiert und die Genauigkeit der Informationsabfrage verbessert, was besonders wichtig für die Entwicklung langfristiger Strategien und die Aufdeckung komplexer Zusammenhänge ist.
Das Herzstück des Systems ist das Entscheidungsfindungsmodul, das die Ergebnisse aller anderen Komponenten integriert. Auf der Grundlage aktueller und historischer Daten generiert es optimale Handelsempfehlungen. Außerdem ist das Modul durch die Integration von Expertenwissen und traditionellen Indikatoren in der Lage, ausgewogene und fundierte Vorschläge zu machen.
Die ursprüngliche Visualisierung des FinAgent-Rahmens ist unten zu sehen.

Im vorigen Artikel haben wir damit begonnen, die von den Autoren des FinAgent-Frameworks vorgeschlagenen Ansätze mit MQL5 umzusetzen. Wir haben Algorithmen für die Low-Level- und High-Level-Reflexionsmodule eingeführt, die als die Objekte CNeuronLowLevelReflection und CNeuronHighLevelReflection implementiert sind. Diese Module analysieren die Marktsignale, die Historie der Handelsentscheidungen und die tatsächlich erzielten finanziellen Ergebnisse und ermöglichen es dem Agenten, seine Verhaltenspolitik an die sich ändernden Marktbedingungen anzupassen. Sie ermöglichen auch flexible Reaktionen auf dynamische Trendverschiebungen und helfen, wichtige Muster in den Daten zu erkennen.
Eine Besonderheit unserer Implementierung ist die Integration von Speicherblöcken direkt in die Reflexionsobjekte. Dieser Ansatz unterscheidet sich von der ursprünglichen Architektur des Frameworks, bei der der Speicher für alle Informationsströme als separates Modul implementiert wurde. Indem wir den Speicher in die Reflexionskomponenten selbst einbetten, vereinfachen wir die Konstruktion von Datenflüssen und Interaktionen zwischen den verschiedenen Elementen des Rahmens.
In Fortsetzung dieser Arbeit werden wir nun die Implementierung mehrerer Schlüsselmodule untersuchen, von denen jedes eine einzigartige Rolle innerhalb der gesamten Systemarchitektur spielt:
- Das Marktanalysemodul wurde entwickelt, um Daten aus einer Vielzahl von Quellen zu verarbeiten, darunter Finanzberichte, Nachrichten und Börsenkurse. Es bringt multimodale Daten in ein einheitliches Format und extrahiert stabile Muster, die zur Vorhersage der zukünftigen Marktdynamik genutzt werden können.
- Auxiliary Tools, die auf Vorwissen basieren, unterstützen die Analyse und Entscheidungsfindung durch historische Muster, statistische Daten und Expertenbewertungen. Sie bieten auch eine logische Interpretierbarkeit der Systementscheidungen.
- Entscheidungsunterstützungssystem, das die Ergebnisse aller Module konsolidiert, um eine adaptive und optimale Handelsstrategie zu entwickeln. Dieses System bietet Handlungsempfehlungen in Echtzeit und ermöglicht es Händlern und Analysten, zeitnah auf veränderte Marktbedingungen zu reagieren und fundiertere Entscheidungen zu treffen.
Das Marktanalysemodul spielt eine zentrale Rolle im System, da es für die Vorverarbeitung und Zusammenführung der Daten zuständig ist. Dieser Schritt ist besonders wichtig, um versteckte Muster aufzudecken, die mit herkömmlichen Datenanalysemethoden nur schwer zu erkennen sind. Die Autoren von FinAgent setzten große Sprachmodelle (LLMs) ein, um Schlüsselaspekte der Daten zu extrahieren und eine Dimensionskompression durchzuführen. In unserer Implementierung haben wir jedoch auf LLMs verzichtet und uns stattdessen auf spezielle Modelle für die Zeitreihenanalyse konzentriert, die eine höhere Präzision und Leistung bieten. In dieser Artikelserie haben wir mehrere Rahmenwerke für die Analyse und Vorhersage multivariater Zeitreihen vorgestellt. Jeder von ihnen könnte hier angewendet werden. Für die Zwecke dieses Artikels haben wir ein Transformatormodell mit segmentierter Aufmerksamkeit gewählt, das über die Klasse CNeuronPSformer implementiert wird.
Dies ist jedoch keineswegs die einzige mögliche Lösung. Der FinAgent-Rahmen unterstützt multimodale Eingabedaten. So können wir nicht nur mit verschiedenen Algorithmen zur Darstellung und Analyse von Zeitreihen experimentieren, sondern sie auch kombinieren. Dieser Ansatz erweitert die Fähigkeiten des Systems erheblich, ermöglicht ein detaillierteres Verständnis der Marktprozesse und trägt zur Entwicklung hocheffektiver, adaptiver Handelsstrategien bei.
Das Modul der Auxiliary Tools integriert Vorwissen über die analysierte Umgebung in die Gesamtmodellarchitektur. Diese Komponente erzeugt analytische Signale, die auf klassischen Indikatoren wie gleitenden Durchschnitten, Oszillatoren und volumenbasierten Indikatoren basieren. Sie alle haben ihre Wirksamkeit im algorithmischen Handel längst bewiesen. Das Modul ist jedoch nicht nur auf Standardwerkzeuge beschränkt.
Darüber hinaus erhöht die Generierung von Signalen durch genau definierte Regeln, die auf den Messwerten technischer Indikatoren basieren, die Interpretierbarkeit der Entscheidungen des Modells und verbessert ihre Zuverlässigkeit und Wirksamkeit. Dies ist ein entscheidender Faktor für die strategische Planung und das Risikomanagement.
Das Modul der Auxiliary Tools
Die Entwicklung eines Moduls zur Signalerzeugung auf der Grundlage der Ergebnisse klassischer Indikatoren innerhalb eines neuronalen Modells ist eine weitaus komplexere Aufgabe, als es auf den ersten Blick erscheinen mag. Die Hauptschwierigkeit liegt nicht in der Interpretation der Signale, sondern in der Bewertung der Metriken, die in das Modell einfließen.
Bei traditionellen Strategien hängen die Signalbeschreibungen direkt von den tatsächlichen Messwerten der Indikatoren ab. Diese Werte gehören jedoch oft zu völlig unzusammenhängenden und unvergleichbaren Verteilungen, was die Modellkonstruktion vor erhebliche Herausforderungen stellt. Dieser Faktor verringert die Trainingseffizienz erheblich, da sich die Algorithmen an die Analyse heterogener Daten anpassen müssen. Die Folge sind längere Bearbeitungszeiten, eine geringere Prognosegenauigkeit und andere nachteilige Auswirkungen. Aus diesem Grund haben wir beschlossen, nur normalisierte Daten in unseren Modellen zu verwenden.
Der Normalisierungsprozess ermöglicht es, alle analysierten Merkmale auf einen gemeinsamen, vergleichbaren Bereich zu skalieren, was wiederum die Qualität des Modelltrainings erheblich verbessert. Dieser Ansatz minimiert das Risiko von Verzerrungen, die durch Unterschiede in den Messeinheiten oder zeitabhängige Schwankungen verursacht werden. Ein wichtiger Vorteil der Normalisierung besteht darin, dass sie eine tiefere Datenanalyse ermöglicht, da die Eingaben in dieser Form für Algorithmen des maschinellen Lernens viel berechenbarer und handhabbarer werden.
Es ist jedoch zu beachten, dass die Normalisierung die Signalerzeugung bei klassischen Strategien erheblich erschwert. Diese Strategien wurden ursprünglich für die Arbeit mit Rohdaten entwickelt und gehen von festen Schwellenwerten für die Interpretation der Indikatoren aus. Bei der Normalisierung werden die Daten transformiert, was zu undefinierten Verschiebungen der Schwellenwerte führt. Darüber hinaus macht es die Normalisierung unmöglich, Signale zu erzeugen, die auf der Kreuzung zweier Linien in einem klassischen Indikator basieren, da es keine Garantie dafür gibt, dass sich beide Linien synchron verschieben. Dies führt dazu, dass die erzeugten Signale verzerrt werden oder gar nicht erscheinen. Daraus ergibt sich die Notwendigkeit, neue Ansätze für die Interpretation der Indikatorergebnisse zu entwickeln.
Hier haben wir, glaube ich, eine einfache, aber konzeptionell gute Lösung gefunden. Das Wesentliche liegt darin, dass während der Normalisierung alle analysierten Merkmale so transformiert werden, dass sie einen Mittelwert von Null und eine Einheitsvarianz haben. Dadurch wird jede Variable mit anderen vergleichbar und kann als eine Art Oszillator interpretiert werden. Dies bietet ein universelles Signalinterpretationsschema: Werte über 0 werden als Kaufsignale behandelt, Werte unter 0 als Verkaufssignale. Es ist auch möglich, Schwellenwerte einzuführen und „Korridore“ zu schaffen, die schwache oder mehrdeutige Signale herausfiltern. Dies minimiert Fehlalarme, erhöht die Analysegenauigkeit und unterstützt eine fundiertere Entscheidungsfindung.
Wir berücksichtigen auch die Möglichkeit von invertierten Signalen für bestimmte Merkmale. Dieses Problem kann durch die Verwendung von trainierbaren Parametern gelöst werden, die sich an historische Daten anpassen.
Die Anwendung dieses Ansatzes schafft die Grundlage für die Entwicklung von Modellen, die sich effektiv an veränderte Bedingungen anpassen und genauere, zuverlässigere Signale erzeugen können.
Um diese Methode der Signalerzeugung zu implementieren, wird zunächst der Kernel von MoreLessEqual auf der Seite von OpenCL konstruiert. In diesem Fall wurde ein einfacher Algorithmus mit einem festen Schwellenwert implementiert.
Die Kernelparameter enthalten Zeiger auf zwei gleich große Datenpuffer. Der eine enthält die Eingabedaten, während der zweite die erzeugten Signale speichert, die durch einen von drei numerischen Werten dargestellt werden:
- -1 – verkaufen
- 0 – kein Signal
- 1 – kaufen
__kernel void MoreLessEqual(__global const float * input, __global float * output) { const size_t i = get_global_id(0); const float value = IsNaNOrInf(input[i], 0); float result = 0;
Innerhalb des Kernelkörpers identifizieren wir den aktuellen Operationsthread und lesen den entsprechenden Eingabewert sofort in eine lokale Variable. Ein obligatorischer Schritt ist die Validierung der Eingaben: alle ungültigen Daten werden automatisch durch 0 ersetzt, um Fehler bei der weiteren Verarbeitung zu vermeiden.
Dann wird eine lokale Variable eingeführt, um Zwischenergebnisse zu speichern. Zu Beginn wird dieser Variablen ein Wert zugewiesen, der das Fehlen eines Signals anzeigt.
Als Nächstes prüfen wir den absoluten Wert der analysierten Variablen. Um ein Signal zu erzeugen, muss dieser Wert den angegebenen Schwellenwert überschreiten.
if(fabs(value) > 1.2e-7) { if(value > 0) result = 1; else result = -1; } output[i] = result; }
Positive Werte oberhalb des Schwellenwerts führen zu einem Kaufsignal, während negative Werte unterhalb des Schwellenwerts ein Verkaufssignal darstellen. Das entsprechende Kennzeichen wird in der lokalen Variable gespeichert. Bevor der Kernel seine Arbeit beendet, wird dieses Kennzeichen in den Ergebnispuffer geschrieben.
Der oben beschriebene Algorithmus ist ein sequentielles Vorwärtspassverfahren, bei dem die Daten ohne trainierbare Parameter verarbeitet werden. Diese Methode stützt sich auf streng deterministische Berechnungen, die darauf abzielen, die Rechenkosten zu minimieren und unnötige Komplexität zu vermeiden – dies ist besonders wichtig bei der Verarbeitung großer Informationsmengen. Es ist auch erwähnenswert, dass in diesem Datenfluss keine Fehlergradientenfortpflanzung angewandt wird, da unser Ziel darin besteht, stabile Signale zu identifizieren, die von Indikatorwerten abgeleitet sind, und nicht, sie an eine Zielausgabe „anzupassen“. Dies macht den Algorithmus besonders attraktiv für Systeme, die sowohl eine hohe Geschwindigkeit als auch eine hohe Genauigkeit der Verarbeitung erfordern.
Sobald der Algorithmus auf der Seite von OpenCL implementiert ist, müssen wir die Verwaltung und den Aufruf des Kernels durch das Hauptprogramm organisieren. Um diese Funktionalität zu implementieren, erstellen wir ein neues Objekt CNeuronMoreLessEqual (siehe unten).
class CNeuronMoreLessEqual : public CNeuronBaseOCL { protected: virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override {return true; } public: CNeuronMoreLessEqual(void) {}; ~CNeuronMoreLessEqual(void) {}; };
Die Struktur dieses neuen Objekts ist sehr einfach. Sie enthält nicht einmal eine Initialisierungsmethode. Die übergeordnete Klasse übernimmt fast alle Funktionen. Wir überschreiben nur die Methoden für die Vorwärts- und Rückwärtsdurchläufe.
Im Vorwärtsdurchlauf werden Zeiger auf die Datenpuffer an die Parameter des zuvor beschriebenen Kernels übergeben, der dann zur Ausführung in eine Warteschlange gestellt wird.
bool CNeuronMoreLessEqual::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!OpenCL || !NeuronOCL) return false; uint global_work_offset[1] = { 0 }; uint global_work_size[1] = { Neurons() }; ResetLastError(); const int kernel = def_k_MoreLessEqual; if(!OpenCL.SetArgumentBuffer(kernel, def_k_mle_inputs, NeuronOCL.getOutputIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); return false; } if(!OpenCL.SetArgumentBuffer(kernel, def_k_mle_outputs, getOutputIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); return false; } //--- if(!OpenCL.Execute(kernel, 1, global_work_offset, global_work_size)) { printf("Error of execution kernel %s: %d; line %d", OpenCL.GetKernelName(kernel), GetLastError(), __LINE__); return false; } //--- return true; }
Auf den ersten Blick mag die Funktionsweise der Methoden der Rückwärtsdurchläufe unklar erscheinen, wenn man bedenkt, dass es keine trainierbaren Parameter und keine Gradientenfortpflanzung gibt. Es ist jedoch wichtig zu beachten, dass diese Methoden innerhalb einer neuronalen Netzarchitektur für alle Schichten obligatorisch sind. Andernfalls würde die entsprechende Methode der übergeordneten Klasse aufgerufen, was sich in unserer spezifischen Architektur möglicherweise nicht korrekt verhält. Um solche Probleme zu vermeiden, wird die Methode zum Überschreiben der Parameter mit einem kurzen Code überschrieben, der einfach true zurückgibt.
Das Weglassen der Gradientenfortpflanzung ist logischerweise gleichbedeutend mit der Weitergabe von Nullwerten. Bei der Methode der Gradientenverteilung setzen wir daher den entsprechenden Puffer im Quelldatenobjekt einfach auf Null zurück, wodurch das Modell korrekt funktioniert und das Risiko von Laufzeitfehlern minimiert wird.
bool CNeuronMoreLessEqual::calcInputGradients(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL || !NeuronOCL.getGradient()) return false; return NeuronOCL.getGradient().Fill(0); }
Damit ist unsere Arbeit an dem Modul der Auxiliary Tools abgeschlossen. Der vollständige Code für die Klasse CNeuronMoreLessEqual und alle ihre Methoden sind im Anhang enthalten.
In diesem Stadium haben wir fast alle Schlüsselmodule des FinAgent-Rahmens abgedeckt. Die verbleibende Komponente, die zu diskutieren ist, ist das Entscheidungsfindungsmodul, das als Kernelement der Gesamtarchitektur dient. Dieses Modul gewährleistet die Synthese von Informationen aus mehreren Datenströmen – oft mehr als zwei. Wir haben uns entschieden, das Entscheidungsfindungsmodul direkt in das zusammengesetzte Rahmenobjekt zu integrieren, anstatt es als separate Einheit zu implementieren. Diese Designentscheidung hat die Interoperabilität aller Systemkomponenten verbessert.
Aufbau des FinAgent-Rahmens
Und nun ist es an der Zeit, alle zuvor geschaffenen Module in einer einheitlichen, umfassenden Struktur – dem FinAgent-Framework – zusammenzuführen, um ihre Integration und synergetische Interaktion zu gewährleisten. Module verschiedener Funktionstypen werden kombiniert, um ein gemeinsames Ziel zu erreichen: die Schaffung eines effizienten und flexiblen Systems zur Analyse komplexer Marktdaten und zur Entwicklung von Strategien, die die Dynamik und die spezifischen Merkmale der Finanzmärkte berücksichtigen. Diese Funktionalität wird durch ein neues Objekt CNeuronFinAgent implementiert. Seine Struktur ist unten dargestellt.
class CNeuronFinAgent : public CNeuronRelativeCrossAttention { protected: CNeuronTransposeOCL cTransposeState; CNeuronLowLevelReflection cLowLevelReflection[2]; CNeuronHighLevelReflection cHighLevelReflection; CNeuronMoreLessEqual cTools; CNeuronPSformer cMarketIntelligence; CNeuronMemory cMarketMemory; CNeuronRelativeCrossAttention cCrossLowLevel; CNeuronRelativeCrossAttention cMarketToLowLevel; CNeuronRelativeCrossAttention cMarketToTools; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput, CBufferFloat *SecondGradient, ENUM_ACTIVATION SecondActivation = None) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) override; public: CNeuronFinAgent(void) {}; ~CNeuronFinAgent(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_key, uint units_count, uint heads, uint account_descr, uint nactions, uint segments, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual int Type(void) override const { return defNeuronFinAgent; } //--- virtual bool Save(int const file_handle) override; virtual bool Load(int const file_handle) override; //--- virtual bool WeightsUpdate(CNeuronBaseOCL *source, float tau) override; virtual void SetOpenCL(COpenCLMy *obj) override; //--- virtual bool Clear(void) override; };
In dieser Struktur sehen wir eine vertraute Reihe von überschreibbaren Methoden und mehrere interne Objekte, unter denen man leicht die Module erkennen kann, die wir bereits im Rahmen von FinAgent implementiert haben. Die Konstruktion der Informationsflüsse, die ihre Interaktion definieren, wird diskutiert, wenn wir die Implementierungsalgorithmen dieser Klasse von Methoden untersuchen.
Alle internen Objekte werden statisch deklariert, sodass wir den Konstruktor und Destruktor der Klasse leer lassen können. Die Initialisierung aller deklarierten und geerbten Objekte wird in der Methode Init durchgeführt. Die Parameter dieser Methode umfassen mehrere Konstanten, die die Architektur des erstellten Objekts definieren.
bool CNeuronFinAgent::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_key, uint units_count, uint heads, uint account_descr, uint nactions, uint segments, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronRelativeCrossAttention::Init(numOutputs, myIndex, open_cl, 3, window_key, nactions / 3, heads, window, units_count, optimization_type, batch)) return false;
Ein Blick in die Zukunft zeigt, dass unser Entscheidungsblock aus mehreren aufeinanderfolgenden, aufmerksamkeitsübergreifenden Ebenen bestehen wird. Letzteres wird durch das übergeordnete Objekt implementiert, das nicht zufällig auf der Klasse CNeuronRelativeCrossAttention basiert.
Am Ausgang unserer Rahmenimplementierung von FinAgent erwarten wir eine Tensordarstellung der Aktionen des Agenten in Form einer Matrix, wobei jede Zeile ein Vektor ist, der eine einzelne Aktion beschreibt. Die Kauf- und Verkaufsoperationen werden durch verschiedene Zeilen dieser Matrix dargestellt. Jede Operation wird durch drei Parameter beschrieben: das Handelsvolumen und zwei Kursniveaus – Stop-Loss und Take-Profit. Folglich wird unsere Aktionsmatrix drei Spalten enthalten.
Daher geben wir beim Aufruf der Initialisierungsmethode der übergeordneten Klasse das Datenanalysefenster in der Hauptpipeline mit 3 an und die Anzahl der Elemente in der analysierten Sequenz ist dreimal kleiner als die Vektorgröße, die in den Parametern, die die Aktionen des Agenten beschreiben, angegeben ist. Diese Konfiguration ermöglicht es dem Modell, die Wirksamkeit jeder einzelnen Operation im Rahmen eines sekundären Informationsstroms zu bewerten, über den das System verarbeitete Informationen über die Umgebung übermittelt. Daher übertragen wir die entsprechenden Parameter.
Nach erfolgreicher Ausführung der Initialisierungsprozeduren der übergeordneten Klasse fahren wir mit der Vorbereitung der neu deklarierten internen Objekte fort. Wir beginnen mit der Initialisierung der Komponenten des Marktanalysemoduls. In unserer Implementierung besteht dieses Modul aus zwei Objekten: einem segmentierten Aufmerksamkeits-Transformer zur Erkennung stabiler Muster in multivariaten Zeitreihendaten und einem Speicherblock.
int index = 0; if(!cMarketIntelligence.Init(0, index, OpenCL, window, units_count, segments, 0.2f, optimization, iBatch)) return false; index++; if(!cMarketMemory.Init(0, index, OpenCL, window, window_key, units_count, heads, optimization, iBatch)) return false;
Um eine umfassende Analyse der Umgebung zu erreichen, setzen wir zwei Low-Level-Reflexionsmodule ein, die parallel auf Tensoren der Eingabedaten arbeiten, die in verschiedenen Projektionen dargestellt werden. Um die zweite Projektion der analysierten Daten zu erhalten, verwenden wir ein Transpositionsobjekt.
index++; if(!cTransposeState.Init(0, index, OpenCL, units_count, window, optimization, iBatch)) return false;
Als Nächstes werden zwei Low-Level-Reflexionsobjekte initialisiert. Ihre Analyse von Daten aus verschiedenen Projektionen wird durch die Vertauschung der Fenster- und Sequenzlängendimensionen des analysierten Tensors angezeigt.
index++; if(!cLowLevelReflection[0].Init(0, index, OpenCL, window, window_key, units_count, heads, optimization, iBatch)) return false; index++; if(!cLowLevelReflection[1].Init(0, index, OpenCL, units_count, window_key, window, heads, optimization, iBatch)) return false;
Im ersten Fall analysieren wir eine multivariate Zeitreihe, bei der jeder Zeitschritt durch einen Datenvektor dargestellt wird, und vergleichen diese Vektoren, um Abhängigkeiten zwischen ihnen aufzudecken. Im zweiten Fall analysieren wir einzelne univariate Sequenzen, um Abhängigkeiten und Regelmäßigkeiten in ihrer Dynamik zu erkennen.
Anschließend wird das Reflexionsmodul auf hoher Ebene initialisiert, das die jüngsten Handlungen des Agenten im Kontext von Marktveränderungen und Finanzergebnissen untersucht.
index++; if(!cHighLevelReflection.Init(0, index, OpenCL, window, window_key, units_count, heads, account_descr, nactions, optimization, iBatch)) return false;
In diesem Stadium bereiten wir auch das Objekt des Moduls der Auxiliary Tools vor.
index++; if(!cTools.Init(0, index, OpenCL, window * units_count, optimization, iBatch)) return false; cTools.SetActivationFunction(None);
Die Ergebnisse aller initialisierten Module werden im Entscheidungsfindungsmodul zusammengefasst, das, wie bereits erwähnt, aus mehreren aufeinander folgenden Blöcken besteht, die sich gegenseitig beeinflussen. In der ersten Stufe werden die Informationen aus den beiden Low-Level-Reflexionsmodulen integriert.
index++; if(!cCrossLowLevel.Init(0, index, OpenCL, window, window_key, units_count, heads, units_count, window, optimization, iBatch)) return false;
Als Nächstes reichern wir die Ausgabe des Marktanalysemoduls mit Informationen an, die aus den Modulen der unteren Ebene der Reflexion stammen.
index++; if(!cMarketToLowLevel.Init(0, index, OpenCL, window, window_key, units_count, heads, window, units_count, optimization, iBatch)) return false;
Dann fügen wir eine Schicht von Vorwissen hinzu.
index++; if(!cMarketToTools.Init(0, index, OpenCL, window, window_key, units_count, heads, window, units_count, optimization, iBatch)) return false; //--- return true; }
Die letzte Schicht des Entscheidungsfindungsmoduls wurde bereits zuvor initialisiert. Es wird durch das übergeordnete Objekt repräsentiert.
Nach erfolgreicher Initialisierung aller verschachtelten Objekte geben wir das logische Ergebnis der Operationen an das aufrufende Programm zurück und beenden die Methode.
Der nächste Schritt unserer Arbeit ist die Konstruktion des Algorithmus für den Vorwärtsdurchlauf unserer FinAgent-Rahmenimplementierung in der Methode feedForward. In den Methodenparametern erhalten wir Zeiger auf zwei Eingabedatenobjekte. Die Methodenparameter enthalten Zeiger auf zwei Eingabedatenobjekte: Das erste enthält Informationen über den aktuellen Umweltzustand, während das zweite den Kontostand und die aktuellen Finanzergebnisse darstellt.
bool CNeuronFinAgent::feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) { if(!cMarketIntelligence.FeedForward(NeuronOCL)) return false; if(!cMarketMemory.FeedForward(cMarketIntelligence.AsObject())) return false;
Die Informationen über das analysierte Marktumfeld werden zunächst im Marktanalysemodul verarbeitet, das mithilfe des Segmented-Attention-Transformators Muster identifiziert und deren stabile Kombinationen im Speichermodul erkennt.
Die in zwei Projektionen entdeckten Muster werden dann für eine umfassende Analyse der Marktdynamik an die Low-Level-Reflexionsmodule weitergeleitet.
if(!cTransposeState.FeedForward(cMarketIntelligence.AsObject())) return false; if(!cLowLevelReflection[0].FeedForward(cMarketIntelligence.AsObject())) return false; if(!cLowLevelReflection[1].FeedForward(cTransposeState.AsObject())) return false;
Es ist zu beachten, dass die Low-Level-Reflexionsmodule ausschließlich mit den im aktuellen Marktumfeld erkannten Mustern arbeiten, ohne Daten aus dem Speicherblock des Marktanalysemoduls zu verwenden. Dieser Ansatz konzentriert sich auf die unmittelbare Marktreaktion auf die entdeckten Muster und ermöglicht eine genauere Bewertung der aktuellen Veränderungen und Trends, ohne sich auf historische Daten zu stützen.
Die gleiche Logik gilt auch für das Modul für die Reflexion auf hoher Ebene.
if(!cHighLevelReflection.FeedForward(cMarketIntelligence.AsObject(), SecondInput)) return false;
Zur Erinnerung: Die Eingabe in das Reflexionsmodul auf hoher Ebene umfasst sowohl die aktuellen Marktumfelddaten (als Ausgabe des Marktanalysemoduls) als auch den Finanzergebnisvektor. Der Tensor der vorherigen Aktionen des Agenten wird rekursiv aus dem Ergebnispuffer des High-Level-Reflexionsmoduls verwendet.
Das Modul der Auxiliary Tools hingegen arbeitet direkt mit den rohen Eingabedaten, da es auf der Grundlage des in den analysierten Indikatormesswerten enthaltenen Vorwissens nach Signalen sucht.
if(!cTools.FeedForward(NeuronOCL)) return false;
Als Nächstes gehen wir dazu über, die Prozesse des Entscheidungsfindungsmoduls zu organisieren. Zunächst reichern wir die Ergebnisse der Low-Level-Reflexionsanalyse an, indem wir die in der Dynamik der univariaten Sequenzen ermittelten Abhängigkeiten integrieren. Dies erhöht die analytische Genauigkeit und vertieft das Verständnis des Modells für Systeminteraktionen, was eine umfassendere Bewertung des Marktumfelds ermöglicht.
if(!cCrossLowLevel.FeedForward(cLowLevelReflection[0].AsObject(), cLowLevelReflection[1].getOutput())) return false;
In der folgenden Phase integrieren wir die aus der Low-Level-Reflexion gewonnenen Informationen in die Darstellung der stabilen Muster, die der Memory Block im Marktanalysemodul erzeugt. Dieser Schritt verfeinert und verstärkt die entdeckten Beziehungen und führt zu einer präziseren und umfassenderen Wahrnehmung der aktuellen Dynamik und Interaktionen des Marktes.
if(!cMarketToLowLevel.FeedForward(cMarketMemory.AsObject(), cCrossLowLevel.getOutput())) return false;
Es ist wichtig zu betonen, dass die Low-Level-Reflexionsmodule den aktuellen Marktzustand analysieren und die Reaktion des Marktes auf einzelne Muster identifizieren. Einige dieser Muster können jedoch nur selten auftreten, sodass die entsprechenden Marktreaktionen statistisch unbedeutend sind. In solchen Fällen werden die Informationen im Speicher des Low-Level-Reflexionsmoduls gespeichert, da ähnliche Muster in Zukunft auftreten können. Dadurch kann das Modell zusätzliche Daten über die Reaktionen des Marktes sammeln.
Unbestätigte Informationen können jedoch nicht zur Entscheidungsfindung herangezogen werden. Daher stützen wir uns im Entscheidungsfindungsmodul nur auf stabile Muster und fordern entsprechende Reaktionsdaten vom Low-Level-Reflexionsmodul an, um eine genauere und fundiertere Bewertung vornehmen zu können.
Anschließend erweitern wir die Ergebnisse der Marktanalyse durch die Einbeziehung von Vorwissen.
if(!cMarketToTools.FeedForward(cMarketToLowLevel.AsObject(), cTools.getOutput())) return false;
Beachten Sie, dass wir keine trainierbaren Parameter für die Interpretation der vom Modul der Auxiliary Tools generierten Flags eingeführt haben, obwohl dies bereits diskutiert wurde. Stattdessen wird diese Funktionalität an die Schlüssel- und Wertbildungsparameter innerhalb des Moduls der Kreuzaufmerksamkeit delegiert. Die Interpretation und Verarbeitung dieser Flags ist somit direkt in den Mechanismus der Kreuzaufmerksamkeit integriert. Dies macht explizite zusätzliche Parameter überflüssig.
Am Ende der Methode feedForward analysieren wir die Ergebnisse des High-Level-Reflexionsmoduls im Zusammenhang mit den identifizierten stabilen Mustern und den Marktreaktionen auf diese. Dieser Vorgang wird mit den Werkzeugen der übergeordneten Klasse durchgeführt.
return CNeuronRelativeCrossAttention::feedForward(cHighLevelReflection.AsObject(), cMarketToTools.getOutput());
}
Das logische Ergebnis der Operationen wird dann an das aufrufende Programm zurückgegeben, womit das Feed-Forward-Verfahren abgeschlossen ist.
Nach dem Vorwärtsdurchlauf werden die Prozesse des Rückwärtsdurchlaufs organisiert. In diesem Abschnitt wird der Algorithmus für die Methode zur Verteilung der Fehlergradienten (calcInputGradients) im Detail untersucht, während die Methode zur Optimierung der trainierbaren Parameter (updateInputWeights) einer unabhängigen Untersuchung vorbehalten bleibt.
bool CNeuronFinAgent::calcInputGradients(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput, CBufferFloat *SecondGradient, ENUM_ACTIVATION SecondActivation = -1) { if(!NeuronOCL || !SecondGradient) return false;
Die Methodenparameter enthalten wiederum Zeiger auf dieselben Eingabedatenobjekte – diesmal müssen wir die Fehlergradienten entsprechend dem Einfluss der Eingabedaten auf die endgültige Ausgabe des Modells übergeben. Der Methodenkörper beginnt mit einer Validierung dieser Zeiger, da weitere Operationen sinnlos sind, wenn sie ungültig sind.
Wie Sie wissen, spiegelt die Fehlergradientenverteilung den Informationsfluss des Vorwärtsdurchläufe vollständig wider, nur in umgekehrter Richtung. Der Vorwärtsdurchlauf wird mit einem Aufruf der entsprechenden Methode der übergeordneten Klasse abgeschlossen. Daher beginnt der Algorithmus der Rückwärtsdurchläufe mit dem Aufruf der Gradientenverteilungsmethode der Elternklasse. Es verteilt den Fehler des Modells zwischen dem Reflexionsmodul auf hoher Ebene und dem vorhergehenden Block des Entscheidungsfindungsmoduls, der die Aufmerksamkeit auf sich zieht.
if(!CNeuronRelativeCrossAttention::calcInputGradients(cHighLevelReflection.AsObject(), cMarketToTools.getOutput(), cMarketToTools.getGradient(), (ENUM_ACTIVATION)cMarketToTools.Activation())) return false;
Anschließend werden die Fehlergradienten nacheinander durch alle aufmerksamkeitsübergreifenden Blöcke des Entscheidungsfindungsmoduls propagiert, wobei die Fehler entsprechend ihres Einflusses auf das Ergebnis des Modells auf alle Informationsflüsse des Systems verteilt werden.
if(!cMarketToLowLevel.calcHiddenGradients(cMarketToTools.AsObject(), cTools.getOutput(), cTools.getGradient(), (ENUM_ACTIVATION)cTools.Activation())) return false; //--- if(!cMarketMemory.calcHiddenGradients(cMarketToLowLevel.AsObject(), cCrossLowLevel.getOutput(), cCrossLowLevel.getGradient(), (ENUM_ACTIVATION)cCrossLowLevel.Activation())) return false;
Als Nächstes propagieren wir die Fehlergradienten durch die Low-Level-Reflexionsmodule.
if(!cLowLevelReflection[0].calcHiddenGradients(cCrossLowLevel.AsObject(), cLowLevelReflection[1].getOutput(), cLowLevelReflection[1].getGradient(), (ENUM_ACTIVATION)cLowLevelReflection[1].Activation())) return false; if(!cTransposeState.calcHiddenGradients(cLowLevelReflection[1].AsObject())) return false;
Zu diesem Zeitpunkt sind die Fehlergradienten auf alle Rahmenmodule verteilt worden. Wir müssen nun Daten aus allen Informationsströmen auf der Ebene der ursprünglichen Eingangsdaten sammeln. Es sei daran erinnert, dass alle Reflexionsmodule und der Speicherblock des Marktanalysemoduls mit den vorverarbeiteten Daten arbeiten, die durch den segmentierten Aufmerksamkeitstransformator erzeugt wurden.
Der erste Schritt ist die Übertragung des Fehlergradienten aus dem Speicherblock.
if(!((CNeuronBaseOCL*)cMarketIntelligence.AsObject()).calcHiddenGradients(cMarketMemory.AsObject())) return false;
Als Nächstes ersetzen wir den Zeiger auf den Fehlergradientenpuffer unseres Vorverarbeitungsobjekts für Eingabedaten, sodass wir die kumulierten Gradientenwerte speichern können.
CBufferFloat *temp = cMarketIntelligence.getGradient(); if(!cMarketIntelligence.SetGradient(cMarketIntelligence.getPrevOutput(), false) || !((CNeuronBaseOCL*)cMarketIntelligence.AsObject()).calcHiddenGradients(cHighLevelReflection.AsObject(), SecondInput, SecondGradient, SecondActivation) || !SumAndNormilize(temp, cMarketIntelligence.getGradient(), temp, 1, false, 0, 0, 0, 1)) return false;
Anschließend rufen wir die Gradientenverteilungsmethode des High-Level-Reflexionsmoduls auf. Danach müssen wir die Ergebnisse aus beiden Datenströmen zusammenrechnen.
Es ist zu beachten, dass das High-Level-Reflexionsmodul mit zwei Datenströmen arbeitet. So verarbeitet dieses Modul während der Gradientenfortpflanzung gleichzeitig Fehler aus dem Hauptstrom und dem Strom der Finanzergebnisse. Auf diese Weise kann das Modell Fehler in beiden entscheidenden Aspekten der Analyse berücksichtigen und eine genauere Abstimmung des Systems gewährleisten.
Die Low-Level-Reflexionsmodule behandeln die Gradientenausbreitung auf ähnliche Weise. Im Gegensatz zum High-Level-Reflexionsmodul arbeiten diese jedoch mit einer einzigen Quelle von Eingabedaten, was den Prozess der Fehlergradientenverteilung vereinfacht.
if(!((CNeuronBaseOCL*)cMarketIntelligence.AsObject()).calcHiddenGradients(cLowLevelReflection[0].AsObject()) || !SumAndNormilize(temp, cMarketIntelligence.getGradient(), temp, 1, false, 0, 0, 0, 1)) return false; if(!((CNeuronBaseOCL*)cMarketIntelligence.AsObject()).calcHiddenGradients(cTransposeState.AsObject()) || !SumAndNormilize(temp, cMarketIntelligence.getGradient(), temp, 1, false, 0, 0, 0, 1) || !cMarketIntelligence.SetGradient(temp, false)) return false;
Vergessen Sie nicht, dass nach jeder Iteration die neu gewonnenen Gradientenwerte zu den zuvor akkumulierten Gradienten addiert werden müssen. Dadurch wird sichergestellt, dass alle Modellfehler korrekt berücksichtigt werden. Nach der Verarbeitung aller Informationsflüsse ist es wichtig, die ursprünglichen Pufferzeiger wieder in ihren Ausgangszustand zu versetzen.
Schließlich geben wir den Fehlergradienten an die Eingabeebene des Hauptinformationsstroms zurück und schließen die Methode ab, indem wir das logische Ergebnis der Operationen an das aufrufende Programm zurückgeben.
if(!NeuronOCL.calcHiddenGradients(cMarketIntelligence.AsObject())) return false; //--- return true; }
Beachten Sie, dass das Modul der Auxiliary Tools nicht am Algorithmus der Fehlergradientenverteilung beteiligt ist. Wie bereits erwähnt, planen wir nicht, Gradienten durch diesen Informationsfluss zu verbreiten. Außerdem wäre das Löschen des Gradientenpuffers für das Datenquellenobjekt in diesem Zusammenhang schädlich, da das gleiche Objekt auch Gradienten über den Hauptinformationsstrom empfängt.
Damit ist unsere Diskussion über die Algorithmen zur Implementierung des FinAgent-Rahmens in MQL5 abgeschlossen. Der vollständige Quellcode für alle vorgestellten Objekte und Methoden ist in den Anhängen für Ihre Referenz und weitere Experimente verfügbar. Dort finden Sie auch den vollständigen Programmcode und die Architektur des trainierbaren Modells, das bei der Erstellung dieses Artikels verwendet wurde. Alle Komponenten wurden fast unverändert aus dem vorherigen Artikel über den Aufbau eines Agenten mit Schichtenspeicher übernommen. Die einzigen Änderungen betreffen die Modellarchitektur, bei der wir eine einzelne neuronale Schicht durch den oben beschriebenen integrierten FinAgent-Rahmen ersetzt haben.
//--- layer 2 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronFinAgent; //--- Windows { int temp[] = {BarDescr, AccountDescr, 2 * NActions, Segments}; //Window, Account description, N Actions, Segments if(ArrayCopy(descr.windows, temp) < int(temp.Size())) return false; } descr.count = HistoryBars; descr.window_out = 32; descr.step = 4; // Heads descr.batch = 1e4; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { delete descr; return false; }
Die Architektur aller übrigen Schichten ist unverändert erhalten geblieben. Und nun gehen wir zur letzten Phase unserer Arbeit über – der Evaluierung der Effektivität der implementierten Ansätze an realen historischen Daten.
Tests
In den letzten beiden Artikeln haben wir den FinAgent-Rahmen im Detail untersucht. Während dieses Prozesses haben wir unsere eigene Interpretation der von den Autoren vorgeschlagenen Ansätze umgesetzt. Wir haben die Algorithmen des Frameworks an unsere spezifischen Anforderungen angepasst. Wir haben nun eine weitere wichtige Phase erreicht: die Bewertung der Wirksamkeit der implementierten Lösungen anhand realer historischer Daten.
Bitte beachten Sie, dass wir während der Entwicklung erhebliche Änderungen an den Kernalgorithmen von FinAgent vorgenommen haben. Diese Änderungen betreffen wichtige Aspekte des Modellbetriebs. Daher bewerten wir in dieser Evaluierung unsere angepasste Version und nicht den ursprünglichen Rahmen.
Das Modell wurde anhand historischer Daten für das Währungspaar EURUSD für das Jahr 2023 mit dem Zeitrahmen H1 trainiert. Alle vom Modell verwendeten Indikatoreinstellungen wurden auf ihren Standardwerten belassen, sodass wir uns auf die Bewertung des Algorithmus selbst und seine Fähigkeit, mit Rohdaten ohne zusätzliche Einstellungen zu arbeiten, konzentrieren konnten.
Für die erste Trainingsphase haben wir einen Datensatz verwendet, der in früheren Studien erstellt wurde. Wir haben einen Trainingsalgorithmus angewandt, der „fast ideale“ Zielhandlungen für den Agenten generiert, sodass wir das Modell trainieren können, ohne den Trainingsdatensatz ständig zu aktualisieren. Dieser Ansatz hat zwar gut funktioniert, aber wir sind der Meinung, dass eine regelmäßige Aktualisierung des Trainingssatzes die Genauigkeit verbessern und die Abdeckung der verschiedenen Kontostände erweitern würde.
Nach mehreren Trainingszyklen zeigte das Modell sowohl bei Trainings- als auch bei Testdaten eine stabile Rentabilität. Die abschließenden Tests wurden mit historischen Daten für Januar 2024 durchgeführt, wobei alle Modellparameter und Indikatoreinstellungen beibehalten wurden. Dieser Ansatz ermöglicht eine objektive Bewertung der Modellleistung unter Bedingungen, die dem realen Marktumfeld so nahe wie möglich kommen. Die Ergebnisse werden im Folgenden vorgestellt.


Während des Testzeitraums führte das Modell 95 Handelsgeschäfte aus und übertraf damit deutlich die Leistung früherer Modelle in einem ähnlichen Zeitraum. Über 42 % der Handelsgeschäfte wurden mit Gewinn abgeschlossen. Da der Durchschnitt der Handelsgeschäfte mit Gewinn ist 1,5 Mal größer war als der Durchschnitt jener mit Verlust, war das Modell insgesamt profitabel. Der Gewinnfaktor wurde mit 1,09 angegeben.
Interessanterweise wurde der Großteil der Gewinne in der ersten Monatshälfte realisiert, als die Preise innerhalb einer relativ engen Spanne schwankten. Als sich ein Abwärtstrend entwickelte, bewegte sich die Gleichgewichtslinie seitwärts, und es wurde ein gewisser Drawdown beobachtet.

Meiner Meinung nach kann das beobachtete Verhalten wahrscheinlich auf die Algorithmen im Marktanalysemodul und im Modul der Auxiliary Tools zurückgeführt werden. Dieser Bereich bleibt für weitere Untersuchungen offen.
Schlussfolgerung
Wir haben das FinAgent-Framework erforscht, eine fortschrittliche Lösung für umfassende Marktanalysen und die Auswertung historischer Daten. Durch die Integration von textlichen und visuellen Informationen erweitert der Rahmen die Möglichkeiten, fundierte Handelsentscheidungen zu treffen, erheblich. Mit seinen fünf architektonischen Schlüsselkomponenten demonstriert FinAgent sowohl Genauigkeit als auch hohe Anpassungsfähigkeit, was für den Handel auf den Finanzmärkten, die durch häufig wechselnde Bedingungen gekennzeichnet sind, entscheidend ist.
Der Rahmen ist nicht auf eine einzige Art von Analyse beschränkt. Es bietet eine breite Palette von Werkzeugen, die sowohl mit textlichen als auch mit grafischen Daten arbeiten können. Dank dieser Vielseitigkeit kann das Modell mehrere Marktfaktoren berücksichtigen, was zu einem besseren Verständnis der Marktdynamik führt. Diese Eigenschaften machen FinAgent zu einem vielversprechenden Werkzeug für die Entwicklung von Handelsstrategien, die sich an veränderte Marktbedingungen anpassen und selbst geringe Marktschwankungen berücksichtigen können.
Im praktischen Teil unserer Arbeit haben wir unsere Interpretation der Rahmenansätze in MQL5 umgesetzt. Wir haben das Modell durch die Integration dieser Ansätze trainiert und es an realen historischen Daten getestet. Die Ergebnisse zeigten, dass das Modell in der Lage ist, Gewinne zu erzielen. Es wurde jedoch festgestellt, dass die Rentabilität von den Marktbedingungen abhängt. Außerdem sind weitere Experimente erforderlich, um die Anpassungsfähigkeit des Modells an sich dynamisch verändernde Marktumgebungen zu verbessern.
Referenzen
- A Multimodal Foundation Agent for Financial Trading: Tool-Augmented, Diversified, and Generalist
- Andere Artikel dieser Serie
Programme, die im diesem Artikel verwendet werden
| # | Name | Typ | Beschreibung |
|---|---|---|---|
| 1 | Research.mq5 | Expert Advisor | Expert Advisor für die Probenahme |
| 2 | ResearchRealORL.mq5 | Expert Advisor | Expert Advisor für die Probenahme mit der Real-ORL-Methode |
| 3 | Study.mq5 | Expert Advisor | EA für das Modelltraining |
| 4 | Test.mq5 | Expert Advisor | Expert Advisor für Modelltests |
| 5 | Trajectory.mqh | Klassenbibliothek | Struktur der Beschreibung des Systemzustands und der Modellarchitektur |
| 6 | NeuroNet.mqh | Klassenbibliothek | Eine Bibliothek von Klassen zur Erstellung eines neuronalen Netzes |
| 7 | NeuroNet.cl | Code-Bibliothek | OpenCL-Programmcode |
Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/16867
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.
Marktsimulation (Teil 04): Erstellen der Klasse C_Orders (I)
Neuronale Netze im Handel: Ein multimodaler, werkzeuggestützter Agent für Finanzmärkte (FinAgent)
Von der Grundstufe bis zur Mittelstufe: Template und Typename (V)
Erstellung eines Indikators für die Volatilitätsprognose mit Python
- 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.