English 日本語
preview
Dekodierung von Intraday-Handelsstrategien des Opening Range Breakout

Dekodierung von Intraday-Handelsstrategien des Opening Range Breakout

MetaTrader 5Handel |
126 11
Zhuo Kai Chen
Zhuo Kai Chen

Einführung

Die Strategien des Opening Range Breakout (ORB) basieren auf der Idee, dass die erste Handelsspanne, die sich kurz nach der Markteröffnung bildet, wichtige Preisniveaus widerspiegelt, bei denen sich Käufer und Verkäufer auf einen Wert einigen. Durch die Identifizierung von Ausbrüchen über oder unter einer bestimmten Spanne können Händler von der Dynamik profitieren, die oft folgt, wenn die Marktrichtung klarer wird. 

In diesem Artikel werden wir drei ORB-Strategien untersuchen, die aus den Papieren der Concretum Group übernommen wurden. Zunächst werden wir den Forschungshintergrund, einschließlich der Schlüsselkonzepte und der verwendeten Methodik, behandeln. Dann werden wir für jede Strategie erklären, wie sie funktioniert, ihre Signalregeln auflisten und ihre Leistung statistisch analysieren. Schließlich werden wir sie aus einer Portfolioperspektive betrachten und uns dabei auf das Thema Diversifizierung konzentrieren.  

Dieser Artikel geht nicht näher auf die Programmierung ein, sondern konzentriert sich stattdessen auf den Forschungsprozess, einschließlich der Nachbildung, Analyse und Prüfung der Strategien aus diesen drei Arbeiten. Dieses Buch ist für Leser geeignet, die auf der Suche nach potenziellen Handelsvorteilen sind oder sich dafür interessieren, wie diese Strategien untersucht und repliziert wurden. Dennoch wird der gesamte MQL5-Code für die EAs offengelegt werden. Die Leserinnen und Leser sind eingeladen, den Rahmen selbst zu erweitern.


Hintergrund der Forschung

Dieser Abschnitt behandelt die Forschungsmethodik, die wir zur Analyse der Strategien anwenden werden, sowie Schlüsselkonzepte, die später im Artikel auftauchen werden.  

Die Concretum Group ist eines der wenigen akademischen Forschungsteams, das Intraday-Handelsstrategien entwickelt. In den Studien, die wir übernehmen, konzentrieren sie sich auf Strategien, die zwischen Marktöffnung und -schluss (9:30 Uhr bis 16:00 Uhr Eastern Time) gehandelt werden. Da unser Broker UTC+2/3 verwendet, bedeutet dies 18:30-24:00 Serverzeit - stellen Sie sicher, dass Sie beim Testen die Zeitzone Ihres Brokers berücksichtigen.  

Die ursprüngliche Untersuchung handelt mit QQQ, einem ETF, der den Nasdaq-100-Index abbildet. Es ist wichtig zu wissen, dass der Nasdaq-100 die Performance von 100 großen, technologieorientierten Unternehmen an der Nasdaq-Börse darstellt. Der Index selbst ist eigentlich nicht handelbar, sondern nur seine Derivate. QQQ ermöglicht Anlegern ein Engagement in diesen Unternehmen über eine einzige Aktie. Für unsere Tests werden wir mit USTEC (einem CFD auf den Nasdaq-100) handeln, der die Spekulation auf Kursbewegungen ermöglicht, ohne die zugrunde liegenden Vermögenswerte zu besitzen, wobei häufig eine Hebelwirkung eingesetzt wird, um Gewinne oder Verluste zu vergrößern.

Wir werden in diesem Artikel zwei wichtige Kennzahlen vorstellen: Alpha und Beta. Im Handel steht Alpha für die Überschussrendite, die eine Anlage im Vergleich zu einer Benchmark wie einem Marktindex erzielt. Sie zeigt, ob die Investition die Erwartungen übertrifft und im Wesentlichen den Vorsprung widerspiegelt. Beta misst die Empfindlichkeit einer Anlage gegenüber Marktbewegungen. Ein Beta von 1 bedeutet, dass er die Schwankungen des Marktes widerspiegelt. Ein Wert über 1 deutet auf eine höhere Volatilität hin, ein Wert unter 1 auf eine geringere Volatilität. Diese Kennzahlen sind wichtig, um zu verstehen, inwieweit Ihre Strategie von Markttrends abhängt und nicht von Ihrem einzigartigen Vorteil. Dieses Wissen hilft Ihnen bei der Einschätzung potenzieller direktionaler Verzerrungen in Trendwerten wie Indizes oder Kryptowährungen.

Alpha und Beta werden wie folgt berechnet:

Alpha-Beta

Ri ist die Anlagerendite, Rf ist der risikofreie Zinssatz, der häufig auf der Rendite von Staatsanleihen basiert oder ignoriert wird, und Rm ist die Marktrendite. Kovarianz und Varianz werden in der Regel anhand von Tagesrenditen berechnet.

Ein Schlüsselindikator, der später in diesem Artikel verwendet wird, ist der volumengewichtete Durchschnittspreis (VWAP). Er wird berechnet als:

VWAP

Die Intuition hinter dem VWAP besteht darin, den durchschnittlichen Preis zu messen, zu dem ein Wertpapier gehandelt wird, gewichtet nach dem Volumen, was die „wahren“ Kosten des Handels über einen bestimmten Zeitraum widerspiegelt. Im Gegensatz zu einem einfachen Durchschnitt werden Kurse mit höherer Handelsaktivität stärker gewichtet, was sie zu einer faireren Benchmark macht.

Er wird häufig im algorithmischen Handel eingesetzt:

  • Er dient als Trendfilter.
  • Er dient als Trailing-Stopp.
  • Er dient als Signalgeber (z. B. Einstieg bei Überschreiten des VWAP).

In der Regel beginnen wir mit der Berechnung des VWAP ab der ersten Kerze bei Markteröffnung. In der obigen Gleichung steht Pi für den Kurs der i-ten Kerze, in der Regel der Schlusskurs, und Vi für das Handelsvolumen der i-ten Kerze. Das Handelsvolumen kann aufgrund unterschiedlicher Liquiditätsanbieter von CFD-Broker zu CFD-Broker variieren, aber die relative Gewichtung sollte im Allgemeinen bei allen Brokern gleich sein.

Dieser Artikel implementiert das Leverage-Space-Modell. Bei dieser Methode wird ein bestimmter Prozentsatz unseres Guthabens pro Handel riskiert, der ausgelöst wird, wenn der Stop-Loss erreicht wird. Der Stop-Loss-Bereich wird ein fester Prozentsatz des Vermögens sein, um sich an dessen Wertentwicklung und Volatilität anzupassen. Das Risiko pro Handel wird mit runden Zahlen festgelegt, um der Einfachheit halber einen maximalen Drawdown von etwa 20 % zu erreichen. Wir werden jede Strategie über einen Zeitraum von fünf Jahren, vom 1. Januar 2020 bis zum 1. Januar 2025, testen, um genügend aktuelle Daten zur Bewertung der aktuellen Rentabilität zu sammeln. Eine gründliche statistische Analyse wird Vergleiche mit „Kaufen und Halten“ auf der Grundlage kumulativer prozentualer Gewinne und individueller Leistungskennzahlen umfassen.


Strategie Eins: Richtung der Eröffnungskerze

Die erste Strategie, die wir uns ansehen werden, ist eine klassische Eröffnungsausbruchsstrategie, die in dem Papier Can Day Trading Really Be Profitable? von der Concretum Group vorgestellt wird. Die Motivation hinter den Regeln für die Strategiesignale liegt in der Erfassung kurzfristiger Preisdynamik bei gleichzeitiger Abwägung von Praktikabilität und Risikomanagement für Daytrader. Die Autoren wählten den ORB-Ansatz, um die erhöhte Volatilität und die Richtungsdynamik auszunutzen, die häufig bei der Markteröffnung zu beobachten sind. Dieser Zeitraum gilt als kritisches Zeitfenster, in dem institutionelle Anleger häufig aktiv sind, und Privatanleger können die Kursrichtung dieses Zeitraums als Anhaltspunkt für die Bestimmung des Trends für den gesamten Tag nutzen.

Nach Durchsicht des Papiers haben wir mehrere Möglichkeiten zur Verbesserung der ursprünglichen Strategie gefunden. Der ursprüngliche Ansatz verwendete das Hoch oder Tief der ersten Fünf-Minuten-Kerze als Stop-Loss und einen Take-Profit von 10R. Diese Methode war zwar profitabel, aber für Kleinhändler im Live-Handel unpraktisch. Der enge Stop-Loss der ersten Fünf-Minuten-Kerze erhöhte die relativen Handelskosten. Außerdem war der Take-Profit von 10R unnötig, da wir alle Handelsgeschäfte bis zum Ende des Tages schließen und er nur selten erreicht wurde. Schließlich fehlte der ursprünglichen Strategie ein Regimefilter, sodass das Hinzufügen eines gleitenden Durchschnitts die Strategie verbessern könnte, indem er als Regimefilter dient.

Unsere modifizierten Signalregeln lauten wie folgt:

  • Kaufe fünf Minuten nach Markteröffnung, wenn die fünfminütige Eröffnungskerze eine Aufwärtskerze ist und der Schlusskurs über dem gleitenden Durchschnitt der 350er-Periode liegt.
  • Verkaufe fünf Minuten nach Markteröffnung, wenn die fünfminütige Eröffnungskerze eine Aufwärtskerze ist und der Schlusskurs unter dem gleitenden Durchschnitt der 350er-Periode liegt.
  • Schließe fünf Minuten vor Börsenschluss die bestehende Positionen.
  • Stopp-Loss bei 1 % des Kurses vom Einstiegsniveau.
  • 2% Risiko pro Handel.

Vollständiger MQL5-Code für den EA:

//USTEC-M5
#include <Trade/Trade.mqh>
CTrade trade;

input int startHour = 18;
input int startMinute = 35;
input int endHour = 23;
input int endMinute = 55;
input double risk = 2.0;
input double slp = 0.01;
input int MaPeriods = 350;
input int Magic = 0;

int barsTotal = 0;
int handleMa;
double lastClose=0;
double lastOpen = 0;
double lot = 0.1;

//+------------------------------------------------------------------+
//|Initialization function                                           |
//+------------------------------------------------------------------+ 
int OnInit()
  {
   trade.SetExpertMagicNumber(Magic);
   handleMa = iMA(_Symbol,PERIOD_CURRENT,MaPeriods,0,MODE_SMA,PRICE_CLOSE);
   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
//|Deinitialization function                                         |
//+------------------------------------------------------------------+ 
void OnDeinit(const int reason)
  {
  }

//+------------------------------------------------------------------+
//|On tick function                                                  |
//+------------------------------------------------------------------+ 
void OnTick()
  {
   int bars = iBars(_Symbol, PERIOD_CURRENT);
   if(barsTotal != bars){
      barsTotal=bars;
      double ma[];
      CopyBuffer(handleMa,0,1,1,ma);
      
      if(MarketOpen()){
         lastClose = iClose(_Symbol,PERIOD_CURRENT,1);
         lastOpen = iOpen(_Symbol,PERIOD_CURRENT,1);
         if(lastClose<lastOpen&&lastClose<ma[0])executeSell();
         if (lastClose>lastOpen&&lastClose>ma[0]) executeBuy();
      }
      
      if(MarketClose()){
         for(int i = PositionsTotal()-1; i>=0; i--){
            ulong pos = PositionGetTicket(i);
            string symboll = PositionGetSymbol(i);
            if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol)trade.PositionClose(pos);
            }
       }  
   }
}

//+------------------------------------------------------------------+
//| Detect if market is opened                                       |
//+------------------------------------------------------------------+
bool MarketOpen()
{
    datetime currentTime = TimeTradeServer(); 
    MqlDateTime timeStruct;
    TimeToStruct(currentTime, timeStruct);
    int currentHour = timeStruct.hour;
    int currentMinute = timeStruct.min;
    if (currentHour == startHour &&currentMinute==startMinute)return true;
    else return false;
}

//+------------------------------------------------------------------+
//| Detect if market is closed                                       |
//+------------------------------------------------------------------+
bool MarketClose()
{
    datetime currentTime = TimeTradeServer(); 
    MqlDateTime timeStruct;
    TimeToStruct(currentTime, timeStruct);
    int currentHour = timeStruct.hour;
    int currentMinute = timeStruct.min;

    if (currentHour == endHour && currentMinute == endMinute)return true;
    else return false;
}

//+------------------------------------------------------------------+
//| Sell execution function                                          |
//+------------------------------------------------------------------+        
void executeSell() {      
       double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
       bid = NormalizeDouble(bid,_Digits);
       double sl = bid*(1+slp);
       sl = NormalizeDouble(sl, _Digits);
       lot = calclots(bid*slp);
       trade.Sell(lot,_Symbol,bid,sl);    
}
  
//+------------------------------------------------------------------+
//| Buy execution function                                           |
//+------------------------------------------------------------------+   
void executeBuy() {
       double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
       ask = NormalizeDouble(ask,_Digits);
       double sl = ask*(1-slp);
       sl = NormalizeDouble(sl, _Digits);
       lot = calclots(ask*slp);
       trade.Buy(lot,_Symbol,ask,sl);
}

//+------------------------------------------------------------------+
//| Calculate lot size based on risk and stop loss range             |
//+------------------------------------------------------------------+
double calclots(double slpoints) {
    double riskAmount = AccountInfoDouble(ACCOUNT_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;
}

Ein typischer Handel würde folgendermaßen aussehen:

orb1-Beispiel

Backtest-Ergebnisse:

orb1-Einstellungen

orb1-Parameter

orb1-Aktienkurve

orb1-Ergebnisse

Ohne den Filter des gleitenden Durchschnitts würden die ursprünglichen Strategieregeln einen Handel pro Handelstag erzeugen. Durch den Filter wurde der Handel um die Hälfte reduziert. Da sich die durchschnittliche Haltedauer über die gesamte Handelssitzung erstreckt, spiegeln die Ergebnisse in gewisser Weise den Makrotrend wider, wobei Kaufpositionen häufiger vorkommen und eine höhere Gewinnquote aufweisen. Insgesamt erreicht die Strategie einen Gewinnfaktor von 1,23 und eine Sharpe Ratio von 2,81, was eine starke Performance widerspiegelt. Diese einfachen Regeln sind weniger anfällig für eine Überanpassung, was darauf hindeutet, dass solide Backtest-Ergebnisse wahrscheinlich auch im realen Handel Bestand haben werden.

orb1-Vergleich

orb1 Absenkung

Der EA übertrifft den Buy-and-Hold-Ansatz für USTEC über diesen Fünfjahreszeitraum eindrucksvoll, während der maximale Drawdown mit 18 % nur halb so hoch ist wie der der Benchmark. Die Kapitalkurve bleibt glatt, mit nur einer kurzen Stagnation von Ende 2022 bis Anfang 2023, einer Zeit, in der die USTEC mit einer größeren Inanspruchnahme konfrontiert war.

orb1 monatliche Rendite

orb1 monatliche Drawdown

Alpha: 1.6017
Beta: 0.0090

Ein Beta von 0,9 % zeigt, dass die tägliche Rendite nur zu 0,9 % mit dem Basiswert korreliert. Drawdowns und Renditen bleiben konstant, was darauf hindeutet, dass der Fonds auch in extremen Zeiten wie dem COVID-Absturz 2020 stabil bleibt. Die meisten Monate sind gewinnbringend, und die Rückschläge sind gering, wobei der schlechteste bei 10,2 % liegt. Insgesamt handelt es sich um eine handelbare und rentable Strategie.


Strategie zwei: VWAP Trendfolge

Die zweite Strategie, die wir uns ansehen werden, ist eher eine marktoffene Trendfolgestrategie, die in dem Papier „Volume Weighted Average Price (VWAP) The Holy Grail for Day Trading Systems“ vorgestellt wurde. Die Motivation hinter den Signalregeln besteht darin, den VWAP als klare, volumengewichtete Benchmark für die Identifizierung von Intraday-Trends zu nutzen. Eine Kaufposition wird ausgelöst, wenn der Kurs über dem VWAP schließt, und eine Verkaufsposition, wenn er darunter schließt, um das bestätigte Momentum zu erfassen und gleichzeitig das Rauschen herauszufiltern. Diese Einfachheit gewährleistet umsetzbare, reproduzierbare Signale für Daytrader. Dieser klassische Trendfolge-Ansatz funktioniert am besten bei hoher Volatilität, fängt langlaufende Trends ein und bringt hohe Gewinne im Verhältnis zum Risiko. Während der fünf Stunden, in denen die Börse geöffnet ist, erfährt der Index erhebliche Bewegungen, was eine ausgezeichnete zeitliche Liquidität für den Erfolg der Strategie bietet.

Das ursprüngliche Papier befasste sich mit einem einminütigen Zeitrahmen und behauptete, dieser sei unter den verschiedenen Zeitrahmen am effektivsten. Meine persönlichen Tests haben jedoch ergeben, dass ein 15-Minuten-Zeitrahmen für diese Strategie besser geeignet ist, was wahrscheinlich auf die höheren Handelskosten von CFDs im Vergleich zu ETFs zurückzuführen ist, die einen häufigen Handel weniger rentabel machen. Auch ein Stop-Loss wurde in dem Papier nicht berücksichtigt. In unserem Ansatz werden wir einen einbeziehen, da wir einen höheren Zeitrahmen verwenden. Dieser Zusatz dient als Unfallschutz und liefert eine Referenzspanne für die Risikoberechnung. Schließlich haben wir einen gleitenden Durchschnittstrendfilter hinzugefügt, wie wir es zuvor getan haben.

Unsere modifizierten Signalregeln lauten wie folgt:

  • Kaufe nach der Markteröffnung, wenn keine aktuelle Position eröffnet ist und der letzte 15-Minuten-Schlusskurs über dem VWAP und dem gleitenden 300-Perioden-Durchschnitt liegt.
  • Verkaufe nach der Markteröffnung, wenn keine aktuelle Position eröffnet wurde und der letzte 15-Minuten-Schlusskurs unter dem VWAP und dem gleitenden 300-Perioden-Durchschnitt liegt.
  • Stopp-Loss wird bei 0,8 % des Kurses vom Einstiegsniveau gesetzt.
  • 2% Risiko pro Handelsgeschäft.

Der vollständige MQL5-Code für den EA:

//USTEC-M15
#include <Trade/Trade.mqh>
CTrade trade;

input int startHour = 18;
input int startMinute = 35;
input int endHour = 23;
input int endMinute = 45;

input double risk = 2.0;
input double slp = 0.008;
input int MaPeriods = 300;
input int Magic = 0;

int barsTotal = 0;
int handleMa;
double lastClose=0;
double lot = 0.1;

//+------------------------------------------------------------------+
//|Initialization function                                           |
//+------------------------------------------------------------------+ 
int OnInit()
  {
   trade.SetExpertMagicNumber(Magic);
   handleMa = iMA(_Symbol,PERIOD_CURRENT,MaPeriods,0,MODE_SMA,PRICE_CLOSE);
   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
//|Deinitialization function                                         |
//+------------------------------------------------------------------+ 
void OnDeinit(const int reason)
  { 
  }

//+------------------------------------------------------------------+
//|On tick function                                                  |
//+------------------------------------------------------------------+ 
void OnTick()
  {
   int bars = iBars(_Symbol, PERIOD_CURRENT);
   if(barsTotal != bars){
      barsTotal=bars;
      bool NotInPosition = true;
      double ma[];
      CopyBuffer(handleMa,0,1,1,ma);
      if(MarketOpened()&&!MarketClosed()){
         lastClose = iClose(_Symbol,PERIOD_CURRENT,1);
         int startIndex = getSessionStartIndex();
         double vwap = getVWAP(startIndex);
         for(int i = PositionsTotal()-1; i>=0; i--){
            ulong pos = PositionGetTicket(i);
            string symboll = PositionGetSymbol(i);
            if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol){
               if((PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY&&lastClose<vwap)||(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL&&lastClose>vwap))trade.PositionClose(pos);
               else NotInPosition=false;
            }
         }
         if(lastClose<vwap&&NotInPosition&&lastClose<ma[0])executeSell();
         if(lastClose>vwap&&NotInPosition&&lastClose>ma[0]) executeBuy();
       } 
       if(MarketClosed()){
          for(int i = PositionsTotal()-1; i>=0; i--){
            ulong pos = PositionGetTicket(i);
            string symboll = PositionGetSymbol(i);
            if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol)trade.PositionClose(pos);
          }
       }
        
   }
}

//+------------------------------------------------------------------+
//| Detect if market is opened                                       |
//+------------------------------------------------------------------+ 
bool MarketOpened()
{
    datetime currentTime = TimeTradeServer(); 
    MqlDateTime timeStruct;
    TimeToStruct(currentTime, timeStruct);
    int currentHour = timeStruct.hour;
    int currentMinute = timeStruct.min;
    if (currentHour >= startHour &&currentMinute>=startMinute)return true;
    else return false;
}

//+------------------------------------------------------------------+
//| Detect if market is closed                                       |
//+------------------------------------------------------------------+ 
bool MarketClosed()
{
    datetime currentTime = TimeTradeServer(); 
    MqlDateTime timeStruct;
    TimeToStruct(currentTime, timeStruct);
    int currentHour = timeStruct.hour;
    int currentMinute = timeStruct.min;

    if (currentHour >= endHour && currentMinute >= endMinute)return true;
    else return false;
}

//+------------------------------------------------------------------+
//| Sell execution function                                          |
//+------------------------------------------------------------------+        
void executeSell() {      
       double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
       bid = NormalizeDouble(bid,_Digits);
       double sl = bid*(1+slp);
       sl = NormalizeDouble(sl, _Digits);
       lot = calclots(bid*slp);
       trade.Sell(lot,_Symbol,bid,sl);    
}

//+------------------------------------------------------------------+
//| Buy execution function                                           |
//+------------------------------------------------------------------+    
void executeBuy() {
       double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
       ask = NormalizeDouble(ask,_Digits);
       double sl = ask*(1-slp);
       sl = NormalizeDouble(sl, _Digits);
       lot = calclots(ask*slp);
       trade.Buy(lot,_Symbol,ask,sl);
}

//+------------------------------------------------------------------+
//| Get VWAP function                                                |
//+------------------------------------------------------------------+
double getVWAP(int startCandle)
{
   double sumPV = 0.0;  // Sum of (price * volume)
   long sumV = 0.0;    // Sum of volume

   // Loop from the starting candle index down to 1 (excluding current candle)
   for(int i = startCandle; i >= 1; i--)
   {
      // Calculate typical price: (High + Low + Close) / 3
      double high = iHigh(_Symbol, PERIOD_CURRENT, i);
      double low = iLow(_Symbol, PERIOD_CURRENT, i);
      double close = iClose(_Symbol, PERIOD_CURRENT, i);
      double typicalPrice = (high + low + close) / 3.0;

      // Get volume and update sums
      long volume = iVolume(_Symbol, PERIOD_CURRENT, i);
      sumPV += typicalPrice * volume;
      sumV += volume;
   }

   // Calculate VWAP or return 0 if no volume
   if(sumV == 0)
      return 0.0;
   
   double vwap = sumPV / sumV;

   // Plot the dot
   datetime currentBarTime = iTime(_Symbol, PERIOD_CURRENT, 0);
   string objName = "VWAP" + TimeToString(currentBarTime, TIME_MINUTES);
   ObjectCreate(0, objName, OBJ_ARROW, 0, currentBarTime, vwap);
   ObjectSetInteger(0, objName, OBJPROP_COLOR, clrGreen);    // Green dot
   ObjectSetInteger(0, objName, OBJPROP_STYLE, STYLE_DOT);   // Dot style
   ObjectSetInteger(0, objName, OBJPROP_WIDTH, 1);           // Size of the dot
   
   return vwap;
}

//+------------------------------------------------------------------+
//| Find the index of the candle corresponding to the session open   |
//+------------------------------------------------------------------+
int getSessionStartIndex()
{
   int sessionIndex = 1;
   // Loop over bars until we find the session open
   for(int i = 1; i <=1000; i++)
   {
      datetime barTime = iTime(_Symbol, PERIOD_CURRENT, i);
      MqlDateTime dt;
      TimeToStruct(barTime, dt);
      
      if(dt.hour == startHour && dt.min == startMinute-5)
      {
         sessionIndex = i;
         break;
      }
   }
      
   return sessionIndex;
}

//+------------------------------------------------------------------+
//| Calculate lot size based on risk and stop loss range             |
//+------------------------------------------------------------------+
double calclots(double slpoints) {
    double riskAmount = AccountInfoDouble(ACCOUNT_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;
}

Ein typischer Handel würde folgendermaßen aussehen:

orb2-Beispiel

Backtest-Ergebnisse:

orb2-Einstellungen

orb2-Parameter

orb2-Aktienkurve

orb2-Ergebnis

Verglichen mit der ersten Strategie, dem Opening Range Breakout, wird bei dieser Strategie häufiger gehandelt, im Durchschnitt mehr als ein Handel pro Tag. Dieser Anstieg ist darauf zurückzuführen, dass ein Wiedereinstieg immer dann möglich ist, wenn der Kurs den VWAP während der Marktzeit erneut überschreitet. Die Gewinnquote ist mit 42 % niedriger und liegt unter 50 %, was für einen Trendfolgeansatz mit dynamischem Trailing-Stop typisch ist. Dieses Setup begünstigt Handelsgeschäfte mit höherem Rendite-Risiko-Verhältnis, erhöht aber auch die Chance, ausgestoppt zu werden. Die Sharpe Ratio und der Gewinnfaktor sind mit 3,57 bzw. 1,26 außergewöhnlich hoch.

orb2-Vergleich

orb2 Drawdown

Mit einer Rendite von 501 % über fünf Jahre übertrifft die Strategie die Buy-and-Hold-Strategie deutlich. Dies geschieht mit einem maximalen Drawdown von 16 %, wobei die schlechteste Periode Ende 2021 liegt und sich von der schlechtesten Phase von USTEC unterscheidet, was auf eine unkorrelierte Performance hindeutet.

orb2 monatliche Rendite

orb2 monatlicher Drawdown

Alpha: 4.8714
Beta: 0.0985

Das Beta entspricht dem der ersten Strategie, was ebenfalls auf eine geringe Korrelation mit dem Basiswert hindeutet. Bemerkenswert ist, dass das Alpha dieser Strategie dreimal höher ist als das der ersten Strategie, während der maximale Drawdown ähnlich hoch ist. Dieser Vorteil beruht wahrscheinlich auf häufigerem Handel, kürzeren Haltedauern und einer größeren internen Diversifizierung durch Long- und Short-Möglichkeiten am selben Tag. Die monatliche Tabelle bestätigt die solide Performance, mit gleichmäßig verteilten und konsistenten Drawdowns und Renditen über die Monate hinweg.


Strategie Drei: Concretum Bands Breakout

Die dritte Strategie ist ein Ausbruch aus dem Rauschbereich, die während der Marktöffnungszeit gehandelt wird. Sie wurde erstmals in „Beat the Market An Effective Intraday Momentum Strategy for S&P500 ETF (SPY)“ vorgestellt und ging später über X/Twitter viral. Die Motivation hinter den Signalregeln der Concretum Bands Breakout-Strategie liegt in dem Ziel, signifikante Preisbewegungen zu identifizieren, die durch Ungleichgewichte zwischen Angebot und Nachfrage im Intraday-Handel ausgelöst werden. Die Strategie verwendet volatilitätsbasierte Bandbreiten, die aus dem Schlusskurs des Vortages oder dem Eröffnungswert des aktuellen Tages berechnet und mit einem Volatilitätsmultiplikator angepasst werden, um einen „Rauschbereich“ zu definieren, in dem zufällige Kursschwankungen auftreten. Die Regeln zielen darauf ab, Marktrauschen herauszufiltern, aus wahrscheinlichen Schwankungen Kapital zu schlagen und sich an die schwankende Volatilität anzupassen, um sicherzustellen, dass die Handelsgeschäfte auf echte Trendanfänge und nicht auf flüchtige Schwankungen ausgerichtet sind.

Hier sind die Berechnungen der Bänder.

Formel der Bänder

Da die Signalregeln aus dem Originalbeitrag gut durchdacht sind, werden wir in diesem Artikel nicht viel ändern. Der Einfachheit halber werden wir denselben Handelsinstrument (USTEC) und denselben Risikomanagementansatz beibehalten, was zu anderen Ergebnissen führen kann als der Ansatz in der Studie. Die Signalregeln lauten wie folgt:

  • Kaufe nach der Markteröffnung, wenn der 1-Minuten-Balken das obere Band überschreitet.
  • Verkaufe nach der Markteröffnung, wenn der 1-Minuten-Balken unter das untere Band fällt.
  • Schließe alle Positionen, wenn der Markt geschlossen ist.
  • Der Stop-Loss wird auf 1% des Preises der Einstiegsposition gesetzt, zusammen mit dem VWAP als Trailing-Stop.
  • 4% Risiko pro Handel.

Vollständiger MQL5-Code des EA:

//USTEC-M1
#include <Trade/Trade.mqh>
CTrade trade;

input int startHour = 18;
input int startMinute = 35;
input int endHour = 23;
input int endMinute = 55;

input double risk = 4.0;
input double slp = 0.01;
input int Magic = 0;
input int maPeriod = 400;

int barsTotal = 0;
int handleMa;
double lastClose=0;
double lastOpen = 0;
double lot = 0.1;

//+------------------------------------------------------------------+
//|Initialization function                                           |
//+------------------------------------------------------------------+ 
int OnInit()
  {
   trade.SetExpertMagicNumber(Magic);
   handleMa = iMA(_Symbol,PERIOD_CURRENT,maPeriod,0,MODE_SMA,PRICE_CLOSE);
   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
//|Deinitialization function                                         |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {  
  }

//+------------------------------------------------------------------+
//|On tick function                                                  |
//+------------------------------------------------------------------+ 
void OnTick()
  {
   int bars = iBars(_Symbol, PERIOD_CURRENT);
   if(barsTotal != bars){
      barsTotal=bars;
      bool NotInPosition = true;
      double ma[];
      CopyBuffer(handleMa,0,1,1,ma);
      if(MarketOpened()&&!MarketClosed()){
         lastClose = iClose(_Symbol,PERIOD_CURRENT,1);
         lastOpen = iOpen(_Symbol,PERIOD_CURRENT,1);
         int startIndex = getSessionStartIndex();
         double vwap = getVWAP(startIndex);
         for(int i = PositionsTotal()-1; i>=0; i--){
            ulong pos = PositionGetTicket(i);
            string symboll = PositionGetSymbol(i);
            if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol){
               if((PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY&&lastClose<vwap)||(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL&&lastClose>vwap))trade.PositionClose(pos);
               else NotInPosition=false;
            }
         }
         double lower = getLowerBand();
         double upper = getUpperBand();
         if(NotInPosition&&lastOpen>lower&&lastClose<lower&&lastClose<ma[0])executeSell();
         if(NotInPosition&&lastOpen<upper&&lastClose>upper&&lastClose>ma[0]) executeBuy();
       } 
       if(MarketClosed()){
          for(int i = PositionsTotal()-1; i>=0; i--){
            ulong pos = PositionGetTicket(i);
            string symboll = PositionGetSymbol(i);
            if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol)trade.PositionClose(pos);
          }
       }
        
   }
}

//+------------------------------------------------------------------+
//| Detect if market is opened                                       |
//+------------------------------------------------------------------+ 
bool MarketOpened()
{
    datetime currentTime = TimeTradeServer(); 
    MqlDateTime timeStruct;
    TimeToStruct(currentTime, timeStruct);
    int currentHour = timeStruct.hour;
    int currentMinute = timeStruct.min;
    if (currentHour >= startHour &&currentMinute>=startMinute)return true;
    else return false;
}

//+------------------------------------------------------------------+
//| Detect if market is closed                                       |
//+------------------------------------------------------------------+ 
bool MarketClosed()
{
    datetime currentTime = TimeTradeServer(); 
    MqlDateTime timeStruct;
    TimeToStruct(currentTime, timeStruct);
    int currentHour = timeStruct.hour;
    int currentMinute = timeStruct.min;
    if (currentHour >= endHour && currentMinute >= endMinute)return true;
    else return false;
}

//+------------------------------------------------------------------+
//| Sell execution function                                          |
//+------------------------------------------------------------------+        
void executeSell() {      
       double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
       bid = NormalizeDouble(bid,_Digits);
       double sl = bid*(1+slp);
       sl = NormalizeDouble(sl, _Digits);
       lot = calclots(bid*slp);
       trade.Sell(lot,_Symbol,bid,sl);    
}

//+------------------------------------------------------------------+
//| Buy execution function                                           |
//+------------------------------------------------------------------+     
void executeBuy() {
       double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
       ask = NormalizeDouble(ask,_Digits);
       double sl = ask*(1-slp);
       sl = NormalizeDouble(sl, _Digits);
       lot = calclots(ask*slp);
       trade.Buy(lot,_Symbol,ask,sl);
}

//+------------------------------------------------------------------+
//| Get VWAP function                                                |
//+------------------------------------------------------------------+
double getVWAP(int startCandle)
{
   double sumPV = 0.0;  // Sum of (price * volume)
   long sumV = 0.0;    // Sum of volume

   // Loop from the starting candle index down to 1 (excluding current candle)
   for(int i = startCandle; i >= 1; i--)
   {
      // Calculate typical price: (High + Low + Close) / 3
      double high = iHigh(_Symbol, PERIOD_CURRENT, i);
      double low = iLow(_Symbol, PERIOD_CURRENT, i);
      double close = iClose(_Symbol, PERIOD_CURRENT, i);
      double typicalPrice = (high + low + close) / 3.0;

      // Get volume and update sums
      long volume = iVolume(_Symbol, PERIOD_CURRENT, i);
      sumPV += typicalPrice * volume;
      sumV += volume;
   }

   // Calculate VWAP or return 0 if no volume
   if(sumV == 0)
      return 0.0;
   
   double vwap = sumPV / sumV;

   // Plot the dot
   datetime currentBarTime = iTime(_Symbol, PERIOD_CURRENT, 0);
   string objName = "VWAP" + TimeToString(currentBarTime, TIME_MINUTES);
   ObjectCreate(0, objName, OBJ_ARROW, 0, currentBarTime, vwap);
   ObjectSetInteger(0, objName, OBJPROP_COLOR, clrGreen);    // Green dot
   ObjectSetInteger(0, objName, OBJPROP_STYLE, STYLE_DOT);   // Dot style
   ObjectSetInteger(0, objName, OBJPROP_WIDTH, 1);           // Size of the dot
   
   return vwap;
}

//+------------------------------------------------------------------+
//| Find the index of the candle corresponding to the session open   |
//+------------------------------------------------------------------+
int getSessionStartIndex()
{
   int sessionIndex = 1;
   // Loop over bars until we find the session open
   for(int i = 1; i <=1000; i++)
   {
      datetime barTime = iTime(_Symbol, PERIOD_CURRENT, i);
      MqlDateTime dt;
      TimeToStruct(barTime, dt);
      
      if(dt.hour == startHour && dt.min == 30)
      {
         sessionIndex = i;
         break;
      }
   }
      
   return sessionIndex;
}

//+------------------------------------------------------------------+
//| Get the number of bars from now to market open                   |
//+------------------------------------------------------------------+
int getBarShiftForTime(datetime day_start, int hour, int minute) {
    MqlDateTime dt;
    TimeToStruct(day_start, dt);
    dt.hour = hour;
    dt.min = minute;
    dt.sec = 0;
    datetime target_time = StructToTime(dt);
    int shift = iBarShift(_Symbol, PERIOD_M1, target_time, true);
    return shift;
}

//+------------------------------------------------------------------+
//| Get the upper Concretum band value                               |
//+------------------------------------------------------------------+
double getUpperBand() {
    // Get the time of the current bar
    datetime current_time = iTime(_Symbol, PERIOD_CURRENT, 0);
    MqlDateTime current_dt;
    TimeToStruct(current_time, current_dt);
    int current_hour = current_dt.hour;
    int current_min = current_dt.min;
    
    // Find today's opening price at 9:30 AM
    datetime today_start = iTime(_Symbol, PERIOD_D1, 0);
    int bar_at_930_today = getBarShiftForTime(today_start, 9, 30);
    if (bar_at_930_today < 0) return 0; // Return 0 if no 9:30 bar exists
    double open_930_today = iOpen(_Symbol, PERIOD_M1, bar_at_930_today);
    if (open_930_today == 0) return 0; // No valid price
    
    // Calculate sigma based on the past 14 days
    double sum_moves = 0;
    int valid_days = 0;
    for (int i = 1; i <= 14; i++) {
        datetime day_start = iTime(_Symbol, PERIOD_D1, i);
        int bar_at_930 = getBarShiftForTime(day_start, 9, 30);
        int bar_at_HHMM = getBarShiftForTime(day_start, current_hour, current_min);
        if (bar_at_930 < 0 || bar_at_HHMM < 0) continue; // Skip if bars don't exist
        double open_930 = iOpen(_Symbol, PERIOD_M1, bar_at_930);
        double close_HHMM = iClose(_Symbol, PERIOD_M1, bar_at_HHMM);
        if (open_930 == 0) continue; // Skip if no valid opening price
        double move = MathAbs(close_HHMM / open_930 - 1);
        sum_moves += move;
        valid_days++;
    }
    if (valid_days == 0) return 0; // Return 0 if no valid data
    double sigma = sum_moves / valid_days;
    
    // Calculate the upper band
    double upper_band = open_930_today * (1 + sigma);
    
    // Plot a blue dot at the upper band level
    string obj_name = "UpperBand_" + TimeToString(current_time, TIME_DATE|TIME_MINUTES|TIME_SECONDS);
    ObjectCreate(0, obj_name, OBJ_ARROW, 0, current_time, upper_band);
    ObjectSetInteger(0, obj_name, OBJPROP_ARROWCODE, 159); // Dot symbol
    ObjectSetInteger(0, obj_name, OBJPROP_COLOR, clrBlue);
    ObjectSetInteger(0, obj_name, OBJPROP_WIDTH, 2);
    
    return upper_band;
}

//+------------------------------------------------------------------+
//| Get the lower Concretum band value                               |
//+------------------------------------------------------------------+
double getLowerBand() {
    // Get the time of the current bar
    datetime current_time = iTime(_Symbol, PERIOD_CURRENT, 0);
    MqlDateTime current_dt;
    TimeToStruct(current_time, current_dt);
    int current_hour = current_dt.hour;
    int current_min = current_dt.min;
    
    // Find today's opening price at 9:30 AM
    datetime today_start = iTime(_Symbol, PERIOD_D1, 0);
    int bar_at_930_today = getBarShiftForTime(today_start, 9, 30);
    if (bar_at_930_today < 0) return 0; // Return 0 if no 9:30 bar exists
    double open_930_today = iOpen(_Symbol, PERIOD_M1, bar_at_930_today);
    if (open_930_today == 0) return 0; // No valid price
    
    // Calculate sigma based on the past 14 days
    double sum_moves = 0;
    int valid_days = 0;
    for (int i = 1; i <= 14; i++) {
        datetime day_start = iTime(_Symbol, PERIOD_D1, i);
        int bar_at_930 = getBarShiftForTime(day_start, 9, 30);
        int bar_at_HHMM = getBarShiftForTime(day_start, current_hour, current_min);
        if (bar_at_930 < 0 || bar_at_HHMM < 0) continue; // Skip if bars don't exist
        double open_930 = iOpen(_Symbol, PERIOD_M1, bar_at_930);
        double close_HHMM = iClose(_Symbol, PERIOD_M1, bar_at_HHMM);
        if (open_930 == 0) continue; // Skip if no valid opening price
        double move = MathAbs(close_HHMM / open_930 - 1);
        sum_moves += move;
        valid_days++;
    }
    if (valid_days == 0) return 0; // Return 0 if no valid data
    double sigma = sum_moves / valid_days;
    
    // Calculate the lower band
    double lower_band = open_930_today * (1 - sigma);
    
    // Plot a red dot at the lower band level
    string obj_name = "LowerBand_" + TimeToString(current_time, TIME_DATE|TIME_MINUTES|TIME_SECONDS);
    ObjectCreate(0, obj_name, OBJ_ARROW, 0, current_time, lower_band);
    ObjectSetInteger(0, obj_name, OBJPROP_ARROWCODE, 159); // Dot symbol
    ObjectSetInteger(0, obj_name, OBJPROP_COLOR, clrRed);
    ObjectSetInteger(0, obj_name, OBJPROP_WIDTH, 2);
    
    return lower_band;
}

//+------------------------------------------------------------------+
//| Calculate lot size based on risk and stop loss range             |
//+------------------------------------------------------------------+
double calclots(double slpoints) {
    double riskAmount = AccountInfoDouble(ACCOUNT_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;
}

Ein typischer Handel würde folgendermaßen aussehen:

orb3-Beispiel

Backtest-Ergebnisse:

orb3-Einstellungen

orb3-Parameter

orb3-Kapitalkurve

orb3-Ergebnisse

Die Strategie wird mit einer ähnlichen Häufigkeit wie die erste ORB-Strategie gehandelt, im Durchschnitt etwa alle zwei Handelstage. Er wird nicht täglich gehandelt, da die Kursbewegungen manchmal innerhalb einer Geräuschspanne verbleiben und es nicht gelingt, aus den Bändern auszubrechen. Die Gewinnquote liegt unter 50 %, was auf die Verwendung des VWAP als dynamischen Trailing-Stop zurückzuführen ist. Ein Gewinnfaktor von 1,3 und eine Sharpe Ratio von 5,9 deuten auf hohe Renditen im Verhältnis zum Drawdown hin.

orb3-Vergleich

orb3 Drawdown

Die Strategie übertrifft die „Buy-and-Hold“-Strategie leicht, während der maximale Drawdown nur halb so groß ist. Allerdings kommt es bei dieser Strategie häufiger zu signifikanten Drawdowns als bei der vorherigen Strategie. Dies deutet darauf hin, dass die Strategie trotz ihrer überragenden Performance häufig längere Rückschläge hinnehmen muss, bevor sie neue Höchststände bei den Aktien erreicht.

orb3 monatliche Rendite

orb3 monatlicher Drawdown

Alpha: 1.6562
Beta: -0.1183

Diese Strategie hat ein Beta von -11%, was auf eine leicht negative Korrelation mit dem Basiswert hinweist. Dies ist ein günstiges Ergebnis für Händler, die einen Vorteil suchen, der sich entgegen den Markttrends bewegt. Im Vergleich zu den beiden anderen Strategien hat diese Strategie mehr Drawdown-Monate, etwa 50 %, liefert aber in den profitablen Monaten höhere Erträge. Dieses Muster deutet darauf hin, dass sich Händler auf längere Drawdown-Phasen im Live-Handel einstellen und geduldig auf die größeren Ertragsphasen warten sollten. Mit einer beträchtlichen Stichprobengröße über einen soliden Zeitrahmen bleibt diese Strategie handelbar.


Überlegungen

In unserem letzten Artikel haben wir uns mit dem Aufbau von Modellsystemen anstelle von Einzelstrategien beschäftigt. Wir haben das gleiche Konzept in diesem Artikel angewandt. Alle drei Strategien basieren auf Ausbrüchen aus der offenen Zeitspanne des Aktienmarktes, wobei sich verschiedene Varianten als profitabel erwiesen haben. Wir tauschten auch Erkenntnisse darüber aus, wie man durch die Anpassung akademischer Arbeiten an unser eigenes Wissen und unsere Intuition einen strategischen Vorsprung erzielen kann. Dieser Ansatz eignet sich hervorragend, um robuste Handelskonzepte aufzudecken und unser Verständnis zu erweitern.

Da wir nun drei rentable Strategien in der Hand haben, sollten wir nun eine Portfolioperspektive in Betracht ziehen. Wir müssen ihre kombinierten Ergebnisse, Korrelationen und den maximalen Gesamtverlust untersuchen, bevor wir sie gleichzeitig handeln. Im algorithmischen Handel ist die Diversifizierung der wahre heilige Gral. Sie hilft, die Verluste verschiedener Strategien über verschiedene Zeiträume hinweg auszugleichen. Bis zu einem gewissen Grad wird Ihre maximale Rendite durch den Rückschlag, den Sie zu tolerieren bereit sind, begrenzt. Durch die Kombination verschiedener Strategien können Sie Ihr Engagement erhöhen und gleichzeitig einen ähnlichen Drawdown beibehalten, was die Rendite steigert. Allerdings kann das Risiko auf diese Weise nicht unendlich skaliert werden, da das minimale Risiko immer über dem der einzelnen Handelsgeschäfte liegt.

Einige gängige Wege zur Diversifizierung sind:

  • Handel mit ein und demselben Strategiemodell und dessen Verteilung auf verschiedene unkorrelierte Vermögenswerte.
  • Handel mit verschiedenen Strategiemodellen für ein und denselben Vermögenswert.
  • Verteilen Sie das Kapital auf verschiedene Handelsansätze wie Optionen, Arbitrage und Aktienauswahlen.

Es ist wichtig zu verstehen, dass mehr Diversifizierung nicht immer besser ist, sondern dass es auf eine unkorrelierte Diversifizierung ankommt. So ist es beispielsweise nicht ideal, dieselbe Strategie auf alle Kryptomärkte anzuwenden, da die meisten Krypto-Assets auf breiterer Ebene stark korreliert sind. Darüber hinaus kann es irreführend sein, sich ausschließlich auf die Backtest-Diversifizierung zu verlassen, da die Korrelation vom Zeitraum abhängt, z. B. von täglichen oder monatlichen Renditen. Darüber hinaus können die Korrelationen zwischen den Strategien bei starken Marktveränderungen unerwartet verzerrt und verzerrt werden. Aus diesem Grund ziehen es einige Händler vor, die Korrelationen der Live-Handelsergebnisse im Vergleich zu den Korrelationen der Backtest-Ergebnisse zu verwenden, um zu beurteilen, ob die Vorteile ihrer Strategien abgenommen haben.

Mit diesem Wissen im Hinterkopf sind hier die Backtest-Statistiken der kombinierten Performance der drei Strategien.

kombinierte Kapitalkurve

kombinierte Drawdown

Die Kapital- und Drawdown-Kurven zeigen anschaulich, wie die verschiedenen Strategien ihre Drawdowns über verschiedene Zeiträume hinweg ausgleichen. Der maximale Drawdown liegt nun bei etwa 10 % und damit deutlich unter den maximalen Drawdowns der einzelnen Strategien, die alle über 15 % liegen.

kombinierte Monatsrendite

kombinierte monatlicher Drawdown

Drawdowns und Renditen scheinen gleichmäßig über die Monate verteilt zu sein, was darauf hindeutet, dass keine extremen Regimeperioden die Backtest-Performance unverhältnismäßig stark beeinflussen. Dies ist bei über 3000 Stichproben und einer konsistenten Risikoallokation pro Handelsgeschäft sinnvoll.

Korrelationsmatrix

Die Korrelation misst, wie ähnlich die Kapitalkurven der Backtests der einzelnen Strategien sind. Sie reicht von -1 für entgegengesetztes Verhalten bis 1 für identisches Verhalten und vergleicht in der Regel zwei Subjekte. Wir berechnen sie, indem wir x für die Zeitachse der Aktienkurve und y für die Renditeachse verwenden.

Korrelation

Die Korrelationsmatrix hilft bei der Visualisierung von Performance-Korrelationen zwischen drei oder mehr Strategien. Bei monatlicher Betrachtung zeigt sich, dass die monatlichen Renditen der einzelnen Strategien leicht korreliert sind, im Durchschnitt um 0,3. Korrelationen unter 0,5 sind akzeptabel, wobei negative Korrelationen vorzuziehen sind. Obwohl sowohl Kauf- als auch Verkaufs-Positionen gehandelt werden, weisen alle Strategien positive Korrelationen auf, wahrscheinlich weil sie auf denselben Vermögenswert setzen. Dieser tiefere Einblick zeigt, dass der kombinierte maximale Drawdown zwar niedriger ist als bei den einzelnen Strategien, die monatlichen Renditen aber ähnlich bleiben, da wir mit vergleichbaren Strategien auf denselben Vermögenswert handeln. Dies legt nahe, diese Strategien mit anderen Strategien zu kombinieren, anstatt sie in ein und demselben Portfolio zusammenzufassen.


Schlussfolgerung

In diesem Artikel wurden drei Intraday-Strategien des Opening Range Breakout aus den akademischen Papieren der Concretum Group untersucht. Wir begannen damit, den Forschungshintergrund zu skizzieren und die wichtigsten Konzepte und Methoden zu erläutern. Anschließend haben wir die Beweggründe für die drei Strategien untersucht, Verbesserungsmöglichkeiten aufgezeigt, klare Signalregeln, MQL5-Code und statistische Backtest-Analysen bereitgestellt. Schließlich haben wir den Prozess reflektiert, eine Diversifizierung eingeführt und die kombinierten Ergebnisse analysiert.

Der Artikel bietet Einblicke in die wahre Robustheit der Strategieentwicklung. Eine tiefer gehende statistische Analyse bietet einen breiteren Überblick über die Leistung einer Strategie und ihre Rolle innerhalb eines Portfolios. Alle Bemühungen zielen darauf ab, das Verständnis zu vertiefen und Vertrauen zu schaffen, bevor der Live-Handel beginnt. Die Leser werden ermutigt, den Forschungsprozess nachzuvollziehen und anhand des vorgestellten Rahmens Fachberater zu entwickeln.


Datei-Tabelle

Datei NameDateiverwendung
ORB1.mq5.                                     Das MQL5 EA-Skript für die erste Strategie
ORB2.mq5Das MQL5 EA-Skript für die zweite Strategie
ORB3.mq5Das MQL5 EA-Skript für die dritte Strategie

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

Beigefügte Dateien |
ORB.zip (7.11 KB)
Letzte Kommentare | Zur Diskussion im Händlerforum (11)
Zhuo Kai Chen
Zhuo Kai Chen | 25 Apr. 2025 in 01:19

Ps. Für ORB3 habe ich die Zeit für die Markteröffnung auf 9:30 Uhr fest einprogrammiert. Sie können diese Funktionen so ändern, dass sie mit der Serverzeit der New Yorker Marktöffnungszeit übereinstimmen.

//+------------------------------------------------------------------+
//| Ermittelt den oberen Concretum-Band-Wert|
//+------------------------------------------------------------------+
double getUpperBand(int target_hour = 17, int target_min = 30) {
    // Abfrage der Uhrzeit des aktuellen Taktes
    datetime current_time = iTime(_Symbol, PERIOD_CURRENT, 0);
    MqlDateTime current_dt;
    TimeToStruct(current_time, current_dt);
    int current_hour = current_dt.hour;
    int current_min = current_dt.min;
    
    // Ermitteln Sie den heutigen Eröffnungskurs zur Zielzeit (z. B. 17:30 Uhr Serverzeit)
    datetime today_start = iTime(_Symbol, PERIOD_D1, 0);
    int bar_at_target_today = getBarShiftForTime(today_start, target_hour, target_min);
    if (bar_at_target_today < 0) return 0; // Rückgabe 0, wenn kein Zielbalken existiert
    double open_target_today = iOpen(_Symbol, PERIOD_M1, bar_at_target_today);
    if (open_target_today == 0) return 0; // Kein gültiger Preis
    
    // Berechnung von Sigma auf der Grundlage der letzten 14 Tage
    double sum_moves = 0;
    int valid_days = 0;
    for (int i = 1; i <= 14; i++) {
        datetime day_start = iTime(_Symbol, PERIOD_D1, i);
        int bar_at_target = getBarShiftForTime(day_start, target_hour, target_min);
        int bar_at_HHMM = getBarShiftForTime(day_start, current_hour, current_min);
        if (bar_at_target < 0 || bar_at_HHMM < 0) continue; // Überspringen, wenn keine Balken vorhanden sind
        double open_target = iOpen(_Symbol, PERIOD_M1, bar_at_target);
        double close_HHMM = iClose(_Symbol, PERIOD_M1, bar_at_HHMM);
        if (open_target == 0) continue; // Überspringen, wenn kein gültiger Eröffnungskurs
        double move = MathAbs(close_HHMM / open_target - 1);
        sum_moves += move;
        valid_days++;
    }
    if (valid_days == 0) return 0; // Rückgabe 0, wenn keine gültigen Daten
    double sigma = sum_moves / valid_days;
    
    // Berechnung der oberen Bandbreite
    double upper_band = open_target_today * (1 + sigma);
    
    // Zeichnen Sie einen blauen Punkt auf der oberen Bandebene
    string obj_name = "UpperBand_" + TimeToString(current_time, TIME_DATE|TIME_MINUTES|TIME_SECONDS);
    ObjectCreate(0, obj_name, OBJ_ARROW, 0, current_time, upper_band);
    ObjectSetInteger(0, obj_name, OBJPROP_ARROWCODE, 159); // Punkt-Symbol
    ObjectSetInteger(0, obj_name, OBJPROP_COLOR, clrBlue);
    ObjectSetInteger(0, obj_name, OBJPROP_WIDTH, 2);
    
    return upper_band;
}

//+------------------------------------------------------------------+
//| Ermittelt den unteren Concretum-Band-Wert|
//+------------------------------------------------------------------+
double getLowerBand(int target_hour = 17, int target_min = 30) {
    // Abfrage der Uhrzeit des aktuellen Taktes
    datetime current_time = iTime(_Symbol, PERIOD_CURRENT, 0);
    MqlDateTime current_dt;
    TimeToStruct(current_time, current_dt);
    int current_hour = current_dt.hour;
    int current_min = current_dt.min;
    
    // Ermitteln Sie den heutigen Eröffnungskurs zur Zielzeit (z. B. 17:30 Uhr Serverzeit)
    datetime today_start = iTime(_Symbol, PERIOD_D1, 0);
    int bar_at_target_today = getBarShiftForTime(today_start, target_hour, target_min);
    if (bar_at_target_today < 0) return 0; // Rückgabe 0, wenn kein Zielbalken existiert
    double open_target_today = iOpen(_Symbol, PERIOD_M1, bar_at_target_today);
    if (open_target_today == 0) return 0; // Kein gültiger Preis
    
    // Berechnung von Sigma auf der Grundlage der letzten 14 Tage
    double sum_moves = 0;
    int valid_days = 0;
    for (int i = 1; i <= 14; i++) {
        datetime day_start = iTime(_Symbol, PERIOD_D1, i);
        int bar_at_target = getBarShiftForTime(day_start, target_hour, target_min);
        int bar_at_HHMM = getBarShiftForTime(day_start, current_hour, current_min);
        if (bar_at_target < 0 || bar_at_HHMM < 0) continue; // Überspringen, wenn keine Balken vorhanden sind
        double open_target = iOpen(_Symbol, PERIOD_M1, bar_at_target);
        double close_HHMM = iClose(_Symbol, PERIOD_M1, bar_at_HHMM);
        if (open_target == 0) continue; // Überspringen, wenn kein gültiger Eröffnungskurs
        double move = MathAbs(close_HHMM / open_target - 1);
        sum_moves += move;
        valid_days++;
    }
    if (valid_days == 0) return 0; // Rückgabe 0, wenn keine gültigen Daten
    double sigma = sum_moves / valid_days;
    
    // Berechnen Sie die untere Bandbreite
    double lower_band = open_target_today * (1 - sigma);
    
    // Zeichnen Sie einen roten Punkt auf der unteren Bandebene
    string obj_name = "LowerBand_" + TimeToString(current_time, TIME_DATE|TIME_MINUTES|TIME_SECONDS);
    ObjectCreate(0, obj_name, OBJ_ARROW, 0, current_time, lower_band);
    ObjectSetInteger(0, obj_name, OBJPROP_ARROWCODE, 159); // Punkt-Symbol
    ObjectSetInteger(0, obj_name, OBJPROP_COLOR, clrRed);
    ObjectSetInteger(0, obj_name, OBJPROP_WIDTH, 2);
    
    return lower_band;
}

Die Änderung der Berechnungszeit könnte ein Weg sein, die Strategie weiter zu optimieren :)

Digitus
Digitus | 25 Apr. 2025 in 18:15
Zhuo Kai Chen #:

OMG, ich kann nicht glauben, dass ich das übersehen habe. Es sollte sein:

Ich entschuldige mich aufrichtig für den Flüchtigkeitsfehler. Danke, dass Sie so aufmerksam gelesen und mich darauf hingewiesen haben.

Keine Sorge, ich habe den offenen und den geschlossenen Markt bereits in einer Funktion zusammengeführt.

bool MarketState()
{
   MqlDateTime structTime;
   TimeCurrent(structTime);
   structTime.sec = 0;
   structTime.hour = startHour;
   structTime.min = startMinute; 
   datetime timeStart = StructToTime(structTime);
   structTime.hour = endHour;
   structTime.min = endMinute;   
   datetime timeEnd = StructToTime(structTime);
   if(TimeCurrent() >= timeStart && TimeCurrent() < timeEnd)return true;
   else return false;
}


Noch eine Sache: Sie verwenden die OHLC-Daten des Brokers für Backtests, ohne Verzögerung. Diese Backtests scheinen etwas optimistisch zu sein, verglichen mit Backtests, die mit echten Tickdaten mit zufälliger Verzögerung für Slippage und Requotes durchgeführt werden.

Nochmals vielen Dank für Ihre Bemühungen!

Zhuo Kai Chen
Zhuo Kai Chen | 27 Apr. 2025 in 02:37
Digitus #:

Keine Sorge, ich habe den offenen und den geschlossenen Markt bereits in einer Funktion zusammengefasst.


Eine weitere Sache: Sie verwenden Broker OHLC Daten für Backtesting, keine Verzögerung. Diese Backtests scheinen ein bisschen optimistisch im Vergleich zu Backtests auf echte Tick-Daten mit zufälliger Verzögerung für Slippage und requotes getan.

Nochmals vielen Dank für Ihre Bemühungen!

Gut gemacht mit Ihrer Änderung! Ich habe den gesamten Code in meinem Github aktualisiert.

Für Ihr Anliegen, die Handelslogik tritt jede neue Bar und beinhaltet nicht Tick-Bewegung. Außerdem beträgt die durchschnittliche Haltedauer nur ein paar Stunden, was meiner Meinung nach kein signifikantes Slippage-Problem darstellen wird. Ich würde sagen, nur sehr wenige Makler bieten echte Tick-Daten für mehr als 5 Jahre, und 1 min OHLC ist genug.

Zero Trader
Zero Trader | 20 Juni 2025 in 01:58
Großartiger Artikel
Muhammad Syamil Bin Abdullah
Muhammad Syamil Bin Abdullah | 20 Juni 2025 in 15:19
Vielen Dank für die Mitteilung.
Erstellen von dynamischen MQL5-Grafikschnittstellen durch ressourcengesteuerte Bildskalierung mit bikubischer Interpolation auf Handelscharts Erstellen von dynamischen MQL5-Grafikschnittstellen durch ressourcengesteuerte Bildskalierung mit bikubischer Interpolation auf Handelscharts
In diesem Artikel erforschen wir dynamische MQL5-Grafikschnittstellen, die bikubische Interpolation für hochwertige Bildskalierung auf Handelscharts verwenden. Wir stellen flexible Positionierungsoptionen vor, die eine dynamische Zentrierung oder Eckverankerung mit nutzerdefinierten Versätzen ermöglichen.
Automatisieren von Handelsstrategien in MQL5 (Teil 16): Midnight Range Breakout mit der Preisaktion Break of Structure (BoS) Automatisieren von Handelsstrategien in MQL5 (Teil 16): Midnight Range Breakout mit der Preisaktion Break of Structure (BoS)
In diesem Artikel automatisieren wir die Midnight Range Breakout mit Break of Structure Strategie in MQL5, indem wir den Code für die Breakout-Erkennung und die Handelsausführung detailliert beschreiben. Wir definieren präzise Risikoparameter für Einstieg, Stopp und Gewinn. Backtests und Optimierung sind für den praktischen Handel enthalten.
Datenwissenschaft und ML (Teil 36): Der Umgang mit verzerrten Finanzmärkten Datenwissenschaft und ML (Teil 36): Der Umgang mit verzerrten Finanzmärkten
Die Finanzmärkte sind nicht vollkommen ausgeglichen. Einige Märkte steigen, andere fallen, und wieder andere zeigen ein gewisses Schwankungsverhalten, das auf Unsicherheit in beide Richtungen hindeutet. Diese unausgewogenen Informationen können beim Trainieren von Machine-Learning-Modellen irreführend sein, da sich die Märkte häufig ändern. In diesem Artikel werden wir verschiedene Möglichkeiten erörtern, dieses Problem zu lösen.
Erstellen eines Handelsadministrator-Panels in MQL5 (Teil X): Externe, ressourcenbasierte Schnittstelle Erstellen eines Handelsadministrator-Panels in MQL5 (Teil X): Externe, ressourcenbasierte Schnittstelle
Heute machen wir uns die Möglichkeiten von MQL5 zunutze, um externe Ressourcen - wie Bilder im BMP-Format - zu nutzen und eine einzigartig gestaltete Nutzeroberfläche für das Trading Administrator Panel zu erstellen. Die hier gezeigte Strategie ist besonders nützlich, wenn mehrere Ressourcen, einschließlich Bilder, Töne und mehr, für eine rationelle Verteilung zusammengefasst werden. Nehmen Sie an dieser Diskussion teil und erfahren Sie, wie diese Funktionen implementiert werden, um eine moderne und visuell ansprechende Oberfläche für unser New_Admin_Panel EA zu schaffen.