English Русский 日本語
preview
MQL5-Assistent-Techniken, die Sie kennen sollten (Teil 09): K-Means-Clustering mit fraktalen Wellen

MQL5-Assistent-Techniken, die Sie kennen sollten (Teil 09): K-Means-Clustering mit fraktalen Wellen

MetaTrader 5Handelssysteme | 29 Februar 2024, 09:56
148 0
Stephen Njuki
Stephen Njuki

Einführung

Dieser Artikel setzt die Betrachtung möglicher einfacher Ideen fort, die dank des MQL5-Assistenten implementiert und getestet werden können, hier mit dem Erkunden des k-Means-Algorithmus. Dieser ist wie die Hierarchische Clusteranalyse (AHC), die in diesem Artikel behandelt haben, ein Ansatz des unüberwachten Lernens zur Klassifizierung von Daten.

bannr

Bevor wir uns also an die Arbeit machen, ist es vielleicht hilfreich, noch einmal zu rekapitulieren, was wir unter AHC behandelt haben, und zu sehen, wie es sich vom k-Means Clustering unterscheidet. Beim agglomerativen hierarchischen Clustering-Algorithmus wird zunächst jeder Datenpunkt im zu klassifizierenden Datensatz als ein Cluster behandelt. Der Algorithmus fügt sie dann iterativ je nach Nähe zu Clustern zusammen. In der Regel wird die Anzahl der Cluster nicht im Voraus festgelegt, sondern der Analyst kann sie anhand des konstruierten Dendrogramms bestimmen, das das Endergebnis ist, wenn alle Datenpunkte zu einem einzigen Cluster zusammengeführt werden. Wenn der Analytiker jedoch eine bestimmte Anzahl von Clustern im Sinn hat, endet das ausgegebene Dendrogramm auf der Ebene/Höhe, auf der die Anzahl der Cluster mit der ursprünglichen Zahl des Analytikers übereinstimmt, wie wir in diesem Artikel gesehen haben. Je nachdem, wo das Dendrogramm geschnitten wird, ergeben sich unterschiedliche Clusterzahlen.

Beim K-Means-Clustering hingegen werden die Clusterzentren (Zentroiden) zunächst nach dem Zufallsprinzip auf der Grundlage einer vom Analysten vorgegebenen Zahl ausgewählt. Die Varianz jedes Datenpunkts von seinem nächstgelegenen Zentrum wird dann bestimmt, und es werden iterativ Anpassungen an den Zentrums-/Schwerpunktwerten vorgenommen, bis die Varianz für jeden Cluster am kleinsten ist.

Standardmäßig ist k-Means sehr langsam und ineffizient, weshalb es oft als naives k-Means bezeichnet wird, wobei das „naiv“ impliziert, dass es schnellere Implementierungen gibt. Ein Teil dieses Aufwands ergibt sich aus der zufälligen Zuordnung der anfänglichen Zentren zum Datensatz zu Beginn der Optimierung. Außerdem werden nach der Auswahl der zufälligen Mittelpunkte, Lloyds Algorithmus verwendet, um den richtigen Schwerpunkt und damit die richtigen Kategoriewerte zu ermitteln. Es gibt Ergänzungen und Alternativen zum Lloyd's-Algorithmus, z. B: Jenks' natürliche Brüche bei der der Schwerpunkt auf dem Mittelwert der Cluster und nicht auf dem Abstand zu den gewählten Zentren liegt; k-medians das, wie der Name schon sagt, den Cluster-Median und nicht den Schwerpunkt oder den Mittelwert als Proxy für die ideale Klassifizierung verwendet; k-medoids das tatsächliche Datenpunkte innerhalb jedes Clusters als potenziellen Schwerpunkt verwendet und dadurch robuster gegenüber Rauschen und Ausreißern ist, wie Wikipedia berichtet; und schließlich Fuzzy-Clustering bei dem die Clustergrenzen nicht eindeutig sind und Datenpunkte zu mehr als einem Cluster gehören können und dies auch tun. Dieses letzte Format ist interessant, weil nicht jeder Datenpunkt „klassifiziert“ wird, sondern eine regressive Gewichtung zugewiesen wird, die angibt, wie sehr ein bestimmter Datenpunkt zu jedem der anwendbaren Cluster gehört.

In diesem Artikel wollen wir eine weitere Art der k-Means-Implementierung vorstellen, die als effizienter gilt, und zwar k-Means++. Dieser Algorithmus stützt sich auf Lloyds Methoden wie der naive k-Means-Algorithmus, unterscheidet sich jedoch in der anfänglichen Herangehensweise an die Auswahl der zufälligen Zentren. Dieser Ansatz ist nicht so „zufällig“ wie der naive k-Means-Ansatz und konvergiert daher tendenziell viel schneller und effizienter als letzterer.


Algorithmus-Vergleich

K-means vs. K-Medians

K-means minimiert die quadrierten euklidischen Abstände zwischen Clusterpunkten und ihrem Schwerpunkt, während k-medians die Summe der absoluten Abstände der Punkte von ihrem Median innerhalb eines bestimmten Clusters minimiert (L1-Norm). Diese Unterscheidung, so wird argumentiert, macht k-medians weniger anfällig für Ausreißer und macht das Cluster besser repräsentativ für alle Datenpunkte, da das Clusterzentrum der Median aller Punkte ist und nicht deren Mittelwert. Auch der Berechnungsansatz ist unterschiedlich, da k-medians sich auf Algorithmen stützt, die auf L1-Norm basieren, während k-Means k-Means++ und den Lloyds Algorithmus verwendet. In Anwendungsfällen ist k-Means daher eher in der Lage, kugelförmige oder gleichmäßig verteilte Datensätze zu verarbeiten, während k-medians sich besser für unregelmäßige und seltsam geformte Datensätze eignet. Schließlich werden k-Mediane auch bei der Interpretation bevorzugt, da Mediane in der Regel besser für ein Cluster repräsentativ sind als ihre Mittelwerte.

K-means vs. Jenks-Natural-Breaks

Der Jenks-Natural-Breaks-Algorithmus zielt wie k-Means darauf ab, den Abstand zwischen Datenpunkt und Schwerpunkt so weit wie möglich zu minimieren, wobei der feine Unterschied darin besteht, dass dieser Algorithmus auch versucht, diese Klassen so weit wie möglich auseinander zu ziehen, damit sie unterscheidbar sind. Dies wird durch die Identifizierung von „natürlichen Gruppierungen“ von Daten erreicht. Diese „natürlichen Gruppierungen“ werden innerhalb der Cluster an Punkten identifiziert, an denen die Varianz deutlich zunimmt, und diese Punkte werden als Brüche bezeichnet, woher der Algorithmus seinen Namen hat. Die Brüche werden hervorgehoben, indem die Varianz innerhalb jedes Clusters minimiert wird. Sie eignet sich besser für klassifizierende Datensätze als für regressive oder kontinuierliche Typen. Wie der k-Median-Algorithmus bietet er damit Vorteile bei der Empfindlichkeit gegenüber Ausreißern und bei der Gesamtauswertung im Vergleich zum typischen k-Means.

K-means vs. K-Medoide

Wie bereits erwähnt, stützt sich K-medoids zunächst auf die tatsächlichen Daten und nicht auf fiktive Schwerpunktpunkte. In dieser Hinsicht ähnelt sie der Agglomerativen Hierarchischen Klassifikation, allerdings werden hier keine Dendrogramme erstellt. Die ausgewählten Datenpunkte, die als Zentren verwendet werden, sind diejenigen mit dem geringsten Abstand zu allen anderen Datenpunkten innerhalb des Clusters. Bei dieser Auswahl kann auch eine Vielzahl von Entfernungsmessverfahren eingesetzt werden, darunter der Manhattan-Abstand oder die Kosinus-Ähnlichkeit. Da es sich bei den Zentroiden um tatsächliche Datenpunkte handelt, kann argumentiert werden, dass sie wie Junks und K-Medians repräsentativer für die zugrunde liegenden Daten sind als k-Means, allerdings sind sie rechnerisch ineffizienter, vor allem bei großen Datensätzen.

K-means vs. Fuzzy-Clustering

Beim Fuzzy-Clustering wird, wie bereits erwähnt, jedem Datenpunkt ein regressives Gewicht zugewiesen, das je nach Anzahl der beteiligten Cluster in einem Vektorformat vorliegt. Dieses Gewicht würde für jeden Cluster im Bereich von 0,0 bis 1,0 liegen, da ein Fuzzy-Prototyp (Zugehörigkeitsfunktion) verwendet wird, im Gegensatz zu k-Means, das einen definitiven Schwerpunkt verwendet. Dies liefert in der Regel mehr Informationen und ist daher besser repräsentativ für die Daten. Es übertrifft das typische k-Means in allen oben genannten Punkten, wobei der größte Nachteil in der erwartungsgemäß hohen Rechenleistung liegt.

K-means++

Um das naive k-Means-Clustering effizienter zu machen, wird in der Regel und auch in diesem Artikel die Initialisierung k-Means++ verwendet, bei der die anfänglichen Zentren weniger zufällig sind, sondern eher proportional über die Daten verteilt werden. Dies hat bei den Tests zu wesentlich schnelleren Lösungen und zur Konvergenz mit den Zielschwerpunkten geführt. Insgesamt wird eine bessere Clusterqualität erreicht, und die Empfindlichkeit gegenüber Ausreißerdaten, aber auch gegenüber der anfänglichen Auswahl der Schwerpunktpunkte ist geringer.


Daten

Wie im Artikel über Agglomeratives Hierarchisches Clustering werden wir die fertigen K-means-Klassen von AlgLib verwenden, um einen einfachen und ähnlichen Algorithmus zu entwickeln, wie wir ihn für diesen Artikel hatten, und sehen, ob wir ein kreuzvalidiertes Ergebnis erhalten können. Das zu testende Wertpapier ist GBPUSD, und wir führen Tests vom 2022.01.01 bis zum 2023.02.01 durch und machen dann einen Walk-Forward von diesem Datum bis zum 2023.10.01. Wir werden den täglichen Zeitrahmen verwenden und die letzten Durchläufe mit echten Ticks über den Testzeitraum durchführen.


Struktur

Die Datenstruktur, die zur Organisation der Cluster verwendet wurde, ist identisch mit der, die wir in dem AHC-Artikel hatten, und tatsächlich sind das Verfahren und die verwendeten Signalideen ziemlich identisch. Der Hauptunterschied besteht darin, dass wir bei der agglomerativen Clusterung eine Funktion ausführen mussten, um die Cluster auf der Ebene abzurufen, die unserer Zielclusterzahl entspricht, und daher die Funktion „ClusterizerGetKClusters“ aufgerufen haben, was wir hier nicht tun. Außerdem mussten wir besonders vorsichtig sein und sicherstellen, dass die Struktur tatsächlich Preisinformationen erhält, und zu diesem Zweck überprüfen wir eine Menge auf ungültige Zahlen, wie in diesem kurzen Ausschnitt unten zu sehen ist:

      double _dbl_min=-1000.0,_dbl_max=1000.0;
      
      for(int i=0;i<m_training_points;i++)
      {
         for(int ii=0;ii<m_point_features;ii++)
         {
            double _value=m_close.GetData(StartIndex()+i)-m_close.GetData(StartIndex()+ii+i+1);
            if(_dbl_min>=_value||!MathIsValidNumber(_value)||_value>=_dbl_max){ _value=0.0; }
            m_data.x.Set(i,ii,_value);
            matrix _m=m_data.x.ToMatrix();if(_m.HasNan()){ _m.ReplaceNan(0.0); }m_data.x=CMatrixDouble(_m);
         }
         
         if(i>0)//assign classifier only for data points for which eventual bar range is known
         {
            double _value=m_close.GetData(StartIndex()+i-1)-m_close.GetData(StartIndex()+i);
            if(_dbl_min>=_value||!MathIsValidNumber(_value)||_value>=_dbl_max){ _value=0.0; }
            m_data.y.Set(i-1,_value);
            vector _v=m_data.y.ToVector();if(_v.HasNan()){ _v.ReplaceNan(0.0); }m_data.y=CRowDouble(_v);
         }
      }


ALGLIB

Auf die AlgLib-Bibliothek wurde in dieser Serie bereits mehrfach Bezug genommen, sodass wir gleich zum Code für die Bildung unserer Cluster übergehen werden. Zwei Funktionen in der Bibliothek stehen im Mittelpunkt: die Funktion „SelectInitialCenters“, die entscheidend zur Beschleunigung des gesamten Prozesses beiträgt, da, wie bereits erwähnt, eine zu zufällige anfängliche Auswahl von Clustern die Konvergenz zu den richtigen Clustern in die Länge zieht. Sobald diese Funktion ausgeführt wird, verwenden wir den Lloyd-Algorithmus zur Feinabstimmung der anfänglichen Clusterauswahl und wenden uns dafür der Funktion „KMeansGenerateInternal“ zu.

Die Auswahl der anfänglichen Cluster mit der verfügbaren Funktion kann auf eine von 3 Arten erfolgen, entweder zufällig, mit k-Means++ oder mit Fast-Greedy-Initialisierung. Gehen wir kurz auf die einzelnen Punkte ein. Bei der zufälligen Clusterauswahl wie in den beiden anderen Fällen werden die ausgegebenen Cluster in einer Ausgabematrix mit der Bezeichnung „ct“ gespeichert, wobei jede Zeile einen Cluster darstellt, sodass die Anzahl der Zeilen von „ct“ der beabsichtigten Clusterzahl entspricht, während die Spalten den Merkmalen oder der Vektorkardinalzahl jedes Datenpunkts im Datensatz entsprechen. Mit der Option „random“ wird also einfach jeder Zeile von „ct“ einmal ein zufällig ausgewählter Datenpunkt aus dem Eingabedatensatz zugewiesen. Dies ist unten angegeben:

//--- Random initialization
   if(initalgo==1)
     {
      for(i=0; i<k; i++)
        {
         j=CHighQualityRand::HQRndUniformI(rs,npoints);
         ct.Row(i,xy[j]+0);
        }
      return;
     }


Bei K-means++ wird ebenfalls zunächst ein zufälliges Zentrum gewählt, allerdings nur für den ersten Cluster, im Gegensatz zu früher, wo wir dies für alle Cluster taten. Anschließend wird der Abstand zwischen jedem Punkt des Datensatzes und dem zufällig ausgewählten Clusterzentrum gemessen und die quadrierte Summe dieser Abstände für jede Zeile (oder jeden potenziellen Cluster) protokolliert, und falls diese Summe Null ist, wird einfach ein zufälliger Schwerpunkt für diesen Cluster gewählt. Für alle von Null verschiedenen Summen, die in der Variablen „s“ gespeichert sind, wählen wir den Punkt, der am weitesten von unserem zufällig ausgewählten Ausgangscluster entfernt ist. Der Code ist ziemlich komplex, aber dieser kurze Ausschnitt mit Kommentaren könnte mehr Aufschluss geben:

//--- k-means++ initialization
   if(initalgo==2)
     {
      //--- Prepare distances array.
      //--- Select initial center at random.
      initbuf.m_ra0=vector<double>::Full(npoints,CMath::m_maxrealnumber);
      ptidx=CHighQualityRand::HQRndUniformI(rs,npoints);
      ct.Row(0,xy[ptidx]+0);
      //--- For each newly added center repeat:
      //--- * reevaluate distances from points to best centers
      //--- * sample points with probability dependent on distance
      //--- * add new center
      for(cidx=0; cidx<k-1; cidx++)
        {
         //--- Reevaluate distances
         s=0.0;
         for(i=0; i<npoints; i++)
           {
            v=0.0;
            for(j=0; j<=nvars-1; j++)
              {
               vv=xy.Get(i,j)-ct.Get(cidx,j);
               v+=vv*vv;
              }
            if(v<initbuf.m_ra0[i])
               initbuf.m_ra0.Set(i,v);
            s+=initbuf.m_ra0[i];
           }
         //
         //--- If all distances are zero, it means that we can not find enough
         //--- distinct points. In this case we just select non-distinct center
         //--- at random and continue iterations. This issue will be handled
         //--- later in the FixCenters() function.
         //
         if(s==0.0)
           {
            ptidx=CHighQualityRand::HQRndUniformI(rs,npoints);
            ct.Row(cidx+1,xy[ptidx]+0);
            continue;
           }
         //--- Select point as center using its distance.
         //--- We also handle situation when because of rounding errors
         //--- no point was selected - in this case, last non-zero one
         //--- will be used.
         v=CHighQualityRand::HQRndUniformR(rs);
         vv=0.0;
         lastnz=-1;
         ptidx=-1;
         for(i=0; i<npoints; i++)
           {
            if(initbuf.m_ra0[i]==0.0)
               continue;
            lastnz=i;
            vv+=initbuf.m_ra0[i];
            if(v<=vv/s)
              {
               ptidx=i;
               break;
              }
           }
         if(!CAp::Assert(lastnz>=0,__FUNCTION__": integrity error"))
            return;
         if(ptidx<0)
            ptidx=lastnz;
         ct.Row(cidx+1,xy[ptidx]+0);
        }
      return;
     }

Wie immer hat AlgLib eine öffentliche Dokumentation, die als Referenz für weitere Erklärungen dienen kann.

Für den Fast-Greedy-Initialisierungsalgorithmus schließlich, der von einer Variante von k-Means inspiriert wurde, nämlich k-Means++ wird eine Reihe von Runden durchgeführt, wobei in jeder Runde Berechnungen für den Abstand, der dem aktuell ausgewählten Schwerpunkt am nächsten liegt, vorgenommen werden; dann wird eine unabhängige Stichprobe von etwa der Hälfte der erwarteten Clustergröße durchgeführt, wobei die Wahrscheinlichkeit der Auswahl eines Punktes proportional zu seinem Abstand vom aktuellen Schwerpunkt ist, was so lange wiederholt wird, bis die Anzahl der Stichprobenpunkte doppelt so groß ist wie die Anzahl der Punkte, die einen Cluster füllen würden; und dann wird mit der ausgewählten extragroßen Stichprobe eine „gierige-Auswahl“ aus dieser Stichprobe durchgeführt, bis der kleinere Stichprobenumfang erreicht ist, wobei die Punkte, die am weitesten von den Schwerpunkten entfernt sind, Priorität haben. Ein sehr rechenintensiver und komplizierter Prozess, dessen Code mit Kommentaren unten angegeben ist:

//--- "Fast-greedy" algorithm based on "Scalable k-means++".
//--- We perform several rounds, within each round we sample about 0.5*K points
//--- (not exactly 0.5*K) until we have 2*K points sampled. Before each round
//--- we calculate distances from dataset points to closest points sampled so far.
//--- We sample dataset points independently using distance xtimes 0.5*K divided by total
//--- as probability (similar to k-means++, but each point is sampled independently;
//--- after each round we have roughtly 0.5*K points added to sample).
//--- After sampling is done, we run "greedy" version of k-means++ on this subsample
//--- which selects most distant point on every round.
   if(initalgo==3)
     {
      //--- Prepare arrays.
      //--- Select initial center at random, add it to "new" part of sample,
      //--- which is stored at the beginning of the array
      samplesize=2*k;
      samplescale=0.5*k;
      CApServ::RMatrixSetLengthAtLeast(initbuf.m_rm0,samplesize,nvars);
      ptidx=CHighQualityRand::HQRndUniformI(rs,npoints);
      initbuf.m_rm0.Row(0,xy[ptidx]+0);
      samplescntnew=1;
      samplescntall=1;
      initbuf.m_ra1=vector<double>::Zeros(npoints);
      CApServ::IVectorSetLengthAtLeast(initbuf.m_ia1,npoints);
      initbuf.m_ra0=vector<double>::Full(npoints,CMath::m_maxrealnumber);
      //--- Repeat until samples count is 2*K
      while(samplescntall<samplesize)
        {
         //--- Evaluate distances from points to NEW centers, store to RA1.
         //--- Reset counter of "new" centers.
         KMeansUpdateDistances(xy,0,npoints,nvars,initbuf.m_rm0,samplescntall-samplescntnew,samplescntall,initbuf.m_ia1,initbuf.m_ra1);
         samplescntnew=0;
         //--- Merge new distances with old ones.
         //--- Calculate sum of distances, if sum is exactly zero - fill sample
         //--- by randomly selected points and terminate.
         s=0.0;
         for(i=0; i<npoints; i++)
           {
            initbuf.m_ra0.Set(i,MathMin(initbuf.m_ra0[i],initbuf.m_ra1[i]));
            s+=initbuf.m_ra0[i];
           }
         if(s==0.0)
           {
            while(samplescntall<samplesize)
              {
               ptidx=CHighQualityRand::HQRndUniformI(rs,npoints);
               initbuf.m_rm0.Row(samplescntall,xy[ptidx]+0);
               samplescntall++;
               samplescntnew++;
              }
            break;
           }
         //--- Sample points independently.
         for(i=0; i<npoints; i++)
           {
            if(samplescntall==samplesize)
               break;
            if(initbuf.m_ra0[i]==0.0)
               continue;
            if(CHighQualityRand::HQRndUniformR(rs)<=(samplescale*initbuf.m_ra0[i]/s))
              {
               initbuf.m_rm0.Row(samplescntall,xy[i]+0);
               samplescntall++;
               samplescntnew++;
              }
           }
        }
      //--- Run greedy version of k-means on sampled points
 
      initbuf.m_ra0=vector<double>::Full(samplescntall,CMath::m_maxrealnumber);
      ptidx=CHighQualityRand::HQRndUniformI(rs,samplescntall);
      ct.Row(0,initbuf.m_rm0[ptidx]+0);
      for(cidx=0; cidx<k-1; cidx++)
        {
         //--- Reevaluate distances
         for(i=0; i<samplescntall; i++)
           {
            v=0.0;
            for(j=0; j<nvars; j++)
              {
               vv=initbuf.m_rm0.Get(i,j)-ct.Get(cidx,j);
               v+=vv*vv;
              }
            if(v<initbuf.m_ra0[i])
               initbuf.m_ra0.Set(i,v);
           }
         //--- Select point as center in greedy manner - most distant
         //--- point is selected.
         ptidx=0;
         for(i=0; i<samplescntall; i++)
           {
            if(initbuf.m_ra0[i]>initbuf.m_ra0[ptidx])
               ptidx=i;
           }
         ct.Row(cidx+1,initbuf.m_rm0[ptidx]+0);
        }
      return;
     }


Dieses Verfahren gewährleistet repräsentative Zentren und Effizienz für die nächste Phase.

Nachdem die ersten Zentren ausgewählt wurden, wird der Lloyd-Algorithmus angewendet, der die Kernfunktion von „KMeansGenerateInternal“ ist. Die Implementierung von AlgLib erscheint komplex, aber die Grundlagen des Lloyd-Algorithmus bestehen darin, iterativ nach dem Schwerpunkt jedes Clusters zu suchen und dann jeden Cluster neu zu definieren, indem die Datenpunkte von einem Cluster in einen anderen verschoben werden, um den Abstand zwischen dem Schwerpunkt jedes Clusters und den zugehörigen Punkten zu minimieren.

Für diesen Artikel, wie auch für den Artikel über Dendrogramme, sind die Datenpunkte einfach die Änderungen des Schlusskurses des Handelsinstrument, das in unserem Test GBPUSD war.


Vorhersagen

K-means ist wie AHC von Natur aus eine Klassifizierung, die nicht überwacht wird. Wenn wir also eine Regression oder Vorhersage durchführen wollen, müssen wir die Daten der Spalte „y“ anhängen, die um unseren geclusterten Datensatz verzögert ist. Diese „y“-Daten werden also auch Änderungen des Schlusskurses sein, aber 1 Bar vor den geclusterten Daten, um die Cluster effektiv zu kennzeichnen, und aus Gründen der Effizienz wird der „y“-Datensatz von derselben for-Schleife gefüllt, die auch den zu clusternden x-Matrix-Datensatz füllt. Dies geht aus der nachstehenden kurzen Auflistung hervor:

         if(i>0)//assign classifier only for data points for which eventual bar range is known
         {
            double _value=m_close.GetData(StartIndex()+i-1)-m_close.GetData(StartIndex()+i);
            if(_dbl_min>=_value||!MathIsValidNumber(_value)||_value>=_dbl_max){ _value=0.0; }
            m_data.y.Set(i-1,_value);
            vector _v=m_data.y.ToVector();if(_v.HasNan()){ _v.ReplaceNan(0.0); }m_data.y=CRowDouble(_v);
         }

Sobald die „x“-Matrix und das „y“-Array mit Daten gefüllt sind, erfolgt die Cluster-Definition in den bereits erwähnten Schritten, und anschließend wird das Cluster der aktuellen Schlusskursänderungen oder der obersten Zeile der „x“-Matrix identifiziert. Da er zusammen mit den anderen Datenpunkten zum Clustering verarbeitet wird, würde er einen Cluster-Index haben. Mit diesem Cluster-Index vergleichen wir ihn mit bereits „gekennzeichneten“ Datenpunkten, d. h. mit Daten, für die die eventuelle Preisänderung bei Börsenschluss bekannt ist, um die Summe dieser eventuellen Änderungen zu erhalten. Mit dieser Summe können wir leicht die durchschnittliche Änderung ermitteln, die uns, wenn wir sie mit der aktuellen Spanne (oder Volatilität) normalisieren, eine Gewichtung im Bereich von 0 bis 1 liefert.

//+------------------------------------------------------------------+
//| "Voting" that price will fall.                                   |
//+------------------------------------------------------------------+
int CSignalKMEANS::ShortCondition(void)
  {
      ...
      
      double _output=GetOutput();
      
      int _range_size=1;
      
      double _range=m_high.GetData(m_high.MaxIndex(StartIndex(),StartIndex()+_range_size))-m_low.GetData(m_low.MinIndex(StartIndex(),StartIndex()+_range_size));
      
      _output/=fmax(_range,m_symbol.Point());
      _output*=100.0;
      
      if(_output<0.0){ result=int(fmax(-100.0,round(_output)))*-1; }
      
      ...
  }


Die Funktionen „LongCondition“ (Kaufen) und „ShortCondition“ (Verkaufen) geben Werte im Bereich von 0 bis 100 zurück, sodass unser normierter Wert mit 100 multipliziert werden müsste.


Bewertung und Ergebnisse

Bei einem Backtest über den Zeitraum vom 2022.01.01 bis 2023.02.01 erhalten wir den folgenden Bericht:

b_1

Dieser Bericht stützt sich auf diese Eingaben, die aus einem Optimierungslauf gewonnen wurden:

i_1

Wenn wir mit diesen Einstellungen von 2023.02.02 bis 2023.10.01 einen Vorwärtstest machen, erhalten wir folgenden Bericht:

f_1

Die Ergebnisse dieses sehr kurzen Testzeitraums sind vielversprechend, aber wie immer empfiehlt es sich, mehr Sorgfalt walten zu lassen und über längere Zeiträume zu testen.


Implementierung mit Fractal Waves

Betrachten wir nun eine Option, die Daten aus dem Fraktal-Indikator verwendet, im Gegensatz zu den Veränderungen des Schlusskurses. Der Fraktal-Indikator ist etwas schwierig zu verwenden, vor allem wenn man versucht, ihn mit einem Expert Advisor zu implementieren, weil die Puffer, wenn sie aktualisiert werden, keine Indikatorwerte oder Preise für jeden Index enthalten. Sie müssen jeden Pufferindex daraufhin überprüfen, ob es tatsächlich ein „Fraktal“ (d. h. einen Preis) gibt, und wenn es kein „Fraktal“ gibt, ist der „Standardplatzhalter“ der Maximalwert des Typs double (DBL_MAX). So bereiten wir die Fraktaldaten in der überarbeiteten Funktion „GetOutput“ auf:

//+------------------------------------------------------------------+
//| Get k-means cluster output from identified cluster.              |
//+------------------------------------------------------------------+
double CSignalKMEANS::GetOutput()
   {
      ...
      
      int _size=m_training_points+m_point_features+1,_index=0;
      
      for(int i=0;i<m_fractals.Available();i++)
      {
         double _0=m_fractals.GetData(0,i);
         double _1=m_fractals.GetData(1,i);
         
         if(_0!=DBL_MAX||_1!=DBL_MAX)
         { 
            double _v=0.0;
            if(_0!=DBL_MAX){_v=_0;}
            if(_1!=DBL_MAX){_v=_1;}
            if(!m_loaded){ m_wave[_index]=_v; _index++; } 
            else
            {
               for(int i=_size-1;i>0;i--){ m_wave[i]=m_wave[i-1]; }
               m_wave[0]=_v; break;
            }
         }
         
         if(_index>=int(m_wave.Size())){ break; }
      }
      
      if(!m_loaded){ m_loaded=true; }
      
      if(m_wave[_size-1]==0.0){ return(0.0); }

      ...
      
      ...
   }


Um aktuelle Preisfraktale zu erhalten, müssen wir zunächst das Fraktalindikatorobjekt ordnungsgemäß aktualisieren. Sobald dies geschehen ist, müssen wir die Gesamtzahl der verfügbaren Pufferindizes ermitteln. Dieser Wert gibt an, wie viele Indizes wir in einer for-Schleife durchlaufen müssen, während wir nach den fraktalen Preispunkten suchen. Dabei ist zu beachten, dass der Fraktalindikator über 2 Puffer mit den Indizes 0 und 1 verfügt. Der 0-Puffer-Index ist für Fraktale der Hochs, während der 1-Index-Puffer für die Fraktale der Tiefs bestimmt ist. Das bedeutet, dass wir in unserer for-Schleife 2 Werte haben, die gleichzeitig diese Indexpuffer auf fraktale Preispunkte überprüfen, und wenn einer von ihnen einen Preis registriert (nur einer von ihnen kann gleichzeitig einen Preis registrieren), fügen wir diesen Wert zu unserem Vektor 'm_wave' hinzu.

Nun ist die Zahl der verfügbaren fraktalen Indizes, die uns als Suchgrenze für fraktale Preispunkte dienen, in der Regel begrenzt. Das bedeutet, dass wir, obwohl wir sagen, dass wir einen Wellenpuffer von 12 Indizes haben wollen, am Ende nur 3 beim ersten Durchlauf oder beim allerersten Kursbalken abrufen können. Dies bedeutet, dass unser Wellenpuffer wie ein richtiger Puffer funktionieren muss, der alle Preisindizes speichert, die er abrufen kann, und wartet, bis ein neuer fraktaler Preis verfügbar ist, damit er dem Puffer hinzugefügt werden kann. Dieser Vorgang wird fortgesetzt, bis der Puffer gefüllt ist. Und da der Puffer noch nicht abgelegt oder initialisiert ist, kann der Expert Advisor in der Zwischenzeit keine Signale verarbeiten und befindet sich im Wesentlichen in einer Initialisierungsphase.

Daher ist die Größe des Puffers, der für das Abrufen der Fraktale verwendet wird, von großer Bedeutung. Da diese Fraktale in den k-Means-Clustering-Algorithmus eingegeben werden, bedeutet dies bei unserem System der Verwendung fraktaler Preisänderungen, dass die Größe dieses Puffers die Summe aus der Anzahl der Trainingspunkte, der Anzahl der Merkmale und 1 ist. Wir fügen die 1 am Ende hinzu, denn obwohl unsere Eingabedatenmatrix nur Trainingspunkte plus Merkmale benötigt, ist die zusätzliche Zeile die aktuelle Zeile der Punkte, die noch nicht regressiert sind, d. h. für die wir keinen „y“-Wert haben.

Diese unglückliche Sorgfalt ist also notwendig, aber sobald wir dies hinter uns gelassen haben, erhalten wir Preisinformationen, die in einem wellenförmigen Muster angeordnet sind. Die These ist, dass die Veränderungen zwischen den einzelnen Wellenscheitelpunkten, den fraktalen Preispunkten, die Schlusskursveränderungen ersetzen können, die wir in unserer ersten Implementierung verwendet haben.

Ironischerweise konnten wir uns beim Testen dieses neuen Expertenberaters nicht die Freiheit nehmen, keine Positionspreisausstiege (TP & SL) zu verwenden, wie wir es bei Close-Preisänderungen getan haben, und mussten stattdessen mit TP testen. Und nach dem Testen, obwohl der Backtest vielversprechend war, waren wir nicht in der Lage, einen profitablen Vorwärtstest mit den besten Optimierungsergebnissen zu erhalten, wie wir es mit den engen Preisänderungen getan haben. Hier sind die Berichte.

b_2


f_2

Wenn wir uns die kontinuierliche, ununterbrochene Aktienkurve dieser Geschäfte ansehen, können wir deutlich erkennen, dass die Entwicklung trotz eines vielversprechenden ersten Laufs nicht vielversprechend ist.

g

Dies bedeutet im Grunde, dass diese Idee überarbeitet werden muss, und ein Ausgangspunkt dafür könnte sein, den fraktalen Indikator zu überarbeiten und vielleicht eine benutzerdefinierte Version zu haben, die erstens effizienter ist, da sie nur fraktale Preispunkte hat, und zweitens mit einigen Eingaben anpassbar ist, die die minimale Preisbewegung zwischen jedem fraktalen Punkt leiten oder quantifizieren.


Schlussfolgerung

Zusammenfassend haben wir uns mit dem k-Means Clustering befasst und gezeigt, wie eine Standardimplementierung dank AlgLib in zwei verschiedenen Situationen realisiert werden kann: mit rohen Schlusskursen und mit fraktalen Kursdaten.

Kreuzvalidierungstests beider Einstellungen haben in einem vorläufigen Stadium unterschiedliche Ergebnisse erbracht, wobei das System der rohen Schlusskurse vielversprechender zu sein scheint als der Ansatz der fraktalen Preise. Wir haben einige Gründe, warum dies so ist, und den Quellcode, der dabei verwendet wurde, weiter unten mitgeteilt.


Referenzen

Wikipedia

ResearchGate


Anhang

Die beigefügte Quellenangabe zu diesem Artikel über MQL5-Assistenten zu verwenden, könnte hilfreich sein.


Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/13915

Beigefügte Dateien |
Kmeans.mq5 (6.66 KB)
kmeans_r1.mq5 (6.85 KB)
SignalWZ_9.mqh (10.17 KB)
SignalWZ_9_r1.mqh (11.5 KB)
Entwurfsmuster in der Softwareentwicklung und MQL5 (Teil 4): Verhaltensmuster 2 Entwurfsmuster in der Softwareentwicklung und MQL5 (Teil 4): Verhaltensmuster 2
In diesem Artikel werden wir unsere Serie über das Thema Entwurfmuster abschließen. Wir haben erwähnt, dass es drei Arten von Entwurfmuster gibt: Erzeugungs-, Verhaltens- und strukturelle Muster. Wir werden die verbleibenden Muster des Verhaltenstyps vervollständigen, die dabei helfen können, die Methode der Interaktion zwischen Objekten so festzulegen, dass unser Code sauber wird.
Filterung und Merkmalsextraktion von Frequenzen Filterung und Merkmalsextraktion von Frequenzen
In diesem Artikel untersuchen wir die Anwendung digitaler Filter auf Zeitreihen, die im Frequenzbereich dargestellt werden, um einzigartige Merkmale zu extrahieren, die für Vorhersagemodelle nützlich sein können.
Entwicklung eines Wiedergabesystems — Marktsimulation (Teil 21): FOREX (II) Entwicklung eines Wiedergabesystems — Marktsimulation (Teil 21): FOREX (II)
Wir werden weiterhin ein System für die Arbeit auf dem FOREX-Markt aufbauen. Um dieses Problem zu lösen, müssen wir zuerst das Laden der Ticks deklarieren, bevor wir die vorherigen Balken laden. Dies löst zwar das Problem, zwingt den Nutzer aber gleichzeitig dazu, sich an eine bestimmte Struktur in der Konfigurationsdatei zu halten, was ich persönlich nicht sehr sinnvoll finde. Der Grund dafür ist, dass wir durch die Entwicklung eines Programms, das für die Analyse und Ausführung der Konfigurationsdatei verantwortlich ist, dem Nutzer die Möglichkeit geben können, die von ihm benötigten Elemente in beliebiger Reihenfolge zu deklarieren.
Datenwissenschaft und maschinelles Lernen (Teil 16): Ein frischer Blick auf die Entscheidungsbäume Datenwissenschaft und maschinelles Lernen (Teil 16): Ein frischer Blick auf die Entscheidungsbäume
Tauchen wir ein in die komplizierte Welt der Entscheidungsbäume in der neuesten Folge unserer Serie über Datenwissenschaft und maschinelles Lernen. Dieser Artikel ist auf Händler zugeschnitten, die nach strategischen Einsichten suchen, und dient als umfassende Zusammenfassung, die die wichtige Rolle von Entscheidungsbäumen bei der Analyse von Markttrends beleuchtet. Wir erforschen die Wurzeln und Äste dieser algorithmischen Bäume und erschließen Sie deren Potenzial zur Verbesserung Ihrer Handelsentscheidungen. Erleben Sie mit uns eine erfrischende Perspektive auf Entscheidungsbäume und entdecken Sie, wie sie Ihnen bei der Navigation durch die Komplexität der Finanzmärkte behilflich sein können.