English Русский
preview
Von der CPU zur GPU in MQL5: Ein praktisches OpenCL-Framework zur Beschleunigung von Analysen, Optimierungen und Mustererkennung

Von der CPU zur GPU in MQL5: Ein praktisches OpenCL-Framework zur Beschleunigung von Analysen, Optimierungen und Mustererkennung

MetaTrader 5Integration |
32 0
MetaQuotes
MetaQuotes

Einführung

Der Übergang von der CPU zur GPU in MQL5 scheint oft ein naheliegender Schritt zu sein: Wenn der Grafikprozessor schneller rechnen kann, dann sollte die Handelsanalyse automatisch schneller werden. In der Praxis ist die Situation deutlich komplexer. Die GPU kann in der Tat eine erhebliche Beschleunigung ermöglichen, aber nur, wenn die Aufgabe gut zu einem parallelen Berechnungsmodell passt. Andernfalls erhalten Sie möglicherweise keine Beschleunigung, sondern nur eine komplexere Architektur mit den gleichen oder sogar höheren Kosten.

Dies ist besonders wichtig für den algorithmischen Handel. Die Analyse von Marktdaten, die Iteration von Parametern, groß angelegte Hypothesentests und die Suche nach sich wiederholenden Mustern erfordern oft große Mengen an Rechenleistung. Hier entfaltet die GPU ihr Potenzial. Sie eignet sich besonders gut, wenn derselbe Vorgang an mehreren Elementen durchgeführt werden muss und das Ergebnis nach Abschluss der Parallelverarbeitung gesammelt werden kann. In solchen Szenarien ist eine Grafikkarte keine dekorative Ergänzung mehr, sondern wird zu einer vollwertigen Rechenressource.

Der Einsatz der GPU hat jedoch seinen Preis. Bevor wir mit den Berechnungen beginnen, sollten wir die Daten vorbereiten, sie an das Gerät übergeben, auf die Ausführung durch den Kernel warten und das Ergebnis zurückgeben. Für kompakte Aufgaben kann diese Logik zu schwer sein. In den Bereichen, in denen die CPU schnell und ohne unnötigen Overhead arbeitet, bringt die Verlagerung von Berechnungen auf die GPU keine Vorteile mit sich. Manchmal ist sie sogar hinderlich, vor allem, wenn sich die Aufgabe häufig ändert, eine flexible Logik erfordert oder sich auf kleine Datenmengen bezieht.

In der Umgebung von MQL5 dient OpenCL als Verbindung zwischen der Anwendungslogik und der GPU. Dadurch können wir den arbeitsintensiven Teil der Berechnungen aus dem Hauptprogramm auslagern und eine Batch-Verarbeitung der Daten auf der GPU organisieren. OpenCL an sich ist jedoch keine magische Beschleunigungstaste. Sie ist nur dann sinnvoll, wenn die Aufgabenarchitektur die Besonderheiten des parallelen Rechnens von vornherein berücksichtigt und den Datenaustausch zwischen CPU und GPU minimiert.

Hier wird die GPU als separate Ebene der Berechnungsarchitektur betrachtet, die für schwere und sich wiederholende Operationen vorgesehen ist. Dieser Ansatz ist nützlich bei Forschungs-, Optimierungs- und Musterfindungsaufgaben, bei denen der Rechenaufwand schneller wächst als die Bereitschaft des Forschers zu warten. Die praktische Quintessenz daraus ist einfach: Zuerst müssen wir verstehen, was genau es wert ist, auf die GPU übertragen zu werden, und erst dann können wir erwarten, dass die Beschleunigung einen echten Effekt hat.

CPU vs. GPU


Vorbereiten der Umgebung

Die Arbeit mit OpenCL beginnt nicht mit Berechnungen, sondern mit der Vorbereitung. Zunächst muss das Programm ein verfügbares Gerät finden, einen Arbeitskontext erstellen, den Kernel vorbereiten und Speicher für Daten zuweisen. All dies scheint eine technische Formalität zu sein, aber gerade in dieser Phase geht oft ein erheblicher Teil der Leistung verloren.

Der Hauptfehler besteht darin, die GPU wie einen gewöhnlichen Funktionsaufruf zu behandeln: aufrufen, das Ergebnis abrufen und weitergehen. In der Tat steht hinter einem solchen Aufruf eine ganze Kette von Maßnahmen. Wir müssen einen Kontext erstellen oder verbinden, ein Programm vorbereiten, den Kernel kompilieren, Speicher zuweisen, Daten übertragen und erst dann die Berechnung starten. Wenn wir das immer wieder tun, wird die GPU zu viel Zeit für Vorbereitungen statt für Berechnungen aufwenden.

Daher wird bei einer guten Umsetzung fast alles, was getan werden kann, einmal getan. Der Kontext wird im Voraus erstellt und dann wiederverwendet. Der Kernel wird einmal kompiliert, wenn sich der Code nicht ändert. Es ist auch besser, Speicherpuffer nicht neu zu erstellen, wenn es nicht notwendig ist, sondern sie wiederzuverwenden. Dieser Ansatz reduziert den Overhead und macht die Arbeit mit der GPU wirklich nützlich.

Das Gleiche gilt für die Datenübertragung. Die GPU ist nicht gut geeignet für viele kleine Aufgaben, die ständig hin- und hergeschickt werden. Bei diesem Ansatz wird die Zeit nicht auf Berechnungen, sondern auf den Datenaustausch verwendet. Es ist viel effizienter, Daten in größeren Stapeln zu sammeln und Berechnungen seltener, aber mit einer höheren Belastung durchzuführen. Eine einzige große Ausführung ist fast immer besser als eine Reihe kleinerer.

Ein weiteres häufiges Problem ist die unnötige Synchronisierung. Wenn das Programm nach jedem Schritt anhält und auf die Fertigstellung der GPU wartet, ist das Gerät im Leerlauf. Dadurch wird der Beschleunigungseffekt insgesamt verringert. Es ist besser, die Arbeit so zu strukturieren, dass die GPU eine Aufgabe erhält, diese ohne unnötige Zwischenstopps abarbeitet und das Ergebnis nur dann zurückgibt, wenn es wirklich benötigt wird.

Dies ist besonders wichtig für MQL5. Handelsprogramme reagieren empfindlich auf Verzögerungen, und architektonische Schlampereien machen sich schnell bemerkbar. Wenn der Kontext ständig neu erstellt wird, der Speicher chaotisch zugewiesen wird und der Kernel bei jeder Berechnung kompiliert wird, wird die GPU eher zu einer Quelle von Verzögerungen als zu einem Beschleuniger.

Daher ist der wichtigste praktische Grundsatz hier sehr einfach: Alles, was im Voraus vorbereitet werden kann, sollte im Voraus vorbereitet werden. Alles, was wiederverwendet werden kann, sollte nicht neu erstellt werden. Wenn die Umgebung ordentlich organisiert ist, hilft OpenCL wirklich, schwere Berechnungen zu beschleunigen. Wenn diese Disziplin fehlt, geht der Nutzen leicht verloren, noch bevor die Berechnungen beginnen.


Umgang mit Speicher

Wenn es um GPUs geht, denken viele Menschen in erster Linie an Rechenleistung. In der Praxis kommt es aber oft nicht so sehr auf die Geschwindigkeit der Berechnungen an, sondern darauf, wie genau die Daten geliefert werden. Selbst ein sehr schneller Grafikprozessor wird keine guten Ergebnisse liefern, wenn die Speicherzugriffe ineffizient organisiert sind.

In OpenCL ist der Speicher mehr als nur ein Ort, an dem Daten gespeichert werden. Die Leistung hängt direkt davon ab, wie sie genutzt wird. Die GPU verfügt über mehrere Speicherebenen. Einige arbeiten schneller, andere langsamer. Auf kleine interne Speicherbereiche kann schnell zugegriffen werden, aber der Zugriff auf den Hauptspeicher des Geräts ist wesentlich teurer.

Daraus ergibt sich eine wichtige Faustregel: Die Daten sollten so organisiert werden, dass die GPU so wenig wie möglich auf langsamen Speicher zugreift. Je weniger unnötiges Lesen und Schreiben, desto besser. Es ist besonders wichtig, dass nicht unnötigerweise immer wieder die gleichen Daten zwischen CPU und GPU übertragen werden. Nicht die Berechnung selbst, sondern der Datenaustausch ist häufig der größte Engpass.

Anordnen von GPU-Speicher

Einfach ausgedrückt, GPU mag Aufgaben, bei denen man ein großes Array von Daten einmal sendet, viele ähnliche Operationen durchführen lässt und dann das fertige Ergebnis zurückgibt. Die GPU mag es nicht, wenn das Programm ständig kleine Datenstücke sendet und nach jedem Schritt auf eine Antwort wartet. In diesem Modus verschwindet die Beschleunigung schnell.

Dies ist besonders für Handelsanwendungen von Bedeutung. Wenn ein Programm bei jedem Schritt erst Daten vorbereitet, dann überträgt, dann auf das Ergebnis wartet und dann alles noch einmal wiederholt, wird die Leistung instabil. Es ist viel besser, die erforderlichen Daten im Voraus zu sammeln, sie in einem einzigen Block an das Gerät zu senden, die Berechnung durchzuführen und das Ergebnis im Hauptprogramm zu verwenden.

Dabei ist es wichtig, die Rollen richtig zu verteilen. Die CPU bleibt in der Rolle der Steuerzentrale: Sie sammelt Daten, startet Berechnungen und trifft die endgültige Entscheidung. Die GPU übernimmt den Teil der Arbeit, bei dem wir den gleichen Vorgang schnell und viele Male wiederholen müssen. Diese Struktur ist fast immer zuverlässiger und effizienter als der Versuch, alles auf ein Gerät zu übertragen.

Ein weiterer typischer Fehler ist, dass zu kleine Kernel-Ausführungen gestartet werden. Auf den ersten Blick erscheint dies praktisch: Neue Daten treffen ein und werden sofort an die GPU gesendet. Aber jeder Start kostet auch Zeit. Wenn es zu viele Kernel-Starts gibt, fressen die Overhead-Kosten den gesamten Gewinn auf. Daher ist es in den meisten Fällen besser, die GPU seltener laufen zu lassen, ihr aber eine größere und einfachere Aufgabe zu geben.

Die Arbeit mit dem Gerätespeicher ist daher kein nebensächliches technisches Detail, sondern einer der Hauptfaktoren für die Leistung. Wenn die Daten richtig angeordnet sind, funktioniert die GPU wie eine Pipeline. Andernfalls ist selbst ein leistungsstarkes Gerät untätig und verschwendet Zeit mit unnötigen Anfragen und Übertragungen.

Fazit: Die GPU beschleunigt die Berechnungen nur, wenn die Daten korrekt bereitgestellt werden. Je weniger unnötige Übertragungen, Anfragen und kleine Starts, desto höher ist der tatsächliche Effekt.


Erstellen eines Programms

Im Anwendungsprogramm konkurrieren CPU und GPU nicht miteinander, sondern arbeiten zusammen. Die CPU bleibt die Steuerzentralesie erzeugt die Ausgangsdaten, ruft die gewünschte Methode auf, nimmt das Ergebnis entgegen und vergleicht die Ausführungszeit. Die GPU übernimmt nur den rechnerischen Teil. Dies ist eine klassische Struktur: Anstatt das gesamte Programm auf das Gerät zu ziehen, sollten wir ihm nur den Abschnitt geben, in dem echte Parallelität besteht.

Der einfachste Weg, die Erstellung eines Programms mit OpenCL zu verstehen, ist die Verwendung eines konkreten Beispiels. Die Standardbibliothek von MQL5 enthält eine anschauliche Implementierung der Matrixmultiplikation. Im Hauptprogramm werden zunächst zwei Matrizen erstellt und mit Zufallswerten gefüllt.

void OnStart()
  {
//--- matrix A 1000x2000
   int rows_a = 1000;
   int cols_a = 2000;
//--- matrix B 2000x1000
   int rows_b = cols_a;
   int cols_b = 1000;
//--- matrix C 1000x1000
   int rows_c = rows_a;
   int cols_c = cols_b;
//--- matrix A: size=rows_a*cols_a
   int size_a = rows_a * cols_a;
   int size_b = rows_b * cols_b;
   int size_c = rows_c * cols_c;
//--- prepare matrix A
   float matrix_a[];
   ArrayResize(matrix_a, rows_a * cols_a);
   for(int i = 0; i < rows_a; i++)
      for(int j = 0; j < cols_a; j++)
        {
         matrix_a[i * cols_a + j] = (float)(10 * MathRand() / 32767);
        }
//--- prepare matrix B
   float matrix_b[];
   ArrayResize(matrix_b, rows_b * cols_b);
   for(int i = 0; i < rows_b; i++)
      for(int j = 0; j < cols_b; j++)
        {
         matrix_b[i * cols_b + j] = (float)(10 * MathRand() / 32767);
        }

Zunächst wird eine sequenzielle Berechnung auf der CPU aufgerufen, und dann wird die gleiche Berechnung auf der GPU durchgeführt.

//--- CPU: calculate matrix product matrix_a*matrix_b
   float matrix_c_cpu[];
   ulong time_cpu = 0;
   if(!MatrixMult_CPU(matrix_a, matrix_b, matrix_c_cpu, rows_a, cols_a, cols_b, time_cpu))
     {
      PrintFormat("Error in calculation on CPU. Error code=%d", GetLastError());
      return;
     }
//--- calculate matrix product using GPU
   float matrix_c_gpu_method1[];
   float matrix_c_gpu_method2[];
   ulong time_gpu_method1 = 0;
   ulong time_gpu_method2 = 0;
   if(!MatrixMult_GPU(matrix_a, matrix_b, matrix_c_gpu_method1, matrix_c_gpu_method2,
       rows_a, cols_a, cols_b, size_a, size_b, size_c, time_gpu_method1, time_gpu_method2))
     {
      PrintFormat("Error in calculation on GPU. Error code=%d", GetLastError());
      return;
     }

Die Berechnungen selbst werden in separaten Methoden durchgeführt. Der GPU-Teil wird in zwei Implementierungen desselben Problems vorgestellt: naiv und optimiert. So können wir den Unterschied nicht in Worten, sondern im Code und in der Ausführungszeit sofort erkennen.

Schauen wir uns die CPU-Version an. Hier ist alles ganz klar: eine klassische dreifache Schleifenstruktur. Jedes Element der resultierenden Matrix wird nacheinander berechnet.

bool MatrixMult_CPU(const float &matrix_a[], const float &matrix_b[], float &matrix_c[],
                    const int rows_a, const int cols_a, const int cols_b, ulong &time_cpu)
  {
   int size = rows_a * cols_b;
   if(ArrayResize(matrix_c, size) != size)
      return(false);
//--- CPU calculation started
   time_cpu = GetMicrosecondCount();
   for(int i = 0; i < rows_a; i++)
     {
      for(int j = 0; j < cols_b; j++)
        {
         float sum = 0.0;
         for(int k = 0; k < cols_a; k++)
           {
            sum += matrix_a[cols_a * i + k] * matrix_b[cols_b * k + j];
           }
         matrix_c[cols_b * i + j] = sum;
        }
     }
//--- CPU calculation finished
   time_cpu = ulong((GetMicrosecondCount() - time_cpu) / 1000);
//---
   return(true);
  }

Dieser Code veranschaulicht die Grundidee der Aufgabe. Es gibt zwei Matrizen. Es gibt eine Summe der Produkte nach Zeilen und Spalten. Es gibt eine sequenzielle Ausführung. Auf der CPU funktioniert dies transparent und ohne unnötige Vorbereitung. Doch genau hier liegt die größte Einschränkung: Sobald die Matrixgröße wächst, werden die sequenziellen Berechnungen immer aufwendiger.

Als Nächstes kommt der GPU-Teil. Hier ist es wichtig, den wichtigsten Grundsatz hervorzuheben: OpenCL ist in diesem Beispiel in einer separaten Methode versteckt. Das Hauptprogramm bleibt dadurch übersichtlich, und die gesamte GPU-Bearbeitung ist an einer Stelle konzentriert.

bool MatrixMult_GPU(const float &matrix_a[], const float &matrix_b[], float &matrix1_c[], float &matrix2_c[],
                    const int rows_a, const int cols_a, const int cols_b, const int size_a, const int size_b,
                    const int size_c, ulong &time1_gpu, ulong &time2_gpu)
  {
   const int task_dimension = 2;
//--- prepare matrices for result
   if(ArrayResize(matrix1_c, size_c) != size_c || ArrayResize(matrix2_c, size_c) != size_c)
      return(false);
   ArrayFill(matrix1_c, 0, size_c, (float)0.0);
   ArrayFill(matrix2_c, 0, size_c, (float)0.0);

Hier sehen Sie sofort eine wichtige Sache: Die Ergebnismatrizen für zwei GPU-Optionen sind im Voraus vorbereitet. Dadurch wird verhindert, dass wir Berechnungen mit der Vorbereitung des Speichers vermischen. Auf dieser Ebene ist das Programm bereits diszipliniert strukturiert: Zuerst kommt die Zuweisung von Arrays, dann OpenCL.

Als Nächstes wird der OpenCL-Kontext erstellt und initialisiert.

//--- OpenCL
   ulong timei_gpu = GetMicrosecondCount();
   COpenCL OpenCL;
   if(!OpenCL.Initialize(cl_program, true))
     {
      PrintFormat("Error in OpenCL initialization. Error code=%d", GetLastError());
      return(false);
     }

An dieser Stelle wird die praktische Bedeutung von OpenCL deutlich. Bis zu diesem Punkt war das Programm ein herkömmlicher MQL5-Code. Nun bereitet das Programm die Ausführungsumgebung für die GPU vor. Besonders wichtig ist, dass es sich nicht um eine kostenlose Operation handelt. Wir messen die Initialisierungszeit separat. Bevor wir über die Beschleunigung sprechen, müssen wir einen ehrlichen Blick darauf werfen, wie viel es kostet, die Rechenumgebung zu initialisieren.

Als Nächstes werden zwei Kernel erstellt. Der erste ist eine einfache parallele Variante. Der zweite ist fortgeschrittener, mit lokalen Gruppen.

//--- create kernels
   OpenCL.SetKernelsCount(2);
   OpenCL.KernelCreate(0, "MatrixMult_GPU1");
   OpenCL.KernelCreate(1, "MatrixMult_GPU2");

Hier ist anzumerken, dass die Ausführungszeit weitgehend von der Qualität des im OpenCL-Programm verwendeten Algorithmus abhängt. Wir können die Berechnung einfach parallelisieren, oder wir können die Speicherverwaltung verbessern. Das Beispiel enthält beide Optionen, was es besonders deutlich macht.

Anschließend werden die Puffer vorbereitet. Die Eingabematrizen werden in das Gerät kopiert, und für das Ergebnis wird ein separater Puffer angelegt.

//--- create buffers
   OpenCL.SetBuffersCount(3);
//---
   if(!OpenCL.BufferFromArray(0, matrix_a, 0, size_a, CL_MEM_READ_ONLY))
     {
      PrintFormat("Error in BufferFromArray for matrix A. Error code=%d", GetLastError());
      return(false);
     }
   if(!OpenCL.BufferFromArray(1, matrix_b, 0, size_b, CL_MEM_READ_ONLY))
     {
      PrintFormat("Error in BufferFromArray for matrix B. Error code=%d", GetLastError());
      return(false);
     }
   if(!OpenCL.BufferCreate(2, size_c * sizeof(float), CL_MEM_WRITE_ONLY))
     {
      PrintFormat("Error in BufferCreate for matrix C. Error code=%d", GetLastError());
      return(false);
     }

Dies ist bereits eine echte funktionierende GPU-Struktur. Die Daten werden an das Gerät übertragen, dort berechnet und dann zurückgeschickt. Diese Ordnung macht die Beschleunigung erst möglich. Wenn wir sie in kleine Teile zerlegen, wird die GPU zu viel Zeit mit der Vorbereitung und dem Austausch verbringen, anstatt mit der eigentlichen Berechnung.

Danach werden die Argumente des ersten Kernels gesetzt.

//--- prepare arguments for kernel 0
   int kernel_index = 0;
   OpenCL.SetArgumentBuffer(kernel_index, 0, 0);
   OpenCL.SetArgumentBuffer(kernel_index, 1, 1);
   OpenCL.SetArgumentBuffer(kernel_index, 2, 2);
   OpenCL.SetArgument(kernel_index, 3, rows_a);
   OpenCL.SetArgument(kernel_index, 4, cols_a);
   OpenCL.SetArgument(kernel_index, 5, cols_b);
   timei_gpu = ulong((GetMicrosecondCount() - timei_gpu) / 1000);
   PrintFormat("time of initialization GPU =%d ms", timei_gpu);

Die Logik ist hier sehr einfach: Der Kernel erhält Datenpuffer und Matrixgrößen. Im OpenCL-Kernel wird entschieden, welcher Thread für welches Element zuständig ist. Dies ist ein wichtiger Punkt: Die GPU selbst weiß nicht, wie sie Arrays interpretieren soll. Diese Struktur muss ihr ausdrücklich vermittelt werden.

Dann wird die Problemgröße festgelegt und die erste Berechnungsoption gestartet.

//--- set task dimension a_rows x b_cols
   uint global_work_size[2];
//--- set dimensions
   global_work_size[0] = rows_a;
   global_work_size[1] = cols_b;
   uint global_work_offset[2] = {0, 0};
//--- GPU calculation start kernel 0
   time1_gpu = GetMicrosecondCount();
   if(!OpenCL.Execute(kernel_index, task_dimension, global_work_offset, global_work_size))
     {
      PrintFormat("Error in Execute. Error code=%d", GetLastError());
      return(false);
     }
   if(!OpenCL.BufferRead(2, matrix1_c, 0, 0, size_c))
     {
      PrintFormat("Error in BufferRead for matrix1 C. Error code=%d", GetLastError());
      return(false);
     }
//--- GPU calculation finished
   time1_gpu = ulong((GetMicrosecondCount() - time1_gpu) / 1000);

Dies ist die erste GPU-Option. Es zeigt die Grundidee: Ein Thread berechnet ein Ausgabeelement. Diese Struktur bietet bereits Parallelität, schöpft aber noch nicht alle Möglichkeiten der Speicheroptimierung aus. Deshalb gibt es im Beispiel einen zweiten Kernel daneben. Vor dem Start werden die Argumente auf dieselbe Weise angegeben, aber die Ausführungsmethode ist anders – mit einer lokalen Arbeitsgruppe.

//--- prepare arguments for kernel 1
   kernel_index = 1;
//--- set arguments
   OpenCL.SetArgumentBuffer(kernel_index, 0, 0);
   OpenCL.SetArgumentBuffer(kernel_index, 1, 1);
   OpenCL.SetArgumentBuffer(kernel_index, 2, 2);
   OpenCL.SetArgument(kernel_index, 3, rows_a);
   OpenCL.SetArgument(kernel_index, 4, cols_a);
   OpenCL.SetArgument(kernel_index, 5, cols_b);
   uint local_work_size[2];
   local_work_size[0] = BLOCK_SIZE;
   local_work_size[1] = BLOCK_SIZE;

Hier zeigt sich der praktische Unterschied. Die erste Version parallelisiert einfach die Berechnung. Die zweite gliedert die Berechnung bereits in Blöcke. Dies ist wichtiger, als es auf den ersten Blick scheint. GPUs mögen nicht nur Parallelität, sondern auch eine gute Speicherorganisation. Daher bieten Blöcke und lokale Gruppen erhebliche Vorteile.

Fügen wir die Dimension der Arbeitsgruppen in der zweiten Kernel-Startmethode hinzu.

//--- GPU calculation start, kernel1
   time2_gpu = GetMicrosecondCount();
   if(!OpenCL.Execute(kernel_index, task_dimension, global_work_offset, global_work_size, local_work_size))
     {
      PrintFormat("Error in Execute. Error code=%d", GetLastError());
      return(false);
     }
   if(!OpenCL.BufferRead(2, matrix2_c, 0, 0, size_c))
     {
      PrintFormat("Error in BufferRead for matrix2 C. Error code=%d", GetLastError());
      return(false);
     }
//--- GPU calculation finished
   time2_gpu = ulong((GetMicrosecondCount() - time2_gpu) / 1000);
//--- remove OpenCL objects
   OpenCL.Shutdown();
//---
   return(true);
  }

Der interessanteste Teil ist nun, was innerhalb des OpenCL-Codes passiert. Die erste Kernel-Version sieht so einfach wie möglich aus.

__kernel void MatrixMult_GPU1(__global float *matrix_a,
                              __global float *matrix_b,
                              __global float *matrix_c,
                              int rows_a, int cols_a, int cols_b)
  {
   int i = get_global_id(0);
   int j = get_global_id(1);
   float sum = 0.0;
   for(int k = 0; k < cols_a; k++)
     {
      sum += matrix_a[cols_a * i + k] * matrix_b[cols_b * k + j];
     }
   matrix_c[cols_b * i + j] = sum;
  }

Dies ist eine fast wörtliche Übertragung der mathematischen Logik auf die GPU. Jeder Thread erhält seine Koordinate und berechnet ein Element der Ergebnismatrix.

Die zweite Version ist schon interessanter. Sie verwendet lokale Arrays und Thread-Synchronisierung.

__kernel void MatrixMult_GPU2(__global float *matrix_a,
                              __global float *matrix_b,
                              __global float *matrix_c,
                              int rows_a, int cols_a, int cols_b)
  {
   int group_i = get_group_id(0);
   int group_j = get_group_id(1);
   int i = get_local_id(0);
   int j = get_local_id(1);
   __local float submatrix_a[BLOCK_SIZE][BLOCK_SIZE];
   __local float submatrix_b[BLOCK_SIZE][BLOCK_SIZE];
   int offset_b = BLOCK_SIZE * group_i;
   int offset_a_start = cols_a * BLOCK_SIZE * group_j;
   float sum = (float)0.0;

Hier wird bereits deutlich, dass die Berechnung anders aufgebaut ist. Die Threads werden zu Gruppen zusammengefasst und die Daten in den internen Speicher des Blocks geladen. Dadurch wird die Anzahl der Aufrufe des globalen Speichers reduziert und das Gerät arbeitet effizienter.

Anschließend werden die Fragmente geladen und die Threads synchronisiert.

   for(int offset_a = offset_a_start;
       offset_a < offset_a_start + cols_a;
       offset_a += BLOCK_SIZE,
       offset_b += BLOCK_SIZE * cols_b)
     {
      submatrix_a[i][j] = matrix_a[offset_a + cols_a * i + j];
      submatrix_b[i][j] = matrix_b[offset_b + cols_b * i + j];
      barrier(CLK_LOCAL_MEM_FENCE);
      for(int k = 0; k < BLOCK_SIZE; k++)
         sum += submatrix_a[i][k] * submatrix_b[k][j];
      barrier(CLK_LOCAL_MEM_FENCE);
     }

Hier ist die Antwort auf die Frage, warum es zwei GPU-Implementierungen gibt. Das erste Beispiel zeigt die Parallelisierung. Die zweite zeigt die Speicheroptimierung. Und genau das ist es, was in der praktischen Arbeit oft über das Schicksal der Beschleunigung entscheidet.

Das Ergebnis wird wieder in das Ausgabearray geschrieben.

   int offset_c = BLOCK_SIZE * (cols_b * group_j + group_i);
   matrix_c[offset_c + cols_b * i + j] = sum;
  };

Diese beiden Kernel werden im Hauptprogramm anhand der Ausführungszeit verglichen und die Genauigkeit der Berechnungen wird im Vergleich mit einer CPU-Option kontrolliert.

//--- calculate CPU/GPU ratio
   double CPU_GPU_ratio1 = 0;
   double CPU_GPU_ratio2 = 0;
   if(time_gpu_method1 != 0)
      CPU_GPU_ratio1 = 1.0 * time_cpu / time_gpu_method1;
   if(time_gpu_method2 != 0)
      CPU_GPU_ratio2 = 1.0 * time_cpu / time_gpu_method2;
   PrintFormat("time CPU=%d ms, time GPU global work groups =%d ms, CPU/GPU ratio: %f",
                                           time_cpu, time_gpu_method1, CPU_GPU_ratio1);
   PrintFormat("time CPU=%d ms, time GPU local work groups  =%d ms, CPU/GPU ratio: %f",
                                           time_cpu, time_gpu_method2, CPU_GPU_ratio2);
   PrintFormat("time matrix CPU=%d ms", time_mat);

Der Vollständigkeit halber beziehen wir auch die integrierten Matrixoperationen in den Vergleich ein.

//--- matrix
   matrix<float> A, B, C;
   if(!A.Assign(matrix_a) || !B.Assign(matrix_b))
     {
      PrintFormat("Error of copy data to matrices. Error code=%d", GetLastError());
      return;
     }
   if(!A.Reshape(rows_a, cols_a) || !B.Reshape(rows_b, cols_b))
     {
      PrintFormat("Error of copy data to matrices. Error code=%d", GetLastError());
      return;
     }
   ulong time_mat = GetMicrosecondCount();
   C = A.MatMul(B);
   time_mat = ulong((GetMicrosecondCount() - time_mat) / 1000); 

In einem praktischen Experiment wurde die Zeit für die Matrixmultiplikation auf einem Hybridsystem verglichen: einer CPU und zwei GPUs: NVIDIA GeForce RTX 4060 Laptop-GPU und Intel Iris Xe-Grafik. Die naive CPU-Implementierung wurde als Basiswert verwendet und zeigte eine Zeit von etwa 2056 – 2180 ms. Hier sind die optimierten Matrixoperationen hervorzuheben, die eine stabile Zeit von etwa 32-34 ms zeigten, was für eine CPU dieser Klasse als nahe am erwarteten Leistungsniveau für vektorisierte Verarbeitung angesehen werden kann.

Ergebnisse der Matrixmultiplikation 1000*2000

Bei der Übertragung von Berechnungen auf die GPU waren die Ergebnisse uneinheitlich, was verdeutlicht, wie stark die OpenCL-Effizienz von der Organisation der Berechnungsblöcke abhängt. Im Falle des RTX 4060 betrug die Ausführungszeit bei Verwendung globaler Arbeitsgruppen etwa 38 ms, was formal keinen Vorteil gegenüber CPU-Matrixoperationen darstellt. Mit dem Übergang zu lokalen Arbeitsgruppen änderte sich die Situation jedoch radikal – die Zeit sank auf 11 ms. Dies demonstriert bereits den vollwertigen Betrieb des Kachelansatzes, bei dem die Wiederverwendung von Daten im lokalen Speicher den Druck auf den globalen Speicher verringert und eine effizientere Nutzung der GPU-Recheneinheiten ermöglicht.

Ein ähnlicher, aber deutlicherer Trend ist bei der integrierten Intel Iris Xe zu beobachten. Im Modus für globale Gruppen betrug die Ausführungszeit 213 ms, während sie bei Verwendung lokaler Gruppen auf 45 ms sank. Trotz des anhaltenden allgemeinen Rückstands gegenüber der diskreten GPU ist die relative Beschleunigung hier noch deutlicher, was die Empfindlichkeit der weniger produktiven GPU gegenüber der Optimierung des Speicherzugriffs unterstreicht.

Die Initialisierungszeit der GPU verdient besondere Aufmerksamkeit. Bei der RTX 4060 waren es etwa 99 ms, bei der Iris Xe etwa 4 ms. Dieser Faktor kann in Anwendungsszenarien nicht ignoriert werden, da er die Gesamteffizienz der Berechnungspipeline beeinflusst.

Insgesamt zeigen die Ergebnisse ein klassisches Bild des Übergangs vom speichergebundenen zum rechenoptimaler Ausführungsmodus. Die CPU bleibt bei der gegebenen Größenordnung der Aufgabe wettbewerbsfähig, während die GPUs ihren Vorteil nur bei der richtigen Organisation der lokalen Recheneinheiten ausspielen. Besonders deutlich wird dies bei der RTX 4060, wo der Unterschied zwischen einer suboptimalen und einer optimierten Implementierung etwa das Drei- bis Vierfache erreicht, was die Grenze zwischen ineffizienter und vollwertiger Nutzung des Grafikbeschleunigers definiert.


Test von Kerzenmustern

Das Beispiel mit der Matrixmultiplikation zeigt gut, wie genau eine GPU die Berechnungen beschleunigen kann. Für einen Händler ist dies jedoch nicht ausreichend. Es ist wichtiger zu verstehen, ob dieser Ansatz für ein Problem verwendet werden kann, das wirklich mit dem Markt zusammenhängt. Der nächste Schritt besteht also darin, von der abstrakten Mathematik zur Kerzenmusteranalyse überzugehen.

Kerzenmuster werden gewöhnlich als vorgefertigte Figuren mit bekannten Namen beschrieben. Auf einem Bild sind sie leicht zu erkennen, und in Büchern sehen sie oft überzeugend aus. Bei genauerer Betrachtung stellt sich jedoch die Frage, inwieweit solche Modelle tatsächlich durch Statistiken gestützt werden. Wo sind die genauen Kriterien? Woher wissen Sie, ob ein Muster nicht nur in der Theorie, sondern auch bei realen Daten funktioniert?

Anstatt im Voraus bekannte Formationen aus Lehrbüchern zu übernehmen, können Sie das Problem anders angehen. Zwingen Sie dem Markt keine vorgefertigten Vorlagen auf, sondern schauen Sie, wie er sich in ähnlichen Situationen zuvor verhalten hat.

Es werden die letzten Kerzen genommen, d.h. die aktuelle Marktsituation. Dies ist unser Referenzmuster. Das Programm geht dann die Historie durch und sucht nach Bereichen, die eine ähnliche Form haben wie diese. Der Vergleich basiert nicht auf dem Musternamen, sondern auf den tatsächlichen Kerzenmerkmalen: dem Körper, dem oberen und dem unteren Schatten. Außerdem ist eine leichte Abweichung zulässig, da der Markt fast nie das gleiche Muster perfekt wiederholt.

Wenn es einen ähnlichen Abschnitt in der Historie gibt, stellt das Programm keine Vermutungen an, sondern überprüft das Ergebnis. In dieser Situation wird ein Handel simuliert, indem die Niveaus von Take Profit und Stop Loss sowie ein Zeitlimit festgelegt werden. Dann berechnen wir, ob solche Fälle in der Vergangenheit mit Gewinn oder Verlust endeten.

Hier ist OpenCL besonders nützlich. Diese Aufgabe besteht aus einer großen Anzahl ähnlicher Prüfungen: Wir müssen viele Abschnitte der Historie durchgehen, sie mit der aktuellen Vorlage vergleichen und das Handelsergebnis für jede Option berechnen. Für die CPU ist dies möglich, aber bei einer großen Datenmenge werden die Berechnungen schwierig. Für die GPU hingegen ist es eine natürliche Belastung: viele unabhängige Berechnungen, die parallel durchgeführt werden können.

Außerdem wird in dem Artikel nicht nur eine Kombination von Handelsparametern getestet, sondern ein ganzes Netz von Optionen auf einmal. Das heißt, das Programm berücksichtigt gleichzeitig verschiedene Werte für Take Profit und Stop Loss. So können wir beurteilen, welche Parameter unter ähnlichen Marktbedingungen am sinnvollsten erschienen.

Beginnen wir mit der Logik auf der OpenCL-Seite – hier nimmt die Aufgabe ihre eigentliche Form an. Die CPU bleibt in diesem Design der Dirigent, aber die ganze harte Arbeit – Suchen, Vergleichen, Modellieren – geht an die GPU.

Wir haben es hier mit einer Analyse-Pipeline zu tun.

__kernel void PatternStats3D(__global const float4 *price,
                             __global const float  *tp,
                             __global const float  *sl,
                             __global float        *global_stats,
                             const int bars,
                             const float tolerance,
                             const int horizon)
  {
   const int lid = get_local_id(0);
   const int itp = get_global_id(1);
   const int isl = get_global_id(2);
   const int total_loc = get_local_size(0);
   const int tp_count = get_global_size(1);
   const int sl_count = get_global_size(2);

Die Quelldaten sind äußerst kompakt organisiert. Der Verlauf wird als Vektorarray vom Typ float4 übergeben. Jeder Eintrag ist eine Kerze: Eröffnung, Hoch, Tief, Schlusskurs. Dies ist wichtig. Wir teilen die Daten nicht in separate Arrays auf und erschweren den Zugriff nicht. Die GPU arbeitet besser mit dichten Strukturen, und dies wird hier voll ausgenutzt.

Die Arrays tp und sl werden getrennt übergeben. Auf diese Weise legen wir sofort die zweite und dritte Dimension der Aufgabe fest. Jeder Thread arbeitet nicht nur mit seinem eigenen Abschnitt der Historie, sondern auch mit einer bestimmten Kombination von Handelsparametern. Dadurch wird der Berechnungsraum dreidimensional: Historie × TP × SL.

Dann beginnt die eigentliche Arbeit. Jeder Thread innerhalb der Gruppe erhält seinen eigenen lid und beginnt mit dem Durchlaufen der Historie mit einem Schritt, der der Gruppengröße entspricht.

   __local int buf_stat[BLOCK_SIZE][STAT_DIM];
   int local_stat[STAT_DIM];
   for(int i=0;i<STAT_DIM;i++)
     local_stat[i]=0;
//---
   float4 pattern[PATTERN_SIZE];
   if(bars < (PATTERN_SIZE + horizon))
      return;
   for(int i = 0; i < PATTERN_SIZE; i++)
      pattern[i] = price[bars - 1 - PATTERN_SIZE + i];
//--- border
   for(int i = lid; i < (bars - horizon - PATTERN_SIZE); i += total_loc)
     {
      bool match = true;
      //--- pattern check
      for(int k = 0; k < PATTERN_SIZE ; k++)
        {
         float4 a = price[i + k];
         float body_a  = a.w - a.x;
         float body_b  = pattern[k].w - pattern[k].x;
         if(fabs(body_a - body_b) > tolerance)
           {
            match = false;
            break;
           }
         float upper_a = a.y - fmax(a.w, a.x);
         float upper_b = pattern[k].y - fmax(pattern[k].w, pattern[k].x);
         if(fabs(upper_a - upper_b) > tolerance)
           {
            match = false;
            break;
           }
         float lower_a = fmin(a.w, a.x) - a.z;
         float lower_b = fmin(pattern[k].w, pattern[k].x) - pattern[k].z;
         if(fabs(lower_a - lower_b) > tolerance)
           {
            match = false;
            break;
           }
        }

Dies ist eine klassische Methode. Wir erstellen nicht für jede Bar einen Fluss – das wäre zu teuer. Stattdessen bearbeitet jeder Thread seinen eigenen Datenstreifen. Die Last wird gleichmäßig verteilt, ohne unnötige Kernel-Starts.

Vor dem Beginn der Schleife wird ein Referenzmuster gebildet. Dazu werden die letzten Kerzen aus der Historie herangezogen. Dies ist der aktuelle Markt, der uns interessiert. Keine externen Muster. Keine Vermutungen. Nur der aktuelle Preisstatus.

Jetzt kommt der entscheidende Punkt: der Vergleich. Für jede Position in der Historie wird geprüft, ob der Abschnitt der Norm ähnlich ist. Außerdem basiert der Vergleich auf der Kerzenstruktur:

  • Körper;
  • oberer Schatten;
  • unterer Schatten.

Und das alles mit Toleranz. Dies ist eine subtile, aber wichtige Nuance. Eine genaue Übereinstimmung ist nicht erforderlich. Der Markt schafft keine perfekten Kopien. Wir sind an der Form interessiert, nicht an der Pixelidentität.

Wenn mindestens ein Element außerhalb der Toleranz liegt, wird die Übereinstimmung abgelehnt. Schnell und ohne unnötige Berechnungen. Wenn eine Übereinstimmung gefunden wird, beginnt der zweite Teil – die Handelsmodellierung. Der Einstiegspunkt wird bei der Eröffnung einer neuen Kerze nach dem Muster gewählt.

      //--- simulate a trade
      if(match)
        {
         local_stat[0] += 1;
         int open = i + PATTERN_SIZE;
         float4 bar = price[open];
         float entry = bar.x;
         float tp_val = tp[itp];
         float sl_val = sl[isl];
         float buy_tp  = entry + tp_val;
         float buy_sl  = entry - sl_val;
         float sell_tp = entry - tp_val;
         float sell_sl = entry + sl_val;
         bool buy_tp_hit  = 0, buy_sl_hit  = 0;
         bool sell_tp_hit = 0, sell_sl_hit = 0;
         for(int k = 0; k < horizon; k++)
           {
            bar = price[open + k];
            float high = bar.y;
            float low  = bar.z;
            // SL is checked first (worst-case)
            buy_sl_hit  |= (buy_tp_hit  == 0) & (buy_sl_hit  == 0) & (low  <= buy_sl);
            buy_tp_hit  |= (buy_tp_hit  == 0) & (buy_sl_hit  == 0) & (high >= buy_tp);
            sell_sl_hit |= (sell_tp_hit == 0) & (sell_sl_hit == 0) & (high >= sell_sl);
            sell_tp_hit |= (sell_tp_hit == 0) & (sell_sl_hit == 0) & (low  <= sell_tp);
            if((buy_tp_hit | buy_sl_hit) & (sell_tp_hit | sell_sl_hit))
               break;
           }
         // forced closing by time
         buy_sl_hit  |= (buy_tp_hit  == 0) & (buy_sl_hit  == 0) & (entry  > bar.w);
         buy_tp_hit  |= (buy_tp_hit  == 0) & (buy_sl_hit  == 0) & (entry  < bar.w);
         sell_sl_hit |= (sell_tp_hit == 0) & (sell_sl_hit == 0) & (entry  < bar.w);
         sell_tp_hit |= (sell_tp_hit == 0) & (sell_sl_hit == 0) & (entry  > bar.w);
         //---
         local_stat[1] += (int)buy_tp_hit;
         local_stat[2] += (int)buy_sl_hit;
         local_stat[3] += (int)sell_tp_hit;
         local_stat[4] += (int)sell_sl_hit;
        }
     }

Anschließend werden die TP- und SL-Niveaus für Kauf und Verkauf berechnet. Beachten Sie das Detail: Beide Seiten werden gleichzeitig gezählt. Das spart Berechnungen und gibt ein vollständiges Bild des Marktverhaltens.

Dann wird ein Vorwärtsdurchlauf durch die Historie mit einer Horizontbegrenzung gestartet. Bei jedem Schritt wird Folgendes überprüft:

  • ob SL erreicht worden ist;
  • ob der TP erreicht wurde.

SL wird zuerst geprüft. Dabei handelt es sich nicht um einen Zufall, sondern um die bewusste Annahme eines Worst-Case-Szenarios. Dieser Ansatz macht die Bewertung konservativer – und damit näher an der Realität.

Sobald für beide Seiten ein Ergebnis feststeht, wird die Schleife beendet. Es wird keine zusätzliche Arbeit geleistet.

Wird weder TP noch SL erreicht, wird die Position zeitabhängig zwangsweise geschlossen. Dies ist ein weiteres wichtiges Element. Wir lassen keine Trades offen – jede Situation sollte zu einem Ergebnis führen.

Alle Ergebnisse werden in dem privaten Array local_stat gesammelt. Dies ist von grundlegender Bedeutung. In diesem Stadium findet keine Synchronisierung statt. Jeder Thread arbeitet unabhängig und schnell.

Als Nächstes gehen wir zu dem Punkt über, für den alles entwickelt wurde – die lokale Aggregation. Die ersten BLOCK_SIZE-Threads schreiben ihre Ergebnisse in buf_stat. Dies ist der lokale Speicher, er ist schnell und wird von der Gruppe gemeinsam genutzt.

//--- write to 'local'
   if(lid < BLOCK_SIZE)
      for(int k = 0; k < STAT_DIM; k++)
         buf_stat[lid][k] = local_stat[k];
   barrier(CLK_LOCAL_MEM_FENCE);

Dann kommt ein zusätzlicher Durchlauf, der die Daten komprimiert, wenn es mehr Threads gibt als die Puffergröße. Auf diese Weise lässt sich eine beliebige Gruppengröße in ein festes Verkleinerungsfenster zwingen.

   for(int i = BLOCK_SIZE; i < total_loc; i += BLOCK_SIZE)
     {
      if(lid >= i && lid < (i + BLOCK_SIZE))
         for(int k = 0; k < STAT_DIM; k++)
            buf_stat[lid-i][k] += local_stat[k];
      barrier(CLK_LOCAL_MEM_FENCE);
     }

Danach wird die klassische Reduktion durchgeführt – paarweise Summierung mit Stufenreduktion.

//--- reduction
   for(int stride = BLOCK_SIZE / 2; stride > 0; stride >>= 1)
     {
      if(lid < stride)
        {
         for(int k = 0; k < STAT_DIM; k++)
           {
            buf_stat[lid][k] += buf_stat[lid + stride][k];
            buf_stat[lid + stride][k] = 0;
           }
        }
      barrier(CLK_LOCAL_MEM_FENCE);
     }

Die Ausgabe ist ein Datenstrom, der aggregierte Statistiken für die gesamte Gruppe enthält. Sie führt den letzten Schritt aus – die Aufzeichnung des Ergebnisses.

//--- write the result
   if(lid == 0)
     {
      int idx = (itp * sl_count + isl) * STAT_DIM;
      int count = buf_stat[0][0];
      global_stats[idx + 0] = count;
      float norm = (count > 0) ? (1.0f / ((float)count)) : 0.0f;
      for(int k = 1; k < STAT_DIM; k++)
         global_stats[idx + k] = buf_stat[0][k] * norm;
     }
  }

Hier findet ein wichtiger Wandel statt:

  • Es wird die Gesamtzahl der Treffer beibehalten.
  • Es werden die übrigen Werte auf Wahrscheinlichkeiten reduziert.

Wenn die Berechnung abgeschlossen ist, ist das Ergebnis keine bloße Rohdatenausgabe, sondern eine fertige Statistik. Wir können sehen:

  • wie oft eine ähnliche Situation in den historischen Daten aufgetreten ist;
  • wie oft Take Profit beim Kauf ausgelöst wurde;
  • wie oft Stop Loss ausgelöst wurde;
  • wie sich Verkäufe verhalten haben;
  • welche Kombinationen von Parametern besser aussahen.

Das Hauptprogramm trifft dann die Entscheidung. Es geht um die Statistik und nicht um ein schönes Muster. Wenn zu wenig Daten vorhanden sind, wird das Signal ignoriert. Wenn die Stichprobe ausreichend ist, werden Kauf- und Verkaufswahrscheinlichkeiten verglichen. Dann werden geeignetere Werte für Take Profit und Stop Loss ausgewählt. Erst danach kann eine Position eröffnet werden.

Dies ist ein wichtiger Punkt. In einer solchen Struktur basiert die Entscheidung nicht auf einer Vermutung oder einem starren Regelwerk. Sie basiert auf der Prüfung, wie sich der Markt unter ähnlichen Bedingungen normalerweise verhält. Dieser Ansatz garantiert zwar keinen Gewinn, entspricht aber viel eher der Idee der Systemdatenanalyse.

Testergebnisse Testergebnisse

Die Testergebnisse bestätigen dies. Das System sieht nicht perfekt aus und weist keine besonders schöne Equity-Kurve auf. Das könnte sogar gut sein, denn wir haben es nicht mit einem überangepassten Modell zu tun, sondern mit einer recht einfachen Arbeitsstruktur, die mit dem normalen Marktrauschen, Verlustserien und Drawdowns zu kämpfen hat. Dennoch ist das Ergebnis positiv.

Besonders wichtig ist dabei nicht, dass der Gewinn am Ende moderat ausfiel, sondern dass wir einen statistisch aussagekräftigen Handelsmechanismus schaffen konnten, ohne uns auf klassische Indikatoren und vorgegebene Muster zu verlassen. In dieser Konstruktion spielt OpenCL die Rolle eines Beschleunigers, der eine solche Analyse praktisch und zeitlich machbar macht.

Mit anderen Worten: Die GPU wird hier nicht für die Rentabilität der Strategie benötigt. Sie wird benötigt, um schnell eine große Anzahl historischer Übereinstimmungen zu testen und Marktdaten in eine messbare Hypothese zu verwandeln.

Testergebnisse

Fazit: OpenCL ist für die Analyse von Kerzenmustern nützlich, da es uns ermöglicht, schnell eine Vielzahl ähnlicher Situationen durchzugehen, Handelsergebnisse zu berechnen und uns auf Statistiken statt auf visuelle Vermutungen zu verlassen.


Schlussfolgerung

OpenCL in MQL5 sollte als ein Werkzeug für eine bestimmte Klasse von Aufgaben betrachtet werden. Es bewährt sich bei großen Datenmengen, wiederholten Operationen und der Möglichkeit, Berechnungen effektiv zu parallelisieren. In allen anderen Fällen bleibt die CPU die vernünftigere Wahl: Sie ist einfacher, flexibler und oft schneller, wenn es um kleine oder schlecht skalierbare Berechnungen geht.

Der Artikel geht vom Verständnis der architektonischen Einschränkungen bis hin zu einer praktischen Struktur für die Auslagerung von Berechnungen auf die GPU. Es wurde gezeigt, dass das Ergebnis nicht nur von den GPU-Fähigkeiten, sondern auch von der Qualität der Organisation der Berechnungsarchitektur bestimmt wird. Häufige Initialisierungen, redundante Datenübertragungen und zu kleine Aufgaben fressen jeden Gewinn schnell wieder auf. Im Gegenteil, bei der Wiederverwendung von Kontext, der Arbeit mit großen Arrays und der sorgfältigen Übergabe von Daten spielt die GPU ihre Stärken aus.

Besonders deutlich wird dies am Beispiel der Matrixmultiplikation: Bei ausreichender Datenmenge bietet die Parallelverarbeitung eine Vervielfachung gegenüber der CPU. Der praktische Wert der Struktur zeigt sich jedoch nicht nur in synthetischen Tests. Bei einer Aufgabe, die dem realen Handel näher kommt, kann die GPU die Forschung im Zusammenhang mit Massenoptimierungen, Hypothesentests und der Suche nach stabilen Mustern in Daten beschleunigen. An dieser Stelle wird deutlich, dass die Grafikkarte nicht für sich genommen nützlich ist, sondern als Mittel zur Skalierung der Rechendisziplin.

Die wichtigste Schlussfolgerung bleibt praktisch. Die Auslagerung auf die GPU macht ein Handelssystem nicht profitabel und ersetzt kein sinnvolles Marktmodell. Stattdessen eröffnet sie den Zugang zu einem Ausmaß an Durchrechnung und Analyse, die auf der CPU zu aufwendig oder zu langsam wäre. Das ist ihr eigentlicher Wert: Sie verspricht keine Wunder, sondern ermöglicht einen breiteren und systematischeren Forschungszyklus im MQL5.


Programme, die in diesem Artikel verwendet werden

# Name Typ Beschreibung
1 PatternStats.mq5 Expert Advisor Test-EA
2 PatternStats.cl Codebasis OpenCL-Codebibliothek

Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/22242

Beigefügte Dateien |
MQL5.zip (4.34 KB)
Auf Markov-Ketten basierendes Matrix-Prognosemodell Auf Markov-Ketten basierendes Matrix-Prognosemodell
Wir werden ein Matrix-Prognosemodell auf der Grundlage einer Markov-Kette erstellen. Was sind Markov-Ketten, und wie können wir eine Markov-Kette für den Devisenhandel nutzen?
Deterministische oszillatorische Suchmethode (DOS) Deterministische oszillatorische Suchmethode (DOS)
Der Algorithmus der deterministischen oszillatorischen Suche (DOS) ist ein innovatives globales Optimierungsverfahren, das die Vorteile von Gradienten- und Schwarmalgorithmen ohne die Verwendung von Zufallszahlen kombiniert. Der Mechanismus der Fitness-Oszillation und der Steigung ermöglicht es DOS, komplexe Suchräume auf deterministische Weise zu erkunden.
Eine alternative Log-datei mit der Verwendung der HTML und CSS Eine alternative Log-datei mit der Verwendung der HTML und CSS
In diesem Artikel werden wir eine sehr einfache, aber leistungsfähige Bibliothek zur Erstellung der HTML-Dateien schreiben, dabei lernen wir auch, wie man eine ihre Darstellung einstellen kann (nach seinem Geschmack) und sehen wir, wie man es leicht in seinem Expert Advisor oder Skript hinzufügen oder verwenden kann.
Arbitragehandel im Forex-Markt: Ein Matrix-Handelssystem mit Rückkehr zum fairen Wert mit Risikokontrolle Arbitragehandel im Forex-Markt: Ein Matrix-Handelssystem mit Rückkehr zum fairen Wert mit Risikokontrolle
Der Artikel enthält eine detaillierte Beschreibung des Berechnungsalgorithmus für Cross-Rates, eine Visualisierung der Ungleichgewichtsmatrix und Empfehlungen zur optimalen Einstellung der Parameter MinDiscrepancy und MaxRisk für einen effizienten Handel. Das System berechnet automatisch den „fairen Wert“ jedes Währungspaares anhand der Cross-Rates und generiert Kaufsignale im Falle negativer Abweichungen und Verkaufssignale im Falle positiver Abweichungen.