English 日本語
preview
Erforschung fortgeschrittener maschineller Lerntechniken bei der Darvas Box Breakout Strategie

Erforschung fortgeschrittener maschineller Lerntechniken bei der Darvas Box Breakout Strategie

MetaTrader 5Handel |
42 2
Zhuo Kai Chen
Zhuo Kai Chen

Einführung

Die von Nicolas Darvas entwickelte Darvas-Box-Breakout-Strategie ist ein technischer Handelsansatz, der potenzielle Kaufsignale erkennt, wenn der Kurs einer Aktie über einen festgelegten Bereich der „Box“ ansteigt, was auf eine starke Aufwärtsdynamik hindeutet. In diesem Artikel werden wir dieses Strategiekonzept als Beispiel anwenden, um drei fortgeschrittene Techniken des maschinellen Lernens zu untersuchen. Dazu gehören die Verwendung eines maschinellen Lernmodells zur Generierung von Signalen anstelle von Handelsfiltern, die Verwendung von kontinuierlichen Signalen anstelle von diskreten Signalen und die Verwendung von Modellen, die auf verschiedenen Zeitrahmen trainiert wurden, um Handelsgeschäfte zu bestätigen. Diese Methoden bieten neue Perspektiven, wie maschinelles Lernen den algorithmischen Handel über die traditionellen Praktiken hinaus verbessern kann.

Dieser Artikel befasst sich eingehend mit den Merkmalen und der Theorie hinter drei fortgeschrittenen Techniken, die von Pädagogen nur selten behandelt werden, da sie im Vergleich zu traditionellen Methoden innovativ sind.  Außerdem werden Einblicke in fortgeschrittene Themen wie Feature-Engineering und Hyperparameter-Tuning während des Modelltrainings gewährt. Es wird jedoch nicht jeder Schritt des Arbeitsablaufs beim Training von Modellen für maschinelles Lernen im Detail behandelt. Leser, die sich für die übersprungenen Verfahren interessieren, finden unter diesem Artikel den vollständigen Implementierungsprozess.


Signalerzeugung

Beim maschinellen Lernen gibt es drei Haupttypen: überwachtes Lernen, unüberwachtes Lernen und Verstärkungslernen. Im quantitativen Handel verwenden Händler meist das überwachte Lernen, und zwar aus zwei Hauptgründen.
  1. Unüberwachtes Lernen ist oft zu einfach, um die komplexen Beziehungen zwischen Handelsergebnissen und Marktmerkmalen zu erfassen. Ohne Kennzeichnungen ist es schwierig, die Vorhersageziele zu erreichen, und es eignet sich besser für die Vorhersage indirekter Daten als für die direkten Ergebnisse einer Handelsstrategie.
  2. Verstärkungslernen erfordert die Einrichtung einer Trainingsumgebung mit einer Belohnungsfunktion, die auf die Maximierung des langfristigen Gewinns abzielt, anstatt sich auf genaue Einzelvorhersagen zu konzentrieren. Dieser Ansatz erfordert ein kompliziertes Setup für die einfache Aufgabe, Ergebnisse vorherzusagen, was ihn für Kleinhändler weniger kosteneffizient macht.

Dennoch bietet das überwachte Lernen zahlreiche Anwendungen für den algorithmischen Handel. Eine gängige Methode ist die Verwendung als Filter: Sie beginnen mit einer ursprünglichen Strategie, die viele Stichproben erzeugt, und trainieren dann ein Modell, um zu erkennen, wann die Strategie wahrscheinlich erfolgreich ist oder scheitert. Das Konfidenzniveau des Modells trägt dazu bei, die von ihm vorhergesagten schlechten Abschlüsse herauszufiltern.

Ein anderer Ansatz, den wir in diesem Artikel untersuchen werden, ist die Verwendung von überwachtem Lernen zur Erzeugung von Signalen. Bei typischen Regressionsaufgaben wie der Preisvorhersage ist es ganz einfach: Kaufen Sie, wenn das Modell einen Preisanstieg vorhersagt, und verkaufen Sie, wenn es einen Rückgang vorhersagt. Doch wie lässt sich dies mit einer Kernstrategie wie dem Darvas Box Breakout verbinden?

Zunächst werden wir einen EA entwickeln, um die erforderlichen Merkmalsdaten und Beschriftungsdaten für das spätere Training des Modells in Python zu sammeln.

Die Darvas Box Breakout-Strategie definiert eine Box mit einer Reihe von Ablehnungskerzen nach einem Hoch oder Tief und löst einen Handel aus, wenn der Preis aus dieser Spanne ausbricht. In jedem Fall brauchen wir ein Signal, um mit der Sammlung von Merkmalsdaten und der Vorhersage künftiger Ergebnisse zu beginnen. Als Auslöser setzen wir also den Moment, in dem der Kurs aus der unteren oder oberen Bereich ausbricht. Diese Funktion erkennt, ob es eine Darvas-Box für einen bestimmten Rückblickzeitraum und eine bestimmte Anzahl von Bestätigungskerzen gibt, weist den Variablen den Wert der Hoch-/Tiefspanne zu und stellt die Box auf dem Chart dar.

double high;
double low;
bool boxFormed = false;

bool DetectDarvasBox(int n = 100, int M = 3)
{
   // Clear previous Darvas box objects
   for (int k = ObjectsTotal(0, 0, -1) - 1; k >= 0; k--)
   {
      string name = ObjectName(0, k);
      if (StringFind(name, "DarvasBox_") == 0)
         ObjectDelete(0, name);
   }
   bool current_box_active = false;
   // Start checking from the oldest bar within the lookback period
   for (int i = M+1; i <= n; i++)
   {
      // Get high of current bar and previous bar
      double high_current = iHigh(_Symbol, PERIOD_CURRENT, i);
      double high_prev = iHigh(_Symbol, PERIOD_CURRENT, i + 1);
      // Check for a new high
      if (high_current > high_prev)
      {
         // Check if the next M bars do not exceed the high
         bool pullback = true;
         for (int k = 1; k <= M; k++)
         {
            if (i - k < 0) // Ensure we don't go beyond available bars
            {
               pullback = false;
               break;
            }
            double high_next = iHigh(_Symbol, PERIOD_CURRENT, i - k);
            if (high_next > high_current)
            {
               pullback = false;
               break;
            }
         }

         // If pullback condition is met, define the box
         if (pullback)
         {
            double top = high_current;
            double bottom = iLow(_Symbol, PERIOD_CURRENT, i);

            // Find the lowest low over the bar and the next M bars
            for (int k = 1; k <= M; k++)
            {
               double low_next = iLow(_Symbol, PERIOD_CURRENT, i - k);
               if (low_next < bottom)
                  bottom = low_next;
            }

            // Check for breakout from i - M - 1 to the current bar (index 0)
            int j = i - M - 1;
            while (j >= 0)
            {
               double close_j = iClose(_Symbol, PERIOD_CURRENT, j);
               if (close_j > top || close_j < bottom)
                  break; // Breakout found
               j--;
            }
            j++; // Adjust to the bar after breakout (or 0 if no breakout)

            // Create a unique object name
            string obj_name = "DarvasBox_" + IntegerToString(i);

            // Plot the box
            datetime time_start = iTime(_Symbol, PERIOD_CURRENT, i);
            datetime time_end;
            if (j > 0)
            {
               // Historical box: ends at breakout
               time_end = iTime(_Symbol, PERIOD_CURRENT, j);
            }
            else
            {
               // Current box: extends to the current bar
               time_end = iTime(_Symbol, PERIOD_CURRENT, 0);
               current_box_active = true;
            }
            high = top;
            low = bottom;
            ObjectCreate(0, obj_name, OBJ_RECTANGLE, 0, time_start, top, time_end, bottom);
            ObjectSetInteger(0, obj_name, OBJPROP_COLOR, clrBlue);
            ObjectSetInteger(0, obj_name, OBJPROP_STYLE, STYLE_SOLID);
            ObjectSetInteger(0, obj_name, OBJPROP_WIDTH, 1);
            boxFormed = true;

            // Since we're only plotting the most recent box, break after finding it
            break;
         }
      }
   }

   return current_box_active;
}

Hier sehen Sie einige Beispiele für die Darvas-Box in einem Chart:

Darvas-Box

Im Vergleich zur Verwendung als Filter hat diese Methode jedoch Nachteile. Wir müssten ausgewogene Ergebnisse mit gleichen Wahrscheinlichkeiten vorhersagen, z. B. ob die nächsten 10 Balken höher oder niedriger ausfallen oder ob der Kurs zuerst 10 Pips nach oben oder unten geht. Ein weiterer Nachteil ist, dass wir den eingebauten Vorteil einer Backbone-Strategie verlieren - der Vorteil hängt ganz von der Vorhersagekraft des Modells ab. Positiv ist, dass wir nicht durch die Stichproben beschränkt sind, die eine Backbone-Strategie nur bei Auslösung liefert, was Ihnen eine größere anfängliche Stichprobengröße und ein größeres Potenzial bietet. Wir implementieren die Handelslogik in der Funktion onTick() wie folgt:

input int checkBar = 30;
input int lookBack = 100;
input int countMax = 10;

void OnTick()
  {
  int bars = iBars(_Symbol,PERIOD_CURRENT);

  if (barsTotal!= bars){
     barsTotal = bars;
     boxFormed = false;
     bool NotInPosition = true;
 
     lastClose = iClose(_Symbol, PERIOD_CURRENT, 1);
     lastlastClose = iClose(_Symbol,PERIOD_CURRENT,2);
     
     for(int i = 0; i<PositionsTotal(); i++){
         ulong pos = PositionGetTicket(i);
         string symboll = PositionGetSymbol(i);
         if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol)NotInPosition = false;}
            /*count++;
            if(count >=countMax ){
              trade.PositionClose(pos);  
              count = 0;}
            }}*/
     DetectDarvasBox(lookBack,checkBar);
     if (NotInPosition&&boxFormed&&((lastlastClose<high&&lastClose>high)||(lastClose<low&&lastlastClose>low)))executeBuy(); 
    }
 }

Bei dieser Strategie ist die Verwendung der gleichen Take-Profit- und Stop-Loss-Größe konsistenter als die Verfolgung der Ergebnisse der nächsten 10 Bars. Ersteres bindet unsere Vorhersage direkt an den endgültigen Gewinn, während letzteres die Unsicherheit durch schwankende Renditen in jeder 10-Bar-Periode erhöht. Es ist erwähnenswert, dass wir Take-Profit und Stop-Loss als Prozentsatz des Preises verwenden, was es anpassungsfähiger für verschiedene Vermögenswerte macht und besser geeignet ist für tendenzielle Vermögenswerte wie Gold oder Indizes. Leser können die Alternative testen, indem sie den kommentierten Code auskommentieren und Take Profit und Stop Loss aus der Buy-Funktion entfernen.

Für die zur Vorhersage der Ergebnisse verwendeten Merkmalsdaten wählte ich die letzten drei normalisierten Renditen, den normalisierten Abstand vom Höchst- und Tiefstwert der Spanne sowie einige gängige stationäre Indikatoren. Wir speichern diese Daten in einem Multi-Array, das dann mit der Klasse CFileCSV aus einer mitgelieferten Datei in eine CSV-Datei gespeichert wird. Stellen Sie sicher, dass alle Zeitrahmen und Symbole wie unten aufgeführt eingestellt sind, damit Sie problemlos zwischen den Zeitrahmen und Assets wechseln können.
string data[50000][12];
int indexx = 0;

void getData(){
double close = iClose(_Symbol,PERIOD_CURRENT,1);
double close2 = iClose(_Symbol,PERIOD_CURRENT,2);
double close3 = iClose(_Symbol,PERIOD_CURRENT,3);
double stationary = 1000*(close-iOpen(_Symbol,PERIOD_CURRENT,1))/close;
double stationary2 = 1000*(close2-iOpen(_Symbol,PERIOD_CURRENT,2))/close2;
double stationary3 = 1000*(close3-iOpen(_Symbol,PERIOD_CURRENT,3))/close3;
double highDistance = 1000*(close-high)/close;
double lowDistance = 1000*(close-low)/close;
double boxSize = 1000*(high-low)/close;
double adx[];       // Average Directional Movement Index
double wilder[];    // Average Directional Movement Index by Welles Wilder
double dm[];        // DeMarker
double rsi[];       // Relative Strength Index
double rvi[];       // Relative Vigor Index
double sto[];       // Stochastic Oscillator


CopyBuffer(handleAdx, 0, 1, 1, adx);         // Average Directional Movement Index
CopyBuffer(handleWilder, 0, 1, 1, wilder);   // Average Directional Movement Index by Welles Wilder
CopyBuffer(handleDm, 0, 1, 1, dm);           // DeMarker
CopyBuffer(handleRsi, 0, 1, 1, rsi);         // Relative Strength Index
CopyBuffer(handleRvi, 0, 1, 1, rvi);         // Relative Vigor Index
CopyBuffer(handleSto, 0, 1, 1, sto);         // Stochastic Oscillator

//2 means 2 decimal places
data[indexx][0] = DoubleToString(adx[0], 2);      // Average Directional Movement Index
data[indexx][1] = DoubleToString(wilder[0], 2);   // Average Directional Movement Index by Welles Wilder
data[indexx][2] = DoubleToString(dm[0], 2);       // DeMarker
data[indexx][3] = DoubleToString(rsi[0], 2);     // Relative Strength Index
data[indexx][4] = DoubleToString(rvi[0], 2);     // Relative Vigor Index
data[indexx][5] = DoubleToString(sto[0], 2);     // Stochastic Oscillator
data[indexx][6] = DoubleToString(stationary,2);
data[indexx][7] = DoubleToString(boxSize,2);
data[indexx][8] = DoubleToString(stationary2,2);
data[indexx][9] = DoubleToString(stationary3,2);
data[indexx][10] = DoubleToString(highDistance,2);
data[indexx][11] = DoubleToString(lowDistance,2);
indexx++;
}

Der endgültige Code für den Expertenberater, der die Daten abruft, sieht folgendermaßen aus:

#include <Trade/Trade.mqh>
CTrade trade;
#include <FileCSV.mqh>
CFileCSV csvFile;
string fileName = "box.csv";
string headers[] = {
    "Average Directional Movement Index", 
    "Average Directional Movement Index by Welles Wilder",  
    "DeMarker", 
    "Relative Strength Index", 
    "Relative Vigor Index", 
    "Stochastic Oscillator",
    "Stationary",
    "Box Size",
    "Stationary2",
    "Stationary3",
    "Distance High",
    "Distance Low"
};

input double lott = 0.01;
input int Magic = 0;
input int checkBar = 30;
input int lookBack = 100;
input int countMax = 10;
input double slp = 0.003;
input double tpp = 0.003;
input bool saveData = true;

string data[50000][12];
int indexx = 0;
int barsTotal = 0;
int count = 0;
double high;
double low;
bool boxFormed = false;
double lastClose;
double lastlastClose;

int handleAdx;     // Average Directional Movement Index - 3
int handleWilder;  // Average Directional Movement Index by Welles Wilder - 3
int handleDm;      // DeMarker - 1
int handleRsi;     // Relative Strength Index - 1
int handleRvi;     // Relative Vigor Index - 2
int handleSto;     // Stochastic Oscillator - 2

int OnInit()
  {
   trade.SetExpertMagicNumber(Magic);
handleAdx=iADX(_Symbol,PERIOD_CURRENT,14);//Average Directional Movement Index - 3
handleWilder=iADXWilder(_Symbol,PERIOD_CURRENT,14);//Average Directional Movement Index by Welles Wilder - 3
handleDm=iDeMarker(_Symbol,PERIOD_CURRENT,14);//DeMarker - 1
handleRsi=iRSI(_Symbol,PERIOD_CURRENT,14,PRICE_CLOSE);//Relative Strength Index - 1
handleRvi=iRVI(_Symbol,PERIOD_CURRENT,10);//Relative Vigor Index - 2
handleSto=iStochastic(_Symbol,PERIOD_CURRENT,5,3,3,MODE_SMA,STO_LOWHIGH);//Stochastic Oscillator - 2
   return(INIT_SUCCEEDED);
  }

void OnDeinit(const int reason)
  {
   if (!saveData) return;
   if(csvFile.Open(fileName, FILE_WRITE|FILE_ANSI))
     {
      //Write the header
      csvFile.WriteHeader(headers);
      //Write data rows
      csvFile.WriteLine(data);
      //Close the file
      csvFile.Close();
     }
   else
     {
      Print("File opening error!");
     }
  }

void OnTick()
  {
  int bars = iBars(_Symbol,PERIOD_CURRENT);

  if (barsTotal!= bars){
     barsTotal = bars;
     boxFormed = false;
     bool NotInPosition = true;
 
     lastClose = iClose(_Symbol, PERIOD_CURRENT, 1);
     lastlastClose = iClose(_Symbol,PERIOD_CURRENT,2);
     
     for(int i = 0; i<PositionsTotal(); i++){
         ulong pos = PositionGetTicket(i);
         string symboll = PositionGetSymbol(i);
         if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol)NotInPosition = false;}
            /*count++;
            if(count >=countMax ){
              trade.PositionClose(pos);  
              count = 0;}
            }}*/
     DetectDarvasBox(lookBack,checkBar);
     if (NotInPosition&&boxFormed&&((lastlastClose<high&&lastClose>high)||(lastClose<low&&lastlastClose>low)))executeBuy(); 
    }
 }

void executeBuy() {
       double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
       ask = NormalizeDouble(ask,_Digits);
       double sl = lastClose*(1-slp);
       double tp = lastClose*(1+tpp);
       trade.Buy(lott,_Symbol,ask,sl,tp);
       if(PositionsTotal()>0)getData();
}

bool DetectDarvasBox(int n = 100, int M = 3)
{
   // Clear previous Darvas box objects
   for (int k = ObjectsTotal(0, 0, -1) - 1; k >= 0; k--)
   {
      string name = ObjectName(0, k);
      if (StringFind(name, "DarvasBox_") == 0)
         ObjectDelete(0, name);
   }
   bool current_box_active = false;
   // Start checking from the oldest bar within the lookback period
   for (int i = M+1; i <= n; i++)
   {
      // Get high of current bar and previous bar
      double high_current = iHigh(_Symbol, PERIOD_CURRENT, i);
      double high_prev = iHigh(_Symbol, PERIOD_CURRENT, i + 1);
      // Check for a new high
      if (high_current > high_prev)
      {
         // Check if the next M bars do not exceed the high
         bool pullback = true;
         for (int k = 1; k <= M; k++)
         {
            if (i - k < 0) // Ensure we don't go beyond available bars
            {
               pullback = false;
               break;
            }
            double high_next = iHigh(_Symbol, PERIOD_CURRENT, i - k);
            if (high_next > high_current)
            {
               pullback = false;
               break;
            }
         }

         // If pullback condition is met, define the box
         if (pullback)
         {
            double top = high_current;
            double bottom = iLow(_Symbol, PERIOD_CURRENT, i);

            // Find the lowest low over the bar and the next M bars
            for (int k = 1; k <= M; k++)
            {
               double low_next = iLow(_Symbol, PERIOD_CURRENT, i - k);
               if (low_next < bottom)
                  bottom = low_next;
            }

            // Check for breakout from i - M - 1 to the current bar (index 0)
            int j = i - M - 1;
            while (j >= 0)
            {
               double close_j = iClose(_Symbol, PERIOD_CURRENT, j);
               if (close_j > top || close_j < bottom)
                  break; // Breakout found
               j--;
            }
            j++; // Adjust to the bar after breakout (or 0 if no breakout)

            // Create a unique object name
            string obj_name = "DarvasBox_" + IntegerToString(i);

            // Plot the box
            datetime time_start = iTime(_Symbol, PERIOD_CURRENT, i);
            datetime time_end;
            if (j > 0)
            {
               // Historical box: ends at breakout
               time_end = iTime(_Symbol, PERIOD_CURRENT, j);
            }
            else
            {
               // Current box: extends to the current bar
               time_end = iTime(_Symbol, PERIOD_CURRENT, 0);
               current_box_active = true;
            }
            high = top;
            low = bottom;
            ObjectCreate(0, obj_name, OBJ_RECTANGLE, 0, time_start, top, time_end, bottom);
            ObjectSetInteger(0, obj_name, OBJPROP_COLOR, clrBlue);
            ObjectSetInteger(0, obj_name, OBJPROP_STYLE, STYLE_SOLID);
            ObjectSetInteger(0, obj_name, OBJPROP_WIDTH, 1);
            boxFormed = true;

            // Since we're only plotting the most recent box, break after finding it
            break;
         }
      }
   }

   return current_box_active;
}

void getData(){
double close = iClose(_Symbol,PERIOD_CURRENT,1);
double close2 = iClose(_Symbol,PERIOD_CURRENT,2);
double close3 = iClose(_Symbol,PERIOD_CURRENT,3);
double stationary = 1000*(close-iOpen(_Symbol,PERIOD_CURRENT,1))/close;
double stationary2 = 1000*(close2-iOpen(_Symbol,PERIOD_CURRENT,2))/close2;
double stationary3 = 1000*(close3-iOpen(_Symbol,PERIOD_CURRENT,3))/close3;
double highDistance = 1000*(close-high)/close;
double lowDistance = 1000*(close-low)/close;
double boxSize = 1000*(high-low)/close;
double adx[];       // Average Directional Movement Index
double wilder[];    // Average Directional Movement Index by Welles Wilder
double dm[];        // DeMarker
double rsi[];       // Relative Strength Index
double rvi[];       // Relative Vigor Index
double sto[];       // Stochastic Oscillator

CopyBuffer(handleAdx, 0, 1, 1, adx);         // Average Directional Movement Index
CopyBuffer(handleWilder, 0, 1, 1, wilder);   // Average Directional Movement Index by Welles Wilder
CopyBuffer(handleDm, 0, 1, 1, dm);           // DeMarker
CopyBuffer(handleRsi, 0, 1, 1, rsi);         // Relative Strength Index
CopyBuffer(handleRvi, 0, 1, 1, rvi);         // Relative Vigor Index
CopyBuffer(handleSto, 0, 1, 1, sto);         // Stochastic Oscillator

//2 means 2 decimal places
data[indexx][0] = DoubleToString(adx[0], 2);      // Average Directional Movement Index
data[indexx][1] = DoubleToString(wilder[0], 2);   // Average Directional Movement Index by Welles Wilder
data[indexx][2] = DoubleToString(dm[0], 2);       // DeMarker
data[indexx][3] = DoubleToString(rsi[0], 2);     // Relative Strength Index
data[indexx][4] = DoubleToString(rvi[0], 2);     // Relative Vigor Index
data[indexx][5] = DoubleToString(sto[0], 2);     // Stochastic Oscillator
data[indexx][6] = DoubleToString(stationary,2);
data[indexx][7] = DoubleToString(boxSize,2);
data[indexx][8] = DoubleToString(stationary2,2);
data[indexx][9] = DoubleToString(stationary3,2);
data[indexx][10] = DoubleToString(highDistance,2);
data[indexx][11] = DoubleToString(lowDistance,2);
indexx++;
}

Wir beabsichtigen, diese Strategie auf dem 15-Minuten-Zeitrahmen des XAUUSD zu handeln, da der Vermögenswert eine solide Volatilität aufweist und 15 Minuten ein Gleichgewicht zwischen geringerem Rauschen und einer höheren Anzahl von Stichproben darstellen. Ein typischer Handel würde folgendermaßen aussehen:

Handelsbeispiel

Wir verwenden die Daten von 2020-2024 als Trainings- und Validierungsdaten und werden das Ergebnis später auf dem MetaTrader 5-Terminal für 2024-2025 testen. Nachdem wir diesen EA im Strategietester ausgeführt haben, wird die CSV-Datei bei der Deinitialisierung des EAs im Verzeichnis /Tester/Agent-sth000 gespeichert.

Wir klicken außerdem mit der rechten Maustaste, um den Backtest-Excel-Bericht wie folgt zu erhalten:

Excel-Bericht

Bitte notieren Sie sich die Zeilennummer der Zeile „Deals“, die wir später als Eingabe verwenden werden.

Zeile suchen

Danach trainieren wir unser Modell in Python.

Das Modell, das wir für diesen Artikel ausgewählt haben, ist ein entscheidungsbaumbasiertes Modell, das sich ideal für Klassifizierungsprobleme eignet, genau wie das Modell, das wir in diesem Artikel verwendet haben.

import pandas as pd

# Replace 'your_file.xlsx' with the path to your file
input_file = 'box.xlsx'

# Load the Excel file and skip the first {skiprows} rows
data1 = pd.read_excel(input_file, skiprows=4417)

# Select the 'profit' column (assumed to be 'Unnamed: 10') and filter rows as per your instructions
profit_data = data1["Profit"][1:-1] 
profit_data = profit_data[profit_data.index % 2 == 0]  # Filter for rows with odd indices
profit_data = profit_data.reset_index(drop=True)  # Reset index
# Convert to float, then apply the condition to set values to 1 if > 0, otherwise to 0
profit_data = pd.to_numeric(profit_data, errors='coerce').fillna(0)  # Convert to float, replacing NaN with 0
profit_data = profit_data.apply(lambda x: 1 if x > 0 else 0)  # Apply condition

# Load the CSV file with semicolon separator
file_path = 'box.csv'
data2 = pd.read_csv(file_path, sep=';')

# Drop rows with any missing or incomplete values
data2.dropna(inplace=True)

# Drop any duplicate rows if present
data2.drop_duplicates(inplace=True)

# Convert non-numeric columns to numerical format
for col in data2.columns:
    if data2[col].dtype == 'object':
        # Convert categorical to numerical using label encoding
        data2[col] = data2[col].astype('category').cat.codes

# Ensure all remaining columns are numeric and cleanly formatted for CatBoost
data2 = data2.apply(pd.to_numeric, errors='coerce')
data2.dropna(inplace=True)  # Drop any rows that might still contain NaNs after conversion

# Merge the two DataFrames on the index
merged_data = pd.merge(profit_data, data2, left_index=True, right_index=True, how='inner')

# Save the merged data to a new CSV file
output_csv_path = 'merged_data.csv'
merged_data.to_csv(output_csv_path)

print(f"Merged data saved to {output_csv_path}")
Wir verwenden diesen Code, um den Excel-Bericht zu kennzeichnen, indem wir den Handelsgeschäften mit positivem Gewinn eine 1 zuweisen und den Handelsgeschäften ohne Gewinn eine 0. Dann kombinieren wir sie mit den Merkmalsdaten aus der CSV-Datei des datenabrufenden EA. Beachten Sie, dass der Wert für „Skiprow“ mit der Zeilennummer von „Deals“ übereinstimmt.
import numpy as np
import pandas as pd
import warnings
warnings.filterwarnings("ignore")
data = pd.read_csv("merged_data.csv",index_col=0)

XX = data.drop(columns=['Profit'])
yy = data['Profit']
y = yy.values
X = XX.values
pd.DataFrame(X,y)

Als Nächstes weisen wir das Label-Array der y-Variablen und den Merkmalsdatenrahmen der X-Variablen zu.

import numpy as np
import pandas as pd
import warnings
import seaborn as sns
warnings.filterwarnings("ignore")
from sklearn.model_selection import train_test_split
import catboost as cb
from sklearn.metrics import roc_curve, roc_auc_score
import matplotlib.pyplot as plt

# Split data
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, shuffle=False)

# Identify categorical features
cat_feature_indices = [i for i, col in enumerate(XX.columns) if XX[col].dtype == 'object']

# Train CatBoost classifier
model = cb.CatBoostClassifier(   
    iterations=5000,             # Number of trees (similar to n_estimators)
    learning_rate=0.02,          # Learning rate
    depth=5,                    # Depth of each tree
    l2_leaf_reg=5,
    bagging_temperature=1,
    early_stopping_rounds=50,
    loss_function='Logloss',    # Use 'MultiClass' if it's a multi-class problem
    verbose=1000)
model.fit(X_train, y_train, cat_features=cat_feature_indices)

Dann teilen wir die Daten im Verhältnis 9:1 in Trainings- und Validierungsdatensätze auf und beginnen mit dem Training des Modells. Die Standardeinstellung für die Funktion „train-test split“ in sklearn beinhaltet Shuffling, was für Zeitreihendaten nicht ideal ist. Stellen Sie daher sicher, dass Sie shuffle=False in den Parametern einstellen. Es ist eine gute Idee, die Hyperparameter so zu verändern, dass je nach Stichprobengröße eine Über- oder Unteranpassung vermieden wird. Ich persönlich habe festgestellt, dass das Anhalten der Iteration bei 0,1 log Verlust gut funktioniert.

import numpy as np
from sklearn.metrics import roc_curve, roc_auc_score
import matplotlib.pyplot as plt

# Assuming you already have y_test, X_test, and model defined
# Predict probabilities
y_prob = model.predict_proba(X_test)[:, 1]  # Probability for positive class

# Compute ROC curve and AUC (for reference)
fpr, tpr, thresholds = roc_curve(y_test, y_prob)
auc_score = roc_auc_score(y_test, y_prob)
print(f"AUC Score: {auc_score:.2f}")

# Define confidence thresholds to test (e.g., 50%, 60%, 70%, etc.)
confidence_thresholds = np.arange(0.5, 1.0, 0.05)  # From 50% to 95% in steps of 5%
accuracies = []
coverage = []  # Fraction of samples classified at each threshold

for thresh in confidence_thresholds:
    # Classify only when probability is >= thresh (positive) or <= (1 - thresh) (negative)
    y_pred_confident = np.where(y_prob >= thresh, 1, np.where(y_prob <= (1 - thresh), 0, -1))
    
    # Filter out unclassified samples (where y_pred_confident == -1)
    mask = y_pred_confident != -1
    y_test_confident = y_test[mask]
    y_pred_confident = y_pred_confident[mask]
    
    # Calculate accuracy and coverage
    if len(y_test_confident) > 0:  # Avoid division by zero
        acc = np.mean(y_pred_confident == y_test_confident)
        cov = len(y_test_confident) / len(y_test)
    else:
        acc = 0
        cov = 0
    
    accuracies.append(acc)
    coverage.append(cov)

# Plot Accuracy vs Confidence Threshold
plt.figure(figsize=(10, 6))
plt.plot(confidence_thresholds, accuracies, marker='o', label='Accuracy', color='blue')
plt.plot(confidence_thresholds, coverage, marker='s', label='Coverage', color='green')
plt.xlabel('Confidence Threshold')
plt.ylabel('Metric Value')
plt.title('Accuracy and Coverage vs Confidence Threshold')
plt.legend(loc='best')
plt.grid(True)
plt.show()

# Also show the original ROC curve for reference
plt.figure(figsize=(8, 6))
plt.plot(fpr, tpr, label=f'ROC Curve (AUC = {auc_score:.2f})', color='blue')
plt.plot([0, 1], [0, 1], 'k--', label='Random Guess')
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Receiver Operating Characteristic (ROC) Curve')
plt.legend(loc='lower right')
plt.grid(True)
plt.show()

Anschließend werden die Ergebnisse visualisiert, um den Validierungstest zu überprüfen. Beim typischen Test-Ansatz von Training und Validierung hilft der Validierungsschritt bei der Auswahl der besten Hyperparameter und bewertet zunächst, ob das trainierte Modell Vorhersagekraft hat. Er dient als Puffer, bevor der letzte Test durchgeführt wird.

Genauigkeit der Konfidenzschwelle

AUC-Punktzahl

Hier liegt der AUC-Wert über 0,5, und die Genauigkeit verbessert sich, wenn wir die Konfidenzschwelle erhöhen, was normalerweise ein positives Zeichen ist. Wenn diese beiden Metriken nicht übereinstimmen, sollten Sie zunächst versuchen, die Hyperparameter anzupassen, bevor Sie das Modell ganz verwerfen.
# Feature importance
feature_importance = model.get_feature_importance()
importance_df = pd.DataFrame({
    'feature': XX.columns,
    'importance': feature_importance
}).sort_values('importance', ascending=False)
print("Feature Importances:")
print(importance_df)
plt.figure(figsize=(10, 6))
sns.barplot(x='importance', y='feature', data=importance_df)
plt.title(' Feature Importance')
plt.xlabel('Importance')
plt.ylabel('Feature')
x = 100/len(XX.columns)
plt.axvline(x,color = 'red', linestyle = '--')
plt.show()

Bedeutung des Merkmals

Dieser Codeblock zeichnet die Bedeutung des Merkmals und die Medianlinie auf. Es gibt viele Möglichkeiten, die Bedeutung von Merkmalen im Bereich des maschinellen Lernens zu definieren, z. B:

  1. Baum-basierte Bedeutung: Misst die Reduktion von Unreinheiten (z.B. Gini) in Entscheidungsbäumen oder Ensembles wie Random Forest und XGBoost.
  2. Bedeutung der Permutation: Bewertet den Leistungsabfall, wenn die Werte eines Merkmals gemischt werden.
  3. SHAP-Werte: Berechnet den Beitrag eines Merkmals zu Vorhersagen auf der Grundlage von Shapley-Werten.
  4. Größe des Koeffizienten: Verwendet den absoluten Wert der Koeffizienten in linearen Modellen.

In unserem Beispiel verwenden wir CatBoost, ein entscheidungsbaumbasiertes Modell. Die Merkmalsbedeutung zeigt an, wie viel Unordnung (Unreinheit) jedes Merkmal reduziert, wenn es zur Aufteilung des Entscheidungsbaums in den In-Sample-Daten verwendet wird. Es ist wichtig, sich darüber im Klaren zu sein, dass die Auswahl der wichtigsten Merkmale für die endgültige Zusammenstellung zwar oft die Effizienz des Modells steigern kann, aber aus diesen Gründen nicht immer die Vorhersagbarkeit verbessert:

  • Die Wichtigkeit von Merkmalen wird anhand der In-Sample-Daten berechnet, ohne dass die Out-of-Sample-Daten berücksichtigt werden.
  • Die Wichtigkeit eines Merkmals hängt von den anderen zu berücksichtigenden Merkmalen ab. Wenn die meisten der von Ihnen ausgewählten Merkmale nicht aussagekräftig genug sind, hilft es nicht, die schwächsten zu streichen.
  • Die Wichtigkeit spiegelt wider, wie effektiv ein Merkmal den Baum aufteilt, und nicht unbedingt, wie entscheidend es für das endgültige Entscheidungsergebnis ist.

Diese Erkenntnisse trafen mich, als ich unerwartet entdeckte, dass die Auswahl der am wenigsten wichtigen Merkmale tatsächlich die Genauigkeit außerhalb der Stichprobe erhöhte. Aber im Allgemeinen trägt die Auswahl der wichtigsten Merkmale und die Kürzung der weniger wichtigen dazu bei, das Modell zu vereinfachen und wahrscheinlich die Genauigkeit insgesamt zu verbessern.

from onnx.helper import get_attribute_value
import onnxruntime as rt
from skl2onnx import convert_sklearn, update_registered_converter
from skl2onnx.common.shape_calculator import (
    calculate_linear_classifier_output_shapes,
)  # noqa
from skl2onnx.common.data_types import (
    FloatTensorType,
    Int64TensorType,
    guess_tensor_type,
)
from skl2onnx._parse import _apply_zipmap, _get_sklearn_operator_name
from catboost import CatBoostClassifier
from catboost.utils import convert_to_onnx_object

def skl2onnx_parser_castboost_classifier(scope, model, inputs, custom_parsers=None):
    
    options = scope.get_options(model, dict(zipmap=True))
    no_zipmap = isinstance(options["zipmap"], bool) and not options["zipmap"]

    alias = _get_sklearn_operator_name(type(model))
    this_operator = scope.declare_local_operator(alias, model)
    this_operator.inputs = inputs

    label_variable = scope.declare_local_variable("label", Int64TensorType())
    prob_dtype = guess_tensor_type(inputs[0].type)
    probability_tensor_variable = scope.declare_local_variable(
        "probabilities", prob_dtype
    )
    this_operator.outputs.append(label_variable)
    this_operator.outputs.append(probability_tensor_variable)
    probability_tensor = this_operator.outputs

    if no_zipmap:
        return probability_tensor

    return _apply_zipmap(
        options["zipmap"], scope, model, inputs[0].type, probability_tensor
    )

def skl2onnx_convert_catboost(scope, operator, container):
    """
    CatBoost returns an ONNX graph with a single node.
    This function adds it to the main graph.
    """
    onx = convert_to_onnx_object(operator.raw_operator)
    opsets = {d.domain: d.version for d in onx.opset_import}
    if "" in opsets and opsets[""] >= container.target_opset:
        raise RuntimeError("CatBoost uses an opset more recent than the target one.")
    if len(onx.graph.initializer) > 0 or len(onx.graph.sparse_initializer) > 0:
        raise NotImplementedError(
            "CatBoost returns a model initializers. This option is not implemented yet."
        )
    if (
        len(onx.graph.node) not in (1, 2)
        or not onx.graph.node[0].op_type.startswith("TreeEnsemble")
        or (len(onx.graph.node) == 2 and onx.graph.node[1].op_type != "ZipMap")
    ):
        types = ", ".join(map(lambda n: n.op_type, onx.graph.node))
        raise NotImplementedError(
            f"CatBoost returns {len(onx.graph.node)} != 1 (types={types}). "
            f"This option is not implemented yet."
        )
    node = onx.graph.node[0]
    atts = {}
    for att in node.attribute:
        atts[att.name] = get_attribute_value(att)
    container.add_node(
        node.op_type,
        [operator.inputs[0].full_name],
        [operator.outputs[0].full_name, operator.outputs[1].full_name],
        op_domain=node.domain,
        op_version=opsets.get(node.domain, None),
        **atts,
    )

update_registered_converter(
    CatBoostClassifier,
    "CatBoostCatBoostClassifier",
    calculate_linear_classifier_output_shapes,
    skl2onnx_convert_catboost,
    parser=skl2onnx_parser_castboost_classifier,
    options={"nocl": [True, False], "zipmap": [True, False, "columns"]},
)
model_onnx = convert_sklearn(
    model,
    "catboost",
    [("input", FloatTensorType([None, X.shape[1]]))],
    target_opset={"": 12, "ai.onnx.ml": 2},
)

# And save.
with open("box2024.onnx", "wb") as f:
    f.write(model_onnx.SerializeToString())

Schließlich exportieren wir die ONNX-Datei und speichern sie im Verzeichnis MQL5/Files.

Kehren wir nun zum MetaTrader 5 Code-Editor zurück, um den Trading EA zu erstellen.

Wir müssen nur den ursprünglichen Datenabruf-EA anpassen, indem wir einige Include-Dateien einbinden, um das CatBoost-Modell zu verarbeiten.

#resource "\\Files\\box2024.onnx" as uchar catboost_onnx[]
#include <CatOnnx.mqh>
CCatBoost cat_boost;
string data[1][12];
vector xx;
vector prob;

Dann passen wir die Funktion getData() so an, dass sie einen Vektor zurückgibt.

vector getData(){
double close = iClose(_Symbol,PERIOD_CURRENT,1);
double close2 = iClose(_Symbol,PERIOD_CURRENT,2);
double close3 = iClose(_Symbol,PERIOD_CURRENT,3);
double stationary = 1000*(close-iOpen(_Symbol,PERIOD_CURRENT,1))/close;
double stationary2 = 1000*(close2-iOpen(_Symbol,PERIOD_CURRENT,2))/close2;
double stationary3 = 1000*(close3-iOpen(_Symbol,PERIOD_CURRENT,3))/close3;
double highDistance = 1000*(close-high)/close;
double lowDistance = 1000*(close-low)/close;
double boxSize = 1000*(high-low)/close;
double adx[];       // Average Directional Movement Index
double wilder[];    // Average Directional Movement Index by Welles Wilder
double dm[];        // DeMarker
double rsi[];       // Relative Strength Index
double rvi[];       // Relative Vigor Index
double sto[];       // Stochastic Oscillator

CopyBuffer(handleAdx, 0, 1, 1, adx);         // Average Directional Movement Index
CopyBuffer(handleWilder, 0, 1, 1, wilder);   // Average Directional Movement Index by Welles Wilder
CopyBuffer(handleDm, 0, 1, 1, dm);           // DeMarker
CopyBuffer(handleRsi, 0, 1, 1, rsi);         // Relative Strength Index
CopyBuffer(handleRvi, 0, 1, 1, rvi);         // Relative Vigor Index
CopyBuffer(handleSto, 0, 1, 1, sto);         // Stochastic Oscillator

data[0][0] = DoubleToString(adx[0], 2);      // Average Directional Movement Index
data[0][1] = DoubleToString(wilder[0], 2);   // Average Directional Movement Index by Welles Wilder
data[0][2] = DoubleToString(dm[0], 2);       // DeMarker
data[0][3] = DoubleToString(rsi[0], 2);     // Relative Strength Index
data[0][4] = DoubleToString(rvi[0], 2);     // Relative Vigor Index
data[0][5] = DoubleToString(sto[0], 2);     // Stochastic Oscillator
data[0][6] = DoubleToString(stationary,2);
data[0][7] = DoubleToString(boxSize,2);
data[0][8] = DoubleToString(stationary2,2);
data[0][9] = DoubleToString(stationary3,2);
data[0][10] = DoubleToString(highDistance,2);
data[0][11] = DoubleToString(lowDistance,2);

vector features(12);    
   for(int i = 0; i < 12; i++)
    {
      features[i] = StringToDouble(data[0][i]);
    }
    return features;
}

Die endgültige Handelslogik in der Funktion OnTick() sieht dann so aus:

void OnTick()
  {
  int bars = iBars(_Symbol,PERIOD_CURRENT);

  if (barsTotal!= bars){
     barsTotal = bars;
     boxFormed = false;
     bool NotInPosition = true;
 
     lastClose = iClose(_Symbol, PERIOD_CURRENT, 1);
     lastlastClose = iClose(_Symbol,PERIOD_CURRENT,2);
     
     for(int i = 0; i<PositionsTotal(); i++){
         ulong pos = PositionGetTicket(i);
         string symboll = PositionGetSymbol(i);
         if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol)NotInPosition = false;}
            /*count++;
            if(count >=countMax){
              trade.PositionClose(pos);  
              count = 0;}
            }}*/
     DetectDarvasBox(lookBack,checkBar);
     if (NotInPosition&&boxFormed&&((lastlastClose<high&&lastClose>high)||(lastClose<low&&lastlastClose>low))){
        xx = getData();
        prob = cat_boost.predict_proba(xx);
        if(prob[1]>threshold)executeBuy(); 
        if(prob[0]>threshold)executeSell();
        }
    }
 }

In der Signallogik wird zunächst geprüft, ob derzeit keine Position offen ist, um zu gewährleisten, dass jeweils nur ein Handel stattfindet. Dann wird festgestellt, ob es einen Ausbruch auf beiden Seiten des Bereichs gibt. Danach ruft es die Funktion getData() auf, um den Merkmalsvektor zu erhalten. Dieser Vektor wird dem CatBoost-Modell als Eingabe übergeben, und das Modell gibt die Vorhersagekonfidenz für jedes Ergebnis in das prob-Array aus. Auf der Grundlage der Konfidenzniveaus für jedes Ergebnis platzieren wir einen Handel, der auf das vorhergesagte Ergebnis wettet. Im Wesentlichen verwenden wir das Modell, um die Kauf- oder Verkaufssignale zu generieren.

Wir führten einen Backtest auf dem MetaTrader 5-Strategietester durch, bei dem wir In-Sample-Daten von 2020 bis 2024 verwendeten, um zu überprüfen, dass unsere Trainingsdaten keine Fehler aufwiesen und dass die Zusammenführung von Merkmalen und Ergebnissen korrekt war. Wenn alles korrekt ist, sollte die Kapitalkurve nahezu perfekt aussehen, etwa so:

in-sample

Anschließend führen wir einen Backtest des Out-of-Sample-Tests von 2024 bis 2025 durch, um festzustellen, ob die Strategie im letzten Zeitraum rentabel war. Wir setzen den Schwellenwert auf 0,7, sodass das Modell nur dann einen Handel in einer Richtung tätigt, wenn das Konfidenzniveau für das Erreichen des Take-Profits in dieser Richtung auf der Grundlage der Trainingsdaten 70 % oder mehr beträgt.

Backtest-Einstellung (diskret)

Parameter

Kapitalkurve (diskret)

Ergebnis (diskret)

Es ist zu erkennen, dass das Modell in der ersten Jahreshälfte außerordentlich gut, im weiteren Verlauf jedoch nicht mehr so gut abschnitt. Dies ist bei Modellen des maschinellen Lernens häufig der Fall, da der aus früheren Daten gewonnene Vorteil oft nur vorübergehend ist und mit der Zeit abnimmt. Dies deutet darauf hin, dass ein kleineres Verhältnis von Test-Train für künftige Live-Trading-Implementierungen besser geeignet sein könnte. Insgesamt zeigt das Modell eine gewisse Vorhersagbarkeit, da es auch nach Berücksichtigung der Handelskosten profitabel blieb.


Kontinuierliches Signal

Beim algorithmischen Handel halten sich die Händler in der Regel an eine einfache Methode, bei der sie diskrete Signale verwenden - entweder kaufen oder verkaufen mit einem festen Risiko pro Handel. Das macht die Sache überschaubar und erleichtert die Analyse der Strategieleistung. Einige Händler haben versucht, diese diskrete Signalmethode zu verfeinern, indem sie ein additives Signal verwenden, bei dem sie das Risiko des Handels auf der Grundlage der Erfüllung der Bedingungen der Strategie anpassen. Kontinuierliche Signale führen diesen additiven Ansatz weiter, indem sie ihn auf abstraktere Strategiebedingungen anwenden und ein Risikoniveau zwischen null und eins erzeugen.

Der Grundgedanke dahinter ist, dass nicht alle Handelsgeschäfte, die die Einstiegskriterien erfüllen, gleich sind. Einige scheinen erfolgreicher zu sein, weil ihre Signale stärker sind und auf nicht-linearen Regeln beruhen, die mit der Strategie verbunden sind. Dies kann als ein Instrument des Risikomanagements angesehen werden - setzen Sie mehr, wenn das Vertrauen groß ist, und schränken Sie es ein, wenn ein Handel weniger vielversprechend erscheint, selbst wenn er immer noch eine positive erwartete Rendite hat. Wir müssen jedoch bedenken, dass dies einen weiteren Faktor für die Leistung der Strategie darstellt und dass die Risiken der Vorausschau und der Überanpassung immer noch problematisch sind, wenn wir bei der Umsetzung nicht vorsichtig sind.

Um dieses Konzept in unserem Handels-EA anzuwenden, müssen wir zunächst die Kauf-/Verkaufsfunktionen so anpassen, dass die Losgröße auf der Grundlage des Risikos berechnet wird, das wir bereit sind zu verlieren, wenn der Stop-Loss erreicht wird. Die Funktion der Losgrößenberechnung sieht folgendermaßen aus:  

double calclots(double slpoints, string symbol, double risk)
{  
   double balance = AccountInfoDouble(ACCOUNT_BALANCE);
   double riskAmount = balance* risk / 100;

   double ticksize = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);
   double tickvalue = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE);
   double lotstep = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);

   double moneyperlotstep = slpoints / ticksize * tickvalue * lotstep;
   double lots = MathFloor(riskAmount / moneyperlotstep) * lotstep;
   lots = MathMin(lots, SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX));
   lots = MathMax(lots, SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN));
   return lots;
}
Anschließend aktualisieren wir die Kauf-/Verkaufsfunktionen so, dass sie diese Funktion calclots() aufrufen und den Risikomultiplikator als Eingabe verwenden:  
void executeSell(double riskMultiplier) {      
       double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
       bid = NormalizeDouble(bid,_Digits);
       double sl = lastClose*(1+slp);
       double tp = lastClose*(1-tpp);
       double lots = 0.1;
       lots = calclots(slp*lastClose,_Symbol,risks*riskMultiplier);
       trade.Sell(lots,_Symbol,bid,sl,tp);  
       }

void executeBuy(double riskMultiplier) {
       double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
       ask = NormalizeDouble(ask,_Digits);
       double sl = lastClose*(1-slp);
       double tp = lastClose*(1+tpp);
       double lots = 0.1;
       lots = calclots(slp*lastClose,_Symbol,risks*riskMultiplier);
       trade.Buy(lots,_Symbol,ask,sl,tp);
}
Da unser maschinelles Lernmodell bereits das Konfidenzniveau ausgibt, können wir es direkt als Eingabe für den Risikomultiplikator verwenden. Wenn wir den Einfluss des Konfidenzniveaus auf das Risiko für jeden Handel anpassen wollen, können wir das Konfidenzniveau einfach nach oben oder unten skalieren.
if(prob[1]>threshold)executeBuy(prob[1]); 
if(prob[0]>threshold)executeSell(prob[0]);

Wenn wir zum Beispiel die Signifikanz der Konfidenzniveauunterschiede verstärken wollen, können wir die Wahrscheinlichkeit dreimal mit sich selbst multiplizieren. Dadurch würde sich die Verhältnisdifferenz zwischen den Wahrscheinlichkeiten erhöhen, wodurch die Auswirkungen der Konfidenzniveaus stärker zum Tragen kämen.

if(prob[1]>threshold)executeBuy(prob[1]*prob[1]*prob[1]); 
if(prob[0]>threshold)executeSell(prob[0]*prob[0]*prob[0]);

Jetzt versuchen wir, das Ergebnis im Backtest zu sehen. 

Backtest-Einstellung (kontinuierlich)

Aktienkurve (kontinuierlich)

Ergebnis (kontinuierlich)

Die eingegangenen Handelsgeschäfte sind immer noch dieselben wie bei der Version mit diskreten Signalen, aber der Gewinnfaktor und das Sharpe-Ratio haben sich leicht verbessert. Dies deutet darauf hin, dass das kontinuierliche Signal in diesem speziellen Szenario die Gesamtleistung des Out-of-Sample-Tests verbessert hat, der frei von Verzerrungen durch Vorausschau ist, da wir nur einmal getestet haben. Es ist jedoch zu beachten, dass dieser Ansatz die Methode mit festem Risiko nur dann übertrifft, wenn die Vorhersagegenauigkeit des Modells höher ist, wenn sein Konfidenzniveau höher ist. Andernfalls könnte der ursprüngliche Ansatz mit festem Risiko besser sein. Da wir die durchschnittliche Losgröße durch die Anwendung von Risikomultiplikatoren zwischen null und eins reduziert haben, müssen wir außerdem den Wert der Risikovariablen erhöhen, wenn wir einen ähnlichen Gesamtgewinn wie zuvor erzielen wollen.


Validierung in anderen Zeitrahmen

Das Training separater maschineller Lernmodelle, die jeweils einen anderen Zeitrahmen von Merkmalen zur Vorhersage desselben Ergebnisses verwenden, kann eine leistungsstarke Methode zur Verbesserung der Handelsfilterung und Signalgenerierung darstellen. Indem Sie ein Modell auf kurzfristige Daten, ein anderes auf mittelfristige und vielleicht ein drittes auf langfristige Trends konzentrieren, gewinnen Sie spezialisierte Erkenntnisse, die, wenn sie kombiniert werden, Vorhersagen zuverlässiger bestätigen können als ein einzelnes Modell. Dieser Multi-Modell-Ansatz kann das Vertrauen in Handelsentscheidungen stärken, indem er Signale gegeneinander abgleicht und das Risiko reduziert, aufgrund von Rauschen in einem bestimmten Zeitrahmen zu handeln.

Auf der anderen Seite kann diese Strategie das System verkomplizieren, insbesondere wenn Sie den Vorhersagen mehrerer Modelle unterschiedliche Gewichtungen zuweisen. Dies kann zu eigenen Verzerrungen oder Fehlern führen, wenn es nicht sorgfältig abgestimmt wird. Jedes Modell könnte auch zu sehr auf seinen spezifischen Zeitrahmen zugeschnitten sein und die breitere Marktdynamik übersehen, und Diskrepanzen zwischen ihren Vorhersagen könnten zu Verwirrung führen, Entscheidungen verzögern oder das Vertrauen untergraben. 

Dieser Ansatz beruht auf zwei Grundannahmen: Im höheren Zeitrahmen gibt es keine Verzerrung durch die Vorausschau (wir müssen den Wert des letzten Balkens verwenden, nicht den aktuellen), und das Modell des höheren Zeitrahmens hat seine eigene Vorhersagbarkeit (es schneidet besser ab als das zufällige Raten bei Out-of-Sample-Tests).  

Um dies umzusetzen, ändern wir zunächst den Code im Datenabruf-EA, indem wir alle Zeitrahmen, die sich auf die Merkmalsextraktion beziehen, auf einen höheren Zeitrahmen, z. B. 1 Stunde, ändern. Dazu gehören Indikatoren, Preisberechnungen und alle anderen verwendeten Funktionen.

int OnInit()
{
   trade.SetExpertMagicNumber(Magic);
   handleAdx = iADX(_Symbol, PERIOD_H1, 14); // Average Directional Movement Index - 3
   handleWilder = iADXWilder(_Symbol, PERIOD_H1, 14); // Average Directional Movement Index by Welles Wilder - 3
   handleDm = iDeMarker(_Symbol, PERIOD_H1, 14); // DeMarker - 1
   handleRsi = iRSI(_Symbol, PERIOD_H1, 14, PRICE_CLOSE); // Relative Strength Index - 1
   handleRvi = iRVI(_Symbol, PERIOD_H1, 10); // Relative Vigor Index - 2
   handleSto = iStochastic(_Symbol, PERIOD_H1, 5, 3, 3, MODE_SMA, STO_LOWHIGH); // Stochastic Oscillator - 2
   return(INIT_SUCCEEDED);
}

void getData()
{
   double close = iClose(_Symbol, PERIOD_H1, 1);
   double close2 = iClose(_Symbol, PERIOD_H1, 2);
   double close3 = iClose(_Symbol, PERIOD_H1, 3);
   double stationary = 1000 * (close - iOpen(_Symbol, PERIOD_H1, 1)) / close;
   double stationary2 = 1000 * (close2 - iOpen(_Symbol, PERIOD_H1, 2)) / close2;
   double stationary3 = 1000 * (close3 - iOpen(_Symbol, PERIOD_H1, 3)) / close3;
}

Danach folgen die gleichen Schritte wie zuvor: Abrufen der Daten, Trainieren des Modells und Exportieren des Modells, genau wie zuvor beschrieben.

Dann, in den Handel EA, machen wir eine zweite Funktion für die Abholung der Features Eingabe, werden wir diese Funktion auf die zweite ML-Modell, das wir importiert, um die Konfidenzniveau Ausgabe zu erhalten Feed.

vector getData2()
{
   double close = iClose(_Symbol, PERIOD_H1, 1);
   double close2 = iClose(_Symbol, PERIOD_H1, 2);
   double close3 = iClose(_Symbol, PERIOD_H1, 3);
   double stationary = 1000 * (close - iOpen(_Symbol, PERIOD_H1, 1)) / close;
   double stationary2 = 1000 * (close2 - iOpen(_Symbol, PERIOD_H1, 2)) / close2;
   double stationary3 = 1000 * (close3 - iOpen(_Symbol, PERIOD_H1, 3)) / close3;
   double highDistance = 1000 * (close - high) / close;
   double lowDistance = 1000 * (close - low) / close;
   double boxSize = 1000 * (high - low) / close;
   double adx[];       // Average Directional Movement Index
   double wilder[];    // Average Directional Movement Index by Welles Wilder
   double dm[];        // DeMarker
   double rsi[];       // Relative Strength Index
   double rvi[];       // Relative Vigor Index
   double sto[];       // Stochastic Oscillator

   CopyBuffer(handleAdx, 0, 1, 1, adx);         // Average Directional Movement Index
   CopyBuffer(handleWilder, 0, 1, 1, wilder);   // Average Directional Movement Index by Welles Wilder
   CopyBuffer(handleDm, 0, 1, 1, dm);           // DeMarker
   CopyBuffer(handleRsi, 0, 1, 1, rsi);         // Relative Strength Index
   CopyBuffer(handleRvi, 0, 1, 1, rvi);         // Relative Vigor Index
   CopyBuffer(handleSto, 0, 1, 1, sto);         // Stochastic Oscillator

   data[0][0] = DoubleToString(adx[0], 2);      // Average Directional Movement Index
   data[0][1] = DoubleToString(wilder[0], 2);   // Average Directional Movement Index by Welles Wilder
   data[0][2] = DoubleToString(dm[0], 2);       // DeMarker
   data[0][3] = DoubleToString(rsi[0], 2);     // Relative Strength Index
   data[0][4] = DoubleToString(rvi[0], 2);     // Relative Vigor Index
   data[0][5] = DoubleToString(sto[0], 2);     // Stochastic Oscillator
   data[0][6] = DoubleToString(stationary, 2);
   data[0][7] = DoubleToString(boxSize, 2);
   data[0][8] = DoubleToString(stationary2, 2);
   data[0][9] = DoubleToString(stationary3, 2);
   data[0][10] = DoubleToString(highDistance, 2);
   data[0][11] = DoubleToString(lowDistance, 2);

   vector features(12);    
   for(int i = 0; i < 12; i++)
   {
      features[i] = StringToDouble(data[0][i]);
   }
   return features;
}

Angenommen, wir wollen den beiden Modellen die gleiche Gewichtung zuweisen, dann nehmen wir einfach den Durchschnitt ihrer Ergebnisse und betrachten ihn als das einzige Ergebnis, das wir zuvor verwendet haben.

if (NotInPosition&&boxFormed&&((lastlastClose<high&&lastClose>high)||(lastClose<low&&lastlastClose>low))){
        xx = getData();
        xx2 = getData2();
        prob = cat_boost.predict_proba(xx);
        prob2 = cat_boost.predict_proba(xx2);
        double probability_buy = (prob[1]+prob2[1])/2;
        double probability_sell = (prob[0]+prob2[0])/2;

        if(probability_buy>threshold)executeBuy(probability_buy); 
        if(probability_sell>threshold)executeSell(probability_sell);
        }
    }

Mit diesen beiden wie oben berechneten Variablen können wir sie nun zu einem einzigen Konfidenzniveau zusammenfassen und für die Validierung verwenden, wobei wir denselben Ansatz wie zuvor verwenden.


Schlussfolgerung

In diesem Artikel haben wir uns zunächst mit der Idee beschäftigt, ein maschinelles Lernmodell als Signalgenerator anstelle eines Filters zu verwenden, und dies anhand einer Darvas-Box-Breakout-Strategie demonstriert. Wir gingen kurz den Prozess der Modellschulung durch und erörterten die Bedeutung von Schwellenwerten für das Konfidenzniveau und die Signifikanz von Merkmalen. Als Nächstes haben wir das Konzept der kontinuierlichen Signale eingeführt und ihre Leistung mit der von diskreten Signalen verglichen. In diesem Beispiel haben wir festgestellt, dass kontinuierliche Signale die Backtest-Performance verbessern, da das Modell mit steigendem Konfidenzniveau tendenziell eine höhere Vorhersagegenauigkeit aufweist. Schließlich haben wir den Entwurf für die Verwendung mehrerer Modelle des maschinellen Lernens, die auf unterschiedlichen Zeitrahmen trainiert wurden, um Signale gemeinsam zu validieren, angesprochen.  

Insgesamt zielte dieser Artikel darauf ab, unkonventionelle Ideen zur Anwendung von maschinellen Lernmodellen im überwachten Lernen für den CTA-Handel vorzustellen. Ziel ist es nicht, eine endgültige Aussage darüber zu treffen, welcher Ansatz am besten funktioniert, da alles vom jeweiligen Szenario abhängt, sondern die Leser zu kreativem Denken anzuregen und einfache Ausgangskonzepte zu erweitern. Letztendlich ist nichts völlig neu - Innovation entsteht oft durch die Kombination bestehender Ideen, um etwas Neues zu schaffen.

Datei-Tabelle

Datei Name Dateiverwendung
Darvas_Box.ipynb Die Jupyter Notebook-Datei für das Training des ML-Modells
Darvas Box Data.mq5 Der EA zum Abrufen von Daten für das Modelltraining
Darvas Box EA.mq5 Der Handels-EA aus dem Artikel
CatOnnx.mqh Eine Include-Datei für die Verarbeitung des CatBoost-Modells
FileCSV.mqh Eine Include-Datei zum Speichern von Daten in CSV

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

Beigefügte Dateien |
Darvas_ML.zip (136.61 KB)
Letzte Kommentare | Zur Diskussion im Händlerforum (2)
linfo2
linfo2 | 24 März 2025 in 00:42
Danke Zhou für den interessanten Artikel und die Code-Beispiele . für mich musste ich einige der Python-Komponenten manuell installieren, damit es funktioniert. das kann anderen Benutzern helfen !pip install catboost!pip install onnxruntime !pip install skl2onnx. nach der Fertigstellung kann ich testen . aber wenn ich versuche, den zugehörigen EA zu laden, habe ich "Failed to set the Output[1] shape Err=5802. Ich bin mir nicht sicher, woher das kommt oder ob es wichtig ist, und ich bin nicht in der Lage herauszufinden, woher das kommt . . die Dokumentation sagt ERR_ONNX_NOT_SUPPORTED

5802

Eigenschaft oder Wert wird von MQL5 nicht unterstützt, es folgt die Meldung ONNX Model Initialised ? haben Sie irgendwelche Vorschläge?
Zhuo Kai Chen
Zhuo Kai Chen | 24 März 2025 in 01:09
linfo2 catboost!pip install onnxruntime !pip install skl2onnx. nach der Fertigstellung kann ich testen . aber wenn ich versuche, den zugehörigen EA zu laden, habe ich "Failed to set the Output[1] shape Err=5802. Ich bin mir nicht sicher, woher das kommt oder ob es wichtig ist, und ich bin nicht in der Lage herauszufinden, woher das kommt . . die Dokumentation sagt ERR_ONNX_NOT_SUPPORTED

5802

Eigenschaft oder Wert wird von MQL5 nicht unterstützt, es folgt die Meldung ONNX Model Initialised ? haben Sie irgendwelche Vorschläge?

Danke für den Hinweis. Der Pip-Installationsteil wurde ignoriert, aber die Benutzer müssen die zugehörige Bibliothek installieren, wenn sie es nicht schon getan haben.

Ihr Fehler kann dadurch verursacht werden, dass die Dimensionen, die Sie beim Training Ihres Modells verwendet haben, sich von denen unterscheiden, die Sie in Ihrem EA verwenden. Wenn Sie zum Beispiel ein Modell mit 5 Merkmalen trainiert haben, sollten Sie auch 5 Merkmale in Ihren EA eingeben, nicht 4 oder 6. Eine detailliertere Anleitung finden Sie in diesem Artikel. Hoffentlich hilft das. Wenn nicht, geben Sie bitte mehr Kontext an.

Erstellen von selbstoptimierenden Expert Advisor in MQL5 (Teil 6): Selbstanpassende Handelsregeln (II) Erstellen von selbstoptimierenden Expert Advisor in MQL5 (Teil 6): Selbstanpassende Handelsregeln (II)
Dieser Artikel befasst sich mit der Optimierung der RSI-Werte und -Perioden für bessere Handelssignale. Wir stellen Methoden zur Schätzung optimaler RSI-Werte vor und automatisieren die Periodenauswahl mithilfe von Rastersuche und statistischen Modellen. Schließlich implementieren wir die Lösung in MQL5 und setzen Python für die Analyse ein. Unser Ansatz ist pragmatisch und geradlinig, um Ihnen zu helfen, potenziell komplizierte Probleme auf einfache Weise zu lösen.
MQL5-Assistent-Techniken, die Sie kennen sollten (Teil 57): Überwachtes Lernen mit gleitendem Durchschnitt und dem stochastischen Oszillator MQL5-Assistent-Techniken, die Sie kennen sollten (Teil 57): Überwachtes Lernen mit gleitendem Durchschnitt und dem stochastischen Oszillator
Der gleitende Durchschnitt und der Stochastik-Oszillator sind sehr gängige Indikatoren, die von manchen Händlern aufgrund ihres verzögerten Charakters nicht oft verwendet werden. In einer dreiteiligen Miniserie, die sich mit den drei wichtigsten Formen des maschinellen Lernens befasst, gehen wir der Frage nach, ob die Voreingenommenheit gegenüber diesen Indikatoren gerechtfertigt ist, oder ob sie vielleicht einen Vorteil haben. Wir führen unsere Untersuchung mit Hilfe eines Assistenten durch, der Expert Advisors zusammenstellt.
Einführung in MQL5 (Teil 14): Ein Anfängerleitfaden zur Erstellung nutzerdefinierter Indikatoren (III) Einführung in MQL5 (Teil 14): Ein Anfängerleitfaden zur Erstellung nutzerdefinierter Indikatoren (III)
Lernen Sie, einen Harmonic Pattern Indikator in MQL5 unter Verwendung von Chart-Objekten zu erstellen. Entdecken Sie, wie Sie Umkehrpunkte erkennen, Fibonacci-Retracements anwenden und die Mustererkennung automatisieren können.
Automatisieren von Handelsstrategien in MQL5 (Teil 14): Stapelstrategie für den Handel mit statistischen MACD-RSI-Methoden Automatisieren von Handelsstrategien in MQL5 (Teil 14): Stapelstrategie für den Handel mit statistischen MACD-RSI-Methoden
In diesem Artikel stellen wir die Stapelstrategie des Handels (Trading-Layering) vor, die MACD- und RSI-Indikatoren mit statistischen Methoden kombiniert, um den dynamischen Handel in MQL5 zu automatisieren. Wir untersuchen die Architektur dieses kaskadierenden Ansatzes, erläutern seine Implementierung anhand wichtiger Codesegmente und geben dem Leser eine Anleitung für die Backtests, um die Leistung zu optimieren. Abschließend wird das Potenzial der Strategie hervorgehoben und die Voraussetzungen für weitere Verbesserungen im automatisierten Handel geschaffen.