English Русский 日本語
preview
Erstellen benutzerdefinierter Indikatoren in MQL5 (Teil 7): Hybride Time-Price-Opportunity-(TPO)-Marktprofile für die Analyse von Handelssitzungen

Erstellen benutzerdefinierter Indikatoren in MQL5 (Teil 7): Hybride Time-Price-Opportunity-(TPO)-Marktprofile für die Analyse von Handelssitzungen

MetaTrader 5Handelssysteme |
19 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Einführung

In unserem vorherigen Artikel (Teil 6) haben wir ein weiterentwickeltes Berechnungssystem für den Relative Strength Index in MetaQuotes Language 5 (MQL5) entwickelt, das ein Glättungsverfahren, Farbverschiebungen zur visuellen Verbesserung sowie die Unterstützung mehrerer Zeitrahmen für eine umfassende Marktanalyse umfasst. In Teil 7 entwickeln wir den hybriden Marktprofil-Indikator Time-Price-Opportunity (TPO), der verschiedene Zeitrahmen für Handelssitzungen unterstützt, darunter Intraday, täglich, wöchentlich, monatlich sowie feste Zeitintervalle mit Zeitzonenanpassungen. Dieser Indikator unterteilt die Kurse in ein Raster, verwaltet Sitzungsdaten zu Höchst-, Tiefst-, Eröffnungs- und Schlusskursen, berechnet den Point of Control (POC) und den Wertebereich (value area) anhand von TPO-Zählwerten und stellt die Ergebnisse im Chart mit anpassbaren Farben dar, um eine detaillierte Analyse von Handelssitzungen zu ermöglichen. Wir werden die folgenden Themen behandeln:

  1. Eine Untersuchung des Konzepts eines hybriden TPO-Marktprofil-Indikator
  2. Implementierung in MQL5
  3. Backtests
  4. Schlussfolgerung

Am Ende werden Sie über einen funktionsfähigen hybriden TPO-Marktprofil-Indikator verfügen, der sich ganz nach Ihren Wünschen anpassen lässt – legen wir los!


Eine Untersuchung des Konzepts eines hybriden TPO-Marktprofil-Indikator

Das hybride Time Price Opportunity (TPO)-Marktprofil ist ein Visualisierungstool, das die Kursverteilung über die Zeit innerhalb definierter Handelssitzungen abbildet. Dabei werden Buchstaben (letters), Rechteckmarkierungen oder einfach Punkte verwendet, um Zeitintervalle auf bestimmten Kursniveaus darzustellen, wodurch Bereiche mit hoher Aktivität wie der Wertebereich und der Point of Control sichtbar werden, in denen der Großteil des Handels stattfand. Dieser Ansatz hilft uns dabei, Unterstützungs-, Widerstands- und Fair-Value-Zonen zu identifizieren, indem die Kursentwicklung in einem Profilhistogramm zusammengefasst wird, wobei dichtere TPO-Ansammlungen auf einen ausgeglichenen Handel hindeuten und weniger dichte Bereiche auf mögliche Ausbrüche oder Ungleichgewichte deuten. Typischerweise wenden wir sie sitzungsübergreifend an, um die Marktstimmung einzuschätzen, Positionen nahe den Rändern von Wertebereichen einzugehen oder Verschiebungen am Point of Control im Hinblick auf Trendfortsetzungen zu beobachten.

Wir planen, Sitzungen auf der Grundlage ausgewählter Zeiträume unter Berücksichtigung von Zeitzonenanpassungen festzulegen. Wir diskretisieren die Preise in einem Raster und erfassen Sitzungskennzahlen wie Höchst- und Tiefstwerte . Wir berechnen den Point of Control als das Kursniveau mit der höchsten TPO-Anzahl. Als Nächstes leiten wir den Wertebereich ab, der einen festgelegten Prozentsatz der gesamten TPOs abdeckt. Abschließend stellen wir das Profil mithilfe von farbcodierten Beschriftungen, Punkten und Quadraten dar, um die Chartanalyse zu erleichtern. Kurz gesagt: Hier ist eine visuelle Darstellung unserer Ziele.

ARCHITEKTUR DES TPO-MARKTPROFILS


Implementierung in MQL5

Um den Indikator in MQL5 zu erstellen, öffnen Sie einfach den MetaEditor, gehen zum Navigator, suchen den Ordner Indikatoren, klicken auf die Registerkarte Neu und folgen den Anweisungen, um die Datei zu erstellen. Sobald der Indikator erstellt ist, legen wir in der Programmierumgebung die Eigenschaften und Einstellungen des Indikators fest, wie beispielsweise die Anzahl der Puffer und Plots.

//+------------------------------------------------------------------+
//|                             Hybrid TPO Market Profile PART 1.mq5 |
//|                           Copyright 2026, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Allan Munene Mutiiria."
#property link "https://t.me/Forex_Algo_Trader"
#property version "1.00"
#property strict

#property indicator_chart_window
#property indicator_buffers 0
#property indicator_plots 0

//+------------------------------------------------------------------+
//| Enums                                                            |
//+------------------------------------------------------------------+
enum MarketProfileTimeframe { // Define market profile timeframe enum
   INTRADAY,                  // Intraday
   DAILY,                     // Daily
   WEEKLY,                    // Weekly
   MONTHLY,                   // Monthly
   FIXED                      // Fixed
};

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
sinput group "Settings"
input double ticksPerTpoLetter = 10;             // Ticks per letter
input int valueAreaPercent = 70;                 // Value Area Percent

sinput group "Time"
input MarketProfileTimeframe profileTimeframe = DAILY;    // Timeframe
input string timezone = "Exchange";              // Timezone
input string dailySessionRange = "0830-1500";    // Daily session
input int intradayProfileLengthMinutes = 60;     // Profile length in minutes (Intraday)
input datetime fixedTimeRangeStart = D'2026.02.01 08:30'; // From (Fixed)
input datetime fixedTimeRangeEnd = D'2026.02.02 15:00';   // Till (Fixed)

sinput group "Rendering"
input int labelFontSize = 10;                   // Font size

sinput group "Colors"
input color defaultTpoColor = clrGray;          // Default
input color singlePrintColor = 0xd56a6a;        // Single Print
input color valueAreaColor = clrBlack;          // Value Area
input color pointOfControlColor = 0x3f7cff;     // POC
input color closeColor = clrRed;                // Close

//+------------------------------------------------------------------+
//| Constants                                                        |
//+------------------------------------------------------------------+
#define MAX_BARS_BACK 5000
#define TPO_CHARACTERS_STRING "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"

//+------------------------------------------------------------------+
//| Structures                                                       |
//+------------------------------------------------------------------+
struct TpoPriceLevel {       // Define TPO price level structure
   double price;             // Store price level
   string tpoString;         // Store TPO string
   int tpoCount;             // Store TPO count
};

struct ProfileSessionData {  // Define profile session data structure
   datetime startTime;       // Store start time
   datetime endTime;         // Store end time
   double sessionOpen;       // Store session open price
   double sessionClose;      // Store session close price
   double sessionHigh;       // Store session high price
   double sessionLow;        // Store session low price
   TpoPriceLevel levels[];   // Store array of price levels
   int periodCount;          // Store period count
   double periodOpens[];     // Store array of period opens
   int pointOfControlIndex;  // Store point of control index
};

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
string objectPrefix = "HTMP_";     //--- Set object prefix
ProfileSessionData sessions[];     //--- Declare sessions array
int activeSessionIndex = -1;       //--- Initialize active session index
double tpoPriceGridStep = 0;       //--- Initialize TPO price grid step
string tpoCharacterSet[];          //--- Declare TPO character set array
datetime previousBarTime = 0;      //--- Initialize previous bar time
datetime lastCompletedBarTime = 0; //--- Initialize last completed bar time
int maxSessionHistory = 20;        //--- Set maximum session history
int timezoneOffsetSeconds = 0;     //--- Initialize timezone offset in seconds

Wir beginnen die Implementierung damit, die Eigenschaften des Indikators mithilfe der Direktive #property zu konfigurieren, ihn über indicator_chart_window so einzustellen, dass er im Hauptchart angezeigt wird, und die Anzahl der Puffer und Plots auf null festzulegen, da sich dieser Indikator auf die Darstellung benutzerdefinierter Objekte konzentriert und nicht auf gezeichnete Linien oder Histogramme. Als Nächstes definieren wir die Enumeration MarketProfileTimeframe, um die Auswahl von Profilzeiträumen zu ermöglichen, darunter Optionen wie INTRADAY für kurzfristige Sitzungen, DAILY für Standard-Handelstage, WEEKLY, MONTHLY und FIXED für benutzerdefinierte Zeiträume, was Flexibilität bei der Analyse von Handelssitzungen bietet.

Anschließend definieren wir die Benutzereingabeparameter, die in Abschnitten mit Eingabegruppen zusammengefasst sind, beginnend mit den Einstellungen für ticksPerTpoLetter zur Steuerung der Kursauflösung pro Time Price Opportunity Buchstaben sowie valueAreaPercent, um den Prozentsatz der gesamten Time Price Opportunities festzulegen, die den Wertebereich definieren. In der Zeitgruppe umfassen die Eingaben die Auswahl des Profilzeitraums, die Zeitzonenangabe für Zeitverschiebungsanpassungen, den täglichen Handelszeitraum als Zeichenfolge wie 0830-1500, die Länge des Intraday-Profils in Minuten sowie Datums- und Zeitangaben für den Beginn und das Ende des festen Zeitraums. Die Rendering-Gruppe fügt labelFontSize für die Textanzeige hinzu, während die Farbgruppe anpassbare Farben definiert, wie beispielsweise defaultTpoColor für Standardbuchstaben, singlePrintColor für isolierte Prints sowie valueAreaColor, pointOfControlColor und closeColor, um Profilelemente optisch voneinander zu unterscheiden.

Um ein einheitliches Vorgehen zu gewährleisten, führen wir über #define Konstanten ein, darunter MAX_BARS_BACK zur Begrenzung der Verarbeitung historischer Bars und TPO_CHARACTERS_STRING als alphabetische Reihenfolge für die Zuordnung von Buchstaben zu den TPO-Perioden. Wir erstellen zwei Strukturen zur Datenorganisation: TpoPriceLevel zur Speicherung einzelner Preisniveau-Details mit Feldern für den Preis, einer Zeichenkette aus Time Price Opportunity-Zeichen und der Anzahl sowie ProfileSessionData zur Verwaltung sitzungsweiter Informationen, darunter Start- und Endzeitpunkte, Eröffnungs-, Schluss-, Höchst- und Tiefstkurse, ein Array von Kursniveaus, die Anzahl der Perioden, ein Array der Eröffnungskurse der Perioden und der Index für den Point of Control.

Abschließend initialisieren wir globale Variablen, die für die Laufzeitverwaltung unerlässlich sind, wie beispielsweise objectPrefix zur Benennung von Chartobjekten, ein Array sessions zur Speicherung von Profildaten, activeSessionIndex, beginnend bei -1, zur Verfolgung der aktuellen Sitzung, tpoPriceGridStep zur Preisquantisierung, das Array tpoCharacterSet für die Zuordnung von Buchstaben, Zeitstempel wie previousBarTime und lastCompletedBarTime, maxSessionHistory, um die Anzahl der gespeicherten Sitzungen auf 20 zu begrenzen, sowie timezoneOffsetSeconds für Zeitanpassungen. Nachdem das erledigt ist, können wir den Indikator initialisieren.

//+------------------------------------------------------------------+
//| Initialize custom indicator                                      |
//+------------------------------------------------------------------+
int OnInit() {
   IndicatorSetString(INDICATOR_SHORTNAME, "Hybrid TPO Market Profile - Part 1"); //--- Set indicator short name
   
   tpoPriceGridStep = ticksPerTpoLetter * _Point;  //--- Calculate TPO price grid step
   
   ArrayResize(tpoCharacterSet, 52);               //--- Resize TPO character set array
   for(int i = 0; i < 52; i++) {                   //--- Loop through characters
      tpoCharacterSet[i] = StringSubstr(TPO_CHARACTERS_STRING, i, 1); //--- Assign character to array
   }
   
   if(timezone != "Exchange") {                    //--- Check if timezone is not exchange
      string tzString = StringSubstr(timezone, 3); //--- Extract timezone string
      int offset = (int)StringToInteger(tzString); //--- Convert offset to integer
      timezoneOffsetSeconds = offset * 3600;       //--- Calculate timezone offset in seconds
   }
   
   ArrayResize(sessions, 0);                       //--- Resize sessions array to zero
   
   return(INIT_SUCCEEDED);                         //--- Return initialization success
}

Wir setzen die Implementierung mit OnInit fort, das ausgeführt wird, wenn der Indikator an ein Chart angehängt wird, und nehmen dabei wichtige Konfigurationen für das hybride Time Price Opportunity-Marktprofil vor. Zunächst weisen wir dem Indikator mithilfe der Funktion IndicatorSetString und dem Parameter INDICATOR_SHORTNAME einen Kurznamen zu, damit in der Benutzeroberfläche der Plattform Hybrid TPO Market Profile – Teil 1 angezeigt wird.

Anschließend berechnen wir die Schrittweite des Preisrasters, indem wir den vom Benutzer eingegebenen Wert ticksPerTpoLetter mit dem Punktwert _Point des Symbols multiplizieren und das Ergebnis in tpoPriceGridStep speichern, um den vertikalen Abstand für die Buchstaben der Time-Price-Opportunity auf der Grundlage der Preisinkremente zu bestimmen. Anschließend bereiten wir den Zeichensatz für die Kennzeichnung von Time Price Opportunities vor, indem wir die Größe des Arrays tpoCharacterSet auf 52 Elemente setzen und es in einer Schleife füllen, wobei wir jeden Buchstaben mithilfe von StringSubstr aus der Konstante TPO_CHARACTERS_STRING extrahieren, um sowohl Groß- als auch Kleinbuchstaben für die Erkennung von Zeiträumen zu berücksichtigen.

Um Zeitanpassungen zu berücksichtigen, prüfen wir, ob sich der Eingabewert timezone von Exchange unterscheidet; ist dies der Fall, extrahieren wir den Offset-Teil ab dem vierten Zeichen mit StringSubstr, wandeln ihn mit StringToInteger in eine Ganzzahl um und berechnen den Wert timezoneOffsetSeconds, indem wir den Offset mit 3600 multiplizieren, um die Stunden in Sekunden darzustellen. Wir setzen das Array sessions zurück, indem wir seine Größe mit ArrayResize auf Null ändern und alle bisherigen Daten löschen, um einen Neuanfang für neue Profilsitzungen zu ermöglichen. Abschließend geben wir INIT_SUCCEEDED zurück, um der Plattform die erfolgreiche Initialisierung zu signalisieren. Wir müssen die Berechnungen für die Indikatoren nun pro Tick durchführen. Um unseren Code modular und leicht wartbar zu gestalten, werden wir die Logik in Hilfsfunktionen organisieren. Beginnen wir damit, die Sitzung anzulegen und die Sitzungszeiträume zu analysieren.

//+------------------------------------------------------------------+
//| Create new session                                               |
//+------------------------------------------------------------------+
int CreateNewSession() {
   int size = ArraySize(sessions);                //--- Get size of sessions array
   
   if(size >= maxSessionHistory) {                //--- Check if size exceeds history limit
      for(int i = 0; i < size - 1; i++) {         //--- Loop to shift sessions
         sessions[i] = sessions[i + 1];           //--- Copy next session to current
      }
      ArrayResize(sessions, size - 1);            //--- Resize sessions array
      size = size - 1;                            //--- Update size
   }
   
   ArrayResize(sessions, size + 1);               //--- Resize sessions array for new session
   int newIndex = size;                           //--- Set new index
   
   sessions[newIndex].startTime = 0;              //--- Initialize start time
   sessions[newIndex].endTime = 0;                //--- Initialize end time
   sessions[newIndex].sessionOpen = 0;            //--- Initialize session open
   sessions[newIndex].sessionClose = 0;           //--- Initialize session close
   sessions[newIndex].sessionHigh = 0;            //--- Initialize session high
   sessions[newIndex].sessionLow = 0;             //--- Initialize session low
   sessions[newIndex].periodCount = 0;            //--- Initialize period count
   sessions[newIndex].pointOfControlIndex = -1;   //--- Initialize point of control index
   ArrayResize(sessions[newIndex].levels, 0);     //--- Resize levels array
   ArrayResize(sessions[newIndex].periodOpens, 0);//--- Resize period opens array
   
   return newIndex;                               //--- Return new index
}

//+------------------------------------------------------------------+
//| Quantize price to grid                                           |
//+------------------------------------------------------------------+
double QuantizePriceToGrid(double price) {
   return MathRound(price / tpoPriceGridStep) * tpoPriceGridStep;   //--- Calculate and return quantized price
}

//+------------------------------------------------------------------+
//| Parse daily session time range                                   |
//+------------------------------------------------------------------+
bool ParseDailySessionTimeRange(int &startHour, int &startMinute, int &endHour, int &endMinute) {
   string parts[];                                                   //--- Declare parts array
   int count = StringSplit(dailySessionRange, '-', parts);           //--- Split daily session range
   if(count != 2) return false;                                      //--- Return false if invalid count
   
   startHour = (int)StringToInteger(StringSubstr(parts[0], 0, 2));   //--- Parse start hour
   startMinute = (int)StringToInteger(StringSubstr(parts[0], 2, 2)); //--- Parse start minute
   endHour = (int)StringToInteger(StringSubstr(parts[1], 0, 2));     //--- Parse end hour
   endMinute = (int)StringToInteger(StringSubstr(parts[1], 2, 2));   //--- Parse end minute
   
   return true;                                                      //--- Return true
}

Zunächst definieren wir die Funktion CreateNewSession, um dem Array eine neue Profilsitzung hinzuzufügen. Dazu ermitteln wir zunächst mit der Funktion ArraySize die aktuelle Größe. Wenn der Wert den maximalen Historiengrenzwert erreicht oder überschreitet, verschieben wir die bestehenden Sitzungen in einer Schleife nach vorn, um die älteste zu entfernen, und verkleinern anschließend das Array mithilfe von ArrayResize, bevor wir die Größe aktualisieren. Als Nächstes erhöhen wir die Größe des Arrays um eins, um die neue Sitzung aufzunehmen, legen deren Index fest und initialisieren alle Felder mit Standardwerten wie Null oder -1, wobei wir auch die Arrays für die Levels und die Eröffnungskurse der Perioden auf leer zurücksetzen. Um sicherzustellen, dass die Preise mit dem Time Price Opportunity-Raster übereinstimmen, setzen wir die Funktion QuantizePriceToGrid ein, die den Eingabepreis durch die Rasterschrittweite teilt, ihn mithilfe der Funktion MathRound rundet und anschließend wieder multipliziert, um ihn auf den nächsten Rasterpunkt auszurichten.

Zur Verarbeitung der täglichen Sitzungen zerlegt die Funktion ParseDailySessionTimeRange die eingegebene Zeitangabe mithilfe der Funktion StringSplit und unter Verwendung eines Bindestrichs als Trennzeichen in einzelne Teile. Werden genau zwei Teile gefunden, extrahieren wir mit StringSubstr und StringToInteger die Stunden und Minuten aus jedem Teil und weisen sie den Referenzparametern zu; andernfalls wird false zurückgegeben, um einen Fehler beim Parsen anzuzeigen. Als Nächstes benötigen wir Funktionen, um die Bars innerhalb der Handelszeit zu filtern und die Kursniveaus innerhalb einer Sitzung zu verwalten.

//+------------------------------------------------------------------+
//| Check if bar is within daily session                             |
//+------------------------------------------------------------------+
bool IsBarWithinDailySession(datetime barTime) {
   if(profileTimeframe != DAILY) return true;                       //--- Return true if not daily timeframe
   
   int startHour, startMinute, endHour, endMinute;                  //--- Declare time variables
   if(!ParseDailySessionTimeRange(startHour, startMinute, endHour, endMinute)) return true; //--- Parse and return true if fail
   
   MqlDateTime dateTimeStruct;                                      //--- Declare date time struct
   TimeToStruct(barTime + timezoneOffsetSeconds, dateTimeStruct);   //--- Convert time to struct
   
   int barMinutes = dateTimeStruct.hour * 60 + dateTimeStruct.min;  //--- Calculate bar minutes
   int startMinutes = startHour * 60 + startMinute;                 //--- Calculate start minutes
   int endMinutes = endHour * 60 + endMinute;                       //--- Calculate end minutes
   
   if(endMinutes > startMinutes) {                                   //--- Check if end after start
      return barMinutes >= startMinutes && barMinutes <= endMinutes; //--- Return if within range
   } else {                                       //--- Handle overnight case
      return barMinutes >= startMinutes || barMinutes <= endMinutes; //--- Return if within range
   }
}

//+------------------------------------------------------------------+
//| Check if new session started                                     |
//+------------------------------------------------------------------+
bool IsNewSessionStarted(datetime currentTime, datetime previousTime) {
   if(previousTime == 0) return true;                                 //--- Return true if no previous time
   
   datetime adjustedCurrent = currentTime + timezoneOffsetSeconds;    //--- Adjust current time
   datetime adjustedPrevious = previousTime + timezoneOffsetSeconds;  //--- Adjust previous time
   
   MqlDateTime currentDateTime, previousDateTime;                     //--- Declare date time structs
   TimeToStruct(adjustedCurrent, currentDateTime);                    //--- Convert current to struct
   TimeToStruct(adjustedPrevious, previousDateTime);                  //--- Convert previous to struct
   
   switch(profileTimeframe) {                                         //--- Switch on profile timeframe
      case DAILY: {                                                   //--- Handle daily case
         int startHour, startMinute, endHour, endMinute;              //--- Declare time variables
         if(!ParseDailySessionTimeRange(startHour, startMinute, endHour, endMinute)) return false; //--- Parse and return false if fail
         
         datetime sessionStart = StringToTime(TimeToString(adjustedCurrent, TIME_DATE) + " " + 
                                              IntegerToString(startHour, 2, '0') + ":" + 
                                              IntegerToString(startMinute, 2, '0')); //--- Calculate session start
         datetime prevSessionStart = StringToTime(TimeToString(adjustedPrevious, TIME_DATE) + " " + 
                                                   IntegerToString(startHour, 2, '0') + ":" + 
                                                   IntegerToString(startMinute, 2, '0')); //--- Calculate previous session start
         
         return adjustedCurrent >= sessionStart && adjustedPrevious < prevSessionStart; //--- Return if new session
      }
      
      case WEEKLY:                                                     //--- Handle weekly case
         return currentDateTime.day_of_week < previousDateTime.day_of_week || 
                currentDateTime.day_of_year < previousDateTime.day_of_year; //--- Return if new week
      
      case MONTHLY:                                                    //--- Handle monthly case
         return currentDateTime.mon != previousDateTime.mon;           //--- Return if new month
      
      case FIXED:                                                      //--- Handle fixed case
         return currentTime >= fixedTimeRangeStart && previousTime < fixedTimeRangeStart; //--- Return if new fixed range
      
      case INTRADAY: {                                                 //--- Handle intraday case
         long currentMinute = (adjustedCurrent / 60) * 60;             //--- Calculate current minute
         long prevMinute = (adjustedPrevious / 60) * 60;               //--- Calculate previous minute
         return (currentMinute % (intradayProfileLengthMinutes * 60)) == 0 && 
                currentMinute != prevMinute;                           //--- Return if new intraday profile
      }
   }
   
   return false;                                                       //--- Return false
}

//+------------------------------------------------------------------+
//| Check if bar is eligible for processing                          |
//+------------------------------------------------------------------+
bool IsBarEligibleForProcessing(datetime barTime) {
   if(profileTimeframe == FIXED) {                                    //--- Check fixed timeframe
      return barTime >= fixedTimeRangeStart && barTime <= fixedTimeRangeEnd; //--- Return if within fixed range
   }
   
   if(profileTimeframe == DAILY) {                                   //--- Check daily timeframe
      return IsBarWithinDailySession(barTime);                       //--- Return if within daily session
   }
   
   return true;                                                      //--- Return true
}

//+------------------------------------------------------------------+
//| Get or create price level                                        |
//+------------------------------------------------------------------+
int GetOrCreatePriceLevel(int sessionIndex, double price) {
   if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return -1; //--- Return invalid if index out of range
   
   int size = ArraySize(sessions[sessionIndex].levels);              //--- Get levels size
   
   for(int i = 0; i < size; i++) {                                   //--- Loop through levels
      if(MathAbs(sessions[sessionIndex].levels[i].price - price) < _Point / 2) //--- Check if price matches
         return i;                                                   //--- Return index
   }
   
   ArrayResize(sessions[sessionIndex].levels, size + 1);             //--- Resize levels array
   sessions[sessionIndex].levels[size].price = price;                //--- Set new price
   sessions[sessionIndex].levels[size].tpoString = "";               //--- Initialize TPO string
   sessions[sessionIndex].levels[size].tpoCount = 0;                 //--- Initialize TPO count
   
   return size;                                                      //--- Return new index
}

Anschließend implementieren wir die Funktion IsBarWithinDailySession, um festzustellen, ob eine bestimmte Bar innerhalb der festgelegten täglichen Handelszeiten liegt, wobei für nicht-tägliche Zeitrahmen sofort true zurückgegeben wird. Wenn das Parsen des Sitzungszeitraums über die Funktion ParseDailySessionTimeRange fehlschlägt, wird standardmäßig true gesetzt; andernfalls konvertieren wir die angepasste Barzeit mithilfe von TimeToStruct in eine MqlDateTime-Struktur, berechnen die Gesamtminuten für die Start- und Endzeit der Bar und prüfen, ob die Minuten der Bar innerhalb des Bereichs liegen, wobei wir sowohl Standard- als auch Übernacht-Sitzungen mit bedingter Logik behandeln.

Anschließend prüft die Funktion IsNewSessionStarted den Beginn einer neuen Profilsitzung, indem sie die angepasste aktuelle Zeit mit der vorherigen Zeit vergleicht, und gibt true zurück, wenn keine vorherige Zeit vorliegt. Wir passen die Zeitstempel an den Zeitzonenversatz an, wandeln sie in MqlDateTime-Strukturen um und verwenden eine switch-Anweisung für den Profilzeitrahmen: Bei täglich wird der Sitzungszeitraum analysiert und die Startzeiten werden mit StringToTime, TimeToString und IntegerToString umgewandelt, um den Übergang in einen neuen Tag zu überprüfen; bei wöchentlichen Profilen werden Wochentag und Jahr verglichen; bei monatlichen Profilen werden Monatsunterschiede geprüft; bei festen Profilen erfolgt ein Vergleich mit der Startangabe; und bei intraday-Profilen wird überprüft, ob die aktuelle Minute mit dem Modulo der Profillänge übereinstimmt, ohne mit der vorherigen übereinzustimmen.

Um die zu berücksichtigenden Bars zu filtern, definieren wir die Funktion IsBarEligibleForProcessing, die für festgelegte Zeiträume prüft, ob der Zeitpunkt der Bar zwischen den Start- und Endwerten liegt, für tägliche Abfragen die Funktion IsBarWithinDailySession verwendet und ansonsten für alle Bars den Wert true zurückgibt. Schließlich verwaltet die Funktion GetOrCreatePriceLevel die Preisstufen innerhalb einer Sitzung, indem sie den Sitzungsindex mithilfe von ArraySize anhand der Größe des Sitzungs-Arrays überprüft und im Falle einer Ungültigkeit den Wert -1 zurückgibt. Die Funktion durchläuft die vorhandenen Niveaus, um mithilfe von MathAbs mit einer Toleranz von einem halben Punkt eine möglichst genaue Übereinstimmung zu finden, und gibt den Index zurück, falls eine Übereinstimmung gefunden wird; andernfalls passt sie die Größe des Niveaus-Arrays an, initialisiert den Preis des neuen Niveaus, setzt die Zeichenkette Time Price Opportunity auf leer und setzt den Zähler auf Null, bevor sie den neuen Index zurückgibt. Da wir die Buchstaben für die Profile zunächst in einer bestimmten Reihenfolge darstellen möchten, müssen wir einen Bubble-Sort-Algorithmus definieren, um die Buchstaben vor der Darstellung entsprechend anzuordnen.

//+------------------------------------------------------------------+
//| Add TPO character to level                                       |
//+------------------------------------------------------------------+
void AddTpoCharacterToLevel(int sessionIndex, int levelIndex, int periodIndex) {
   if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return;                  //--- Return if session index invalid
   if(levelIndex < 0 || levelIndex >= ArraySize(sessions[sessionIndex].levels)) return; //--- Return if level index invalid
   
   string tpoCharacter = tpoCharacterSet[periodIndex % 52];                             //--- Get TPO character
   
   sessions[sessionIndex].levels[levelIndex].tpoString += tpoCharacter;                 //--- Append character to TPO string
   sessions[sessionIndex].levels[levelIndex].tpoCount++;                                //--- Increment TPO count
}

//+------------------------------------------------------------------+
//| Sort price levels descending                                     |
//+------------------------------------------------------------------+
void SortPriceLevelsDescending(int sessionIndex) {
   if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return;                 //--- Return if index invalid
   
   int size = ArraySize(sessions[sessionIndex].levels);                                //--- Get levels size
   
   for(int i = 0; i < size - 1; i++) {                                                 //--- Outer loop for sorting
      for(int j = 0; j < size - i - 1; j++) {                                          //--- Inner loop for comparison
         if(sessions[sessionIndex].levels[j].price < sessions[sessionIndex].levels[j + 1].price) { //--- Check if swap needed
            TpoPriceLevel temp = sessions[sessionIndex].levels[j];                     //--- Store temporary level
            sessions[sessionIndex].levels[j] = sessions[sessionIndex].levels[j + 1];   //--- Swap levels
            sessions[sessionIndex].levels[j + 1] = temp;                               //--- Assign temporary back
         }
      }
   }
}

//+------------------------------------------------------------------+
//| Calculate point of control                                       |
//+------------------------------------------------------------------+
void CalculatePointOfControl(int sessionIndex) {
   if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return;                 //--- Return if index invalid
   
   int size = ArraySize(sessions[sessionIndex].levels);                                //--- Get levels size
   if(size == 0) return;                                                               //--- Return if no levels
   
   int maxTpoCount = 0;                                                                //--- Initialize max TPO count
   int pointOfControlIndex = 0;                                                        //--- Initialize POC index
   
   for(int i = 0; i < size; i++) {                                                     //--- Loop through levels
      if(sessions[sessionIndex].levels[i].tpoCount > maxTpoCount) {                    //--- Check if higher TPO count
         maxTpoCount = sessions[sessionIndex].levels[i].tpoCount;                      //--- Update max TPO count
         pointOfControlIndex = i;                                                      //--- Update POC index
      }
   }
   
   sessions[sessionIndex].pointOfControlIndex = pointOfControlIndex;                   //--- Set POC index
}

//+------------------------------------------------------------------+
//| Get total TPO count                                              |
//+------------------------------------------------------------------+
int GetTotalTpoCount(int sessionIndex) {
   if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return 0;               //--- Return zero if index invalid
   
   int total = 0;                                                                      //--- Initialize total
   int size = ArraySize(sessions[sessionIndex].levels);                                //--- Get levels size
   
   for(int i = 0; i < size; i++) {                                                     //--- Loop through levels
      total += sessions[sessionIndex].levels[i].tpoCount;                              //--- Accumulate TPO count
   }
   
   return total;                                                                       //--- Return total
}

Zunächst implementieren wir die Funktion AddTpoCharacterToLevel, um den bestimmten Kursniveaus Buchstaben für Time Price Opportunity zuzuweisen. Dabei überprüfen wir zunächst die Indizes für die Sitzung und das Niveau anhand der Array-Größen mit ArraySize, um Fehler zu vermeiden, und brechen den Vorgang vorzeitig ab, falls diese ungültig sind. Wir entnehmen dem Satz das entsprechende Zeichen, indem wir den Periodenindex mit Modulo 52 berechnen, fügen es an die Zeichenkette der Stufe an und erhöhen den Zählwert, um die Aktivität bei diesem Kurs zu verfolgen. Um die Preisstufen vom höchsten zum niedrigsten Preis zu sortieren, überprüft die Funktion SortPriceLevelsDescending die Gültigkeit des Sitzungsindex, ermittelt die Größe der Preisstufen und wendet in verschachtelten Schleifen einen Blasensortieralgorithmus an, bei dem benachbarte Elemente vertauscht werden, wenn der aktuelle Preis niedriger ist als der des nächsten Elements. Dabei wird eine temporäre TpoPriceLevel-Struktur für den Austausch verwendet.

Die Funktion CalculatePointOfControl ermittelt das Kursniveau mit den meisten Time Price Opportunities, indem sie die Sitzung validiert, die maximale Anzahl und den Index auf Null setzt und die einzelnen Niveaus durchläuft, um diese zu aktualisieren, sobald eine höhere Anzahl gefunden wird, und schließlich den Index in den Sitzungsdaten speichert. Wir fügen die Funktion GetTotalTpoCount hinzu, um alle Time Price Opportunity-Zählwerte über alle Ebenen einer Sitzung hinweg zu summieren. Bei einem ungültigen Index gibt die Funktion Null zurück; andernfalls initialisiert sie eine Gesamtsumme und sammelt die Zählwerte in einer Schleife, bevor sie das Aggregat für Berechnungen im Wertebereich zurückgibt. Wir können nun mit der Logik zur visuellen Darstellung dieser Komponenten des Marktprofils beginnen. Zunächst definieren wir die Logik für die Darstellung der TPO-Highlight-Markierung für den Schlusskurs sowie der Markierungen für Kurseröffnung und -schluss.

//+------------------------------------------------------------------+
//| Render close TPO highlight                                       |
//+------------------------------------------------------------------+
void RenderCloseTpoHighlight(int sessionIndex, int closeLevelIndex, string &displayStrings[]) {
   if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return;                              //--- Return if session invalid
   if(closeLevelIndex < 0 || closeLevelIndex >= ArraySize(sessions[sessionIndex].levels)) return;   //--- Return if level invalid
   
   string fullString = displayStrings[closeLevelIndex];                                             //--- Get full display string
   int stringLength = StringLen(fullString);                                                        //--- Get string length
   if(stringLength == 0) return;                                                                    //--- Return if empty string
   
   string closeCharacter = StringSubstr(fullString, stringLength - 1, 1);                           //--- Extract close character
   string remainingCharacters = StringSubstr(fullString, 0, stringLength - 1);                      //--- Extract remaining characters
   
   string objectName = objectPrefix + "CloseTPO_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create object name
   int barIndex = iBarShift(_Symbol, _Period, sessions[sessionIndex].startTime);                    //--- Get bar index
   if(barIndex < 0) return;                                                                         //--- Return if invalid bar index
   
   datetime labelTime = iTime(_Symbol, _Period, barIndex);                                          //--- Get label time
   int x, y;                                                                                        //--- Declare coordinates
   ChartTimePriceToXY(0, 0, labelTime, sessions[sessionIndex].levels[closeLevelIndex].price, x, y); //--- Convert to XY
   
   int characterWidth = 8;                                                                          //--- Set character width
   int offsetX = (stringLength - 1) * characterWidth;                                               //--- Calculate offset X
   
   if(ObjectFind(0, objectName) < 0) {                                                              //--- Check if object not found
      ObjectCreate(0, objectName, OBJ_LABEL, 0, 0, 0);                                              //--- Create label object
   }
   
   ObjectSetInteger(0, objectName, OBJPROP_XDISTANCE, x + offsetX);                                 //--- Set X distance
   ObjectSetInteger(0, objectName, OBJPROP_YDISTANCE, y);                                           //--- Set Y distance
   ObjectSetInteger(0, objectName, OBJPROP_CORNER, CORNER_LEFT_UPPER);                              //--- Set corner
   ObjectSetInteger(0, objectName, OBJPROP_ANCHOR, ANCHOR_LEFT);                                    //--- Set anchor
   ObjectSetInteger(0, objectName, OBJPROP_COLOR, closeColor);                                      //--- Set color
   ObjectSetInteger(0, objectName, OBJPROP_FONTSIZE, labelFontSize);                                //--- Set font size
   ObjectSetString(0, objectName, OBJPROP_FONT, "Arial");                                           //--- Set font
   ObjectSetString(0, objectName, OBJPROP_TEXT, closeCharacter + "◄");                              //--- Set text
   ObjectSetInteger(0, objectName, OBJPROP_SELECTABLE, false);                                      //--- Set selectable false
   ObjectSetInteger(0, objectName, OBJPROP_HIDDEN, true);                                           //--- Set hidden true
   
   displayStrings[closeLevelIndex] = remainingCharacters;                                           //--- Update display string
}

//+------------------------------------------------------------------+
//| Render open close markers                                        |
//+------------------------------------------------------------------+
void RenderOpenCloseMarkers(int sessionIndex, int barIndex) {
   if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return;                              //--- Return if index invalid
   if(sessions[sessionIndex].sessionOpen == 0) return;                                              //--- Return if no open
   
   datetime startTime = iTime(_Symbol, _Period, barIndex);                                          //--- Get start time
   
   string openObjectName = objectPrefix + "Open_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create open object name
   if(ObjectFind(0, openObjectName) < 0) {                                                          //--- Check if not found
      ObjectCreate(0, openObjectName, OBJ_TREND, 0, startTime, sessions[sessionIndex].sessionOpen, 
                   startTime, sessions[sessionIndex].sessionOpen);                                  //--- Create trend object
      ObjectSetInteger(0, openObjectName, OBJPROP_RAY_RIGHT, false);                                //--- Set ray right false
      ObjectSetInteger(0, openObjectName, OBJPROP_SELECTABLE, false);                               //--- Set selectable false
      ObjectSetInteger(0, openObjectName, OBJPROP_HIDDEN, true);                                    //--- Set hidden true
   }
   ObjectSetInteger(0, openObjectName, OBJPROP_COLOR, clrDodgerBlue);                               //--- Set color
   ObjectSetInteger(0, openObjectName, OBJPROP_WIDTH, 2);                                           //--- Set width
   
   string closeObjectName = objectPrefix + "Close_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create close object name
   if(ObjectFind(0, closeObjectName) < 0) {                                                         //--- Check if not found
      ObjectCreate(0, closeObjectName, OBJ_TREND, 0, startTime, sessions[sessionIndex].sessionClose, 
                   startTime, sessions[sessionIndex].sessionClose);                                 //--- Create trend object
      ObjectSetInteger(0, closeObjectName, OBJPROP_RAY_RIGHT, false);                               //--- Set ray right false
      ObjectSetInteger(0, closeObjectName, OBJPROP_SELECTABLE, false);                              //--- Set selectable false
      ObjectSetInteger(0, closeObjectName, OBJPROP_HIDDEN, true);                                   //--- Set hidden true
   }
   ObjectSetInteger(0, closeObjectName, OBJPROP_COLOR, closeColor);                                 //--- Set color
   ObjectSetInteger(0, closeObjectName, OBJPROP_WIDTH, 2);                                          //--- Set width
}

Wir definieren die Funktion RenderCloseTpoHighlight, um das TPO-Zeichen auf dem Schlusskursniveau hervorzuheben. Dabei beginnen wir mit Indexüberprüfungen für die Sitzung und die Ebene und verwenden dabei ArraySize, um ungültige Fälle zu überspringen. Wir rufen die Anzeigezeichenfolge auf der Schließebene ab, berechnen ihre Länge mit StringLen und extrahieren, sofern sie nicht leer ist, das letzte Zeichen mit StringSubstr zur Hervorhebung, während der Rest gespeichert wird.

Ein Objektname wird gebildet, indem das Präfix mit der über IntegerToString konvertierten Startzeit der Sitzung kombiniert wird, und der Barindex wird mit iBarShift ermittelt; ist dieser ungültig, wird die Funktion vorzeitig beendet. Als Nächstes rufen wir die Zeitangabe mithilfe von iTime ab, wandeln den Preis und die Zeit mit ChartTimePriceToXY in Chartkoordinaten um und berechnen einen X-Versatz auf der Grundlage der Zeichenbreite und der Zeichenfolgenlänge. Wenn das Objekt laut ObjectFind nicht existiert, erstellen wir mit ObjectCreate und dem Parameter OBJ_LABEL eine Beschriftung und legen anschließend deren Position, Ecke, Ankerpunkt, Farbe, Schriftgröße, Schriftart, Text (mit angehängter Markierung) sowie die Eigenschaften nicht auswählbar und ausgeblendet mithilfe von ObjectSetInteger und ObjectSetString fest, bevor wir die Anzeigekette aktualisieren, um das markierte Zeichen auszuschließen.

Um die Sitzungsgrenzen darzustellen, implementieren wir die Funktion RenderOpenCloseMarkers, die den Sitzungsindex überprüft und den Schritt überspringt, wenn kein Eröffnungskurs festgelegt ist. Wir ermitteln die Startzeit mit iTime anhand des Barindex und bilden dann auf ähnliche Weise Namen für die Open- und Close-Objekte. Für den Eröffnungsmarker erstellen wir, falls er nicht über ObjectFind gefunden wird, mit ObjectCreate unter Verwendung von OBJ_TREND ein Trendlinienobjekt zum Eröffnungskurs, deaktivieren die Verlängerung des rechten Strahls, machen es nicht auswählbar und unsichtbar und weisen ihm eine blaue Farbe mit einer Breite von 2 zu. Ebenso erstellen oder aktualisieren wir für die Schlussmarkierung eine Trendlinie am Schlusskurs, wobei wir ihr die Farbe des Eingabeschlusskurses und dieselbe Breite zuweisen, um sicherzustellen, dass beide als kurze horizontale Linien im Chart an der Bar des Sitzungsbeginns erscheinen. Wir können nun all diese Logik kombinieren, um das vollständige Marktprofil darzustellen.

//+------------------------------------------------------------------+
//| Render session profile                                           |
//+------------------------------------------------------------------+
void RenderSessionProfile(int sessionIndex) {
   if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return;           //--- Return if index invalid
   
   int size = ArraySize(sessions[sessionIndex].levels);                          //--- Get levels size
   if(size == 0 || sessions[sessionIndex].startTime == 0) return;                //--- Return if no levels or no start time
   
   int barIndex = iBarShift(_Symbol, _Period, sessions[sessionIndex].startTime); //--- Get bar index
   if(barIndex < 0) return;                                                      //--- Return if invalid
   
   SortPriceLevelsDescending(sessionIndex);                                      //--- Sort levels descending
   CalculatePointOfControl(sessionIndex);                                        //--- Calculate POC
   
   int totalTpoCount = GetTotalTpoCount(sessionIndex);                           //--- Get total TPO count
   int pointOfControlIndex = sessions[sessionIndex].pointOfControlIndex;         //--- Get POC index
   
   int valueAreaUpperIndex = pointOfControlIndex; //--- Initialize value area upper index
   int valueAreaLowerIndex = pointOfControlIndex; //--- Initialize value area lower index
   
   if(pointOfControlIndex >= 0) {                 //--- Check valid POC index
      int targetTpoCount = (int)(totalTpoCount * valueAreaPercent / 100.0);      //--- Calculate target TPO count
      int currentTpoCount = sessions[sessionIndex].levels[pointOfControlIndex].tpoCount; //--- Set current TPO count
      
      while(currentTpoCount < targetTpoCount && (valueAreaUpperIndex > 0 || valueAreaLowerIndex < size - 1)) { //--- Loop to expand value area
         int upperTpoCount = (valueAreaUpperIndex > 0) ? sessions[sessionIndex].levels[valueAreaUpperIndex - 1].tpoCount : 0; //--- Get upper TPO count
         int lowerTpoCount = (valueAreaLowerIndex < size - 1) ? sessions[sessionIndex].levels[valueAreaLowerIndex + 1].tpoCount : 0; //--- Get lower TPO count
         
         if(upperTpoCount >= lowerTpoCount && valueAreaUpperIndex > 0) {         //--- Check upper expansion
            valueAreaUpperIndex--;                //--- Decrement upper index
            currentTpoCount += upperTpoCount;     //--- Add upper TPO
         } else if(valueAreaLowerIndex < size - 1) { //--- Check lower expansion
            valueAreaLowerIndex++;                //--- Increment lower index
            currentTpoCount += lowerTpoCount;     //--- Add lower TPO
         } else if(valueAreaUpperIndex > 0) {     //--- Fallback upper expansion
            valueAreaUpperIndex--;                //--- Decrement upper index
            currentTpoCount += upperTpoCount;     //--- Add upper TPO
         } else {                                 //--- Break if no more
            break;                                //--- Exit loop
         }
      }
   }
   
   string displayStrings[];                       //--- Declare display strings array
   ArrayResize(displayStrings, size);             //--- Resize display strings
   for(int i = 0; i < size; i++) {                //--- Loop through levels
      displayStrings[i] = sessions[sessionIndex].levels[i].tpoString; //--- Copy TPO string
   }
   
   int closeLevelIndex = -1;                      //--- Initialize close level index
   double closePrice = sessions[sessionIndex].sessionClose; //--- Get close price
   
   for(int i = 0; i < size; i++) {                //--- Loop to find close level
      if(MathAbs(sessions[sessionIndex].levels[i].price - closePrice) < tpoPriceGridStep / 2) { //--- Check price match
         closeLevelIndex = i;                     //--- Set close level index
         break;                                   //--- Exit loop
      }
   }
   
   RenderCloseTpoHighlight(sessionIndex, closeLevelIndex, displayStrings); //--- Render close highlight
   
   for(int i = 0; i < size; i++) {                                        //--- Loop to render levels
      string objectName = objectPrefix + "TPO_" + IntegerToString(sessions[sessionIndex].startTime) + "_" + IntegerToString(i); //--- Create object name
      
      color textColor = defaultTpoColor;                                  //--- Set default color
      
      if(sessions[sessionIndex].levels[i].tpoCount == 1) {                //--- Check single print
         textColor = singlePrintColor;                                    //--- Set single print color
      }
      
      if(i >= valueAreaUpperIndex && i <= valueAreaLowerIndex) {          //--- Check value area
         textColor = valueAreaColor;                                      //--- Set value area color
      }
      
      if(i == sessions[sessionIndex].pointOfControlIndex) {               //--- Check POC
         textColor = pointOfControlColor;                                 //--- Set POC color
      }
      
      if(ObjectFind(0, objectName) < 0) {                                 //--- Check if object not found
         ObjectCreate(0, objectName, OBJ_LABEL, 0, 0, 0);                 //--- Create label
         ObjectSetInteger(0, objectName, OBJPROP_XDISTANCE, 0);           //--- Set X distance
         ObjectSetInteger(0, objectName, OBJPROP_YDISTANCE, 0);           //--- Set Y distance
      }
      
      datetime labelTime = iTime(_Symbol, _Period, barIndex);             //--- Get label time
      int x, y;                                                           //--- Declare coordinates
      ChartTimePriceToXY(0, 0, labelTime, sessions[sessionIndex].levels[i].price, x, y); //--- Convert to XY
      
      ObjectSetInteger(0, objectName, OBJPROP_XDISTANCE, x);              //--- Set X distance
      ObjectSetInteger(0, objectName, OBJPROP_YDISTANCE, y);              //--- Set Y distance
      ObjectSetInteger(0, objectName, OBJPROP_CORNER, CORNER_LEFT_UPPER); //--- Set corner
      ObjectSetInteger(0, objectName, OBJPROP_ANCHOR, ANCHOR_LEFT);       //--- Set anchor
      ObjectSetInteger(0, objectName, OBJPROP_COLOR, textColor);          //--- Set color
      ObjectSetInteger(0, objectName, OBJPROP_FONTSIZE, labelFontSize);   //--- Set font size
      ObjectSetString(0, objectName, OBJPROP_FONT, "Arial");              //--- Set font
      ObjectSetString(0, objectName, OBJPROP_TEXT, displayStrings[i]);    //--- Set text
      ObjectSetInteger(0, objectName, OBJPROP_SELECTABLE, false);         //--- Set selectable false
      ObjectSetInteger(0, objectName, OBJPROP_HIDDEN, true);              //--- Set hidden true
   }
   
   RenderOpenCloseMarkers(sessionIndex, barIndex);                        //--- Render open close markers
}

Hier implementieren wir die Funktion RenderSessionProfile, um das vollständige Marktprofil für eine bestimmte Sitzung im Chart darzustellen. Dabei beginnen wir mit einer Indexüberprüfung anhand der Größe des Sitzungs-Arrays mithilfe von ArraySize, um die Funktion bei Ungültigkeit vorzeitig zu beenden, und prüfen anschließend, ob die Ebenen nicht leer sind und eine gültige Startzeit vorliegt. Wir ermitteln den Startbarindex mit iBarShift anhand des Symbols, des Zeitraums und der Sitzungsstartzeit; bei Ungültigkeit kehren wir zurück. Anschließend rufen wir SortPriceLevelsDescending auf, um die Kursniveaus vom höchsten zum niedrigsten zu sortieren, und CalculatePointOfControl, um das Niveau mit der höchsten Anzahl an Time Price Opportunity-Werten zu ermitteln.

Als Nächstes rufen wir über GetTotalTpoCount die Gesamtanzahl der Time-Price-Opportunities sowie den Index des Point of Control ab und initialisieren damit die oberen und unteren Indizes des Wertebereichs. Wenn der Point of Control gültig ist, berechnen wir eine Zielanzahl als Prozentsatz der Gesamtzahl unter Verwendung des Eingabewerts Bereichsprozentsatz, ausgehend von der aktuellen Anzahl auf dieser Ebene, und erweitern den Wertebereich in einer while-Schleife, indem wir die Anzahlen benachbarter oberer oder unterer Ebenen vergleichen und addieren – vorzugsweise je nachdem, welche mehr Time-Price-Opportunities aufweist –, wobei wir die Indizes verringern oder erhöhen, bis das Ziel oder die Grenzen erreicht sind.

Zur Vorbereitung des Renderings deklarieren wir ein Array für die Anzeigezeichenfolgen und passen dessen Größe mit ArrayResize an die Größe des Levels an, wobei wir die Zeichenfolge Time Price Opportunity jedes Levels über eine Schleife in das Array kopieren. Anschließend suchen wir nach dem Schlusskurs-Index, indem wir die Kursstufen durchlaufen und mithilfe von MathAbs die größte Übereinstimmung mit dem Schlusskurs der Sitzung innerhalb der Hälfte der Rastertoleranz ermitteln; sobald wir diese gefunden haben, brechen wir den Vorgang ab.

Nachdem wir RenderCloseTpoHighlight mit dem Sitzungsindex, dem Schließebene-Index und den Anzeigezeichenfolgen aufgerufen haben, um die Hervorhebung beim Schließen zu handhaben, durchlaufen wir jede Ebene in einer Schleife, um Beschriftungsobjekte zu erstellen oder zu aktualisieren: Dabei bilden wir mithilfe von IntegerToString einen eindeutigen Namen aus dem Präfix, der Startzeit und dem Ebenenindex. Wir legen eine Standardtextfarbe fest, überschreiben diese Farbe für Single Prints, den Wertebereich oder Point of Control basierend auf den Eingabefarben und überprüfen das Vorhandensein mit ObjectFind. Bei Bedarf erstellen wir mit ObjectCreate ein OBJ_LABEL, wobei die Abstände zunächst auf Null gesetzt werden. Wir rufen die Beschriftungszeit über iTime und die Umwandlung von Zeit-Preis in Koordinaten mithilfe von ChartTimePriceToXY ab und legen Position, Ecke, Ankerpunkt, Farbe, Schriftgröße, Schriftart, Text aus der Anzeigekette und nicht auswählbare bzw. ausgeblendete Eigenschaften mithilfe der Funktionen ObjectSetInteger und ObjectSetString fest.

Abschließend rufen wir RenderOpenCloseMarkers mit dem Sitzungsindex und dem Barindex auf, um die visuellen Indikatoren für Eröffnungs- und Schlusskurse im Chart hinzuzufügen. Wir können diese Funktion nun im Ereignis-Handler für die Tick-Berechnung aufrufen, um die rechenintensiven Aufgaben bedingt auszuführen.

//+------------------------------------------------------------------+
//| Calculate custom indicator                                       |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[]) {
   
   if(rates_total < 2) return 0;                                 //--- Return if insufficient rates
   
   datetime currentBarTime = time[rates_total - 1];              //--- Get current bar time
   bool isNewBar = (currentBarTime != lastCompletedBarTime);     //--- Check if new bar
   
   if(IsNewSessionStarted(currentBarTime, previousBarTime) || previousBarTime == 0) { //--- Check new session
      if(activeSessionIndex >= 0 && activeSessionIndex < ArraySize(sessions)) { //--- Check active session
         sessions[activeSessionIndex].endTime = previousBarTime; //--- Set end time
         RenderSessionProfile(activeSessionIndex);               //--- Render session profile
      }
      
      activeSessionIndex = CreateNewSession();                  //--- Create new session
      sessions[activeSessionIndex].startTime = currentBarTime;  //--- Set start time
      sessions[activeSessionIndex].sessionOpen = open[rates_total - 1]; //--- Set session open
      sessions[activeSessionIndex].sessionHigh = high[rates_total - 1]; //--- Set session high
      sessions[activeSessionIndex].sessionLow = low[rates_total - 1];   //--- Set session low
      lastCompletedBarTime = currentBarTime;                    //--- Update last completed bar time
   }
   
   previousBarTime = currentBarTime;                            //--- Update previous bar time
   
   if(isNewBar && IsBarEligibleForProcessing(currentBarTime) && activeSessionIndex >= 0) { //--- Check if process bar
      sessions[activeSessionIndex].sessionHigh = MathMax(sessions[activeSessionIndex].sessionHigh, high[rates_total - 1]); //--- Update session high
      sessions[activeSessionIndex].sessionLow = MathMin(sessions[activeSessionIndex].sessionLow, low[rates_total - 1]); //--- Update session low
      sessions[activeSessionIndex].sessionClose = close[rates_total - 1]; //--- Update session close
      
      int periodIndex = sessions[activeSessionIndex].periodCount; //--- Get period index
      ArrayResize(sessions[activeSessionIndex].periodOpens, periodIndex + 1); //--- Resize period opens
      
      sessions[activeSessionIndex].periodOpens[periodIndex] = open[rates_total - 1]; //--- Set period open
      sessions[activeSessionIndex].periodCount++;                 //--- Increment period count
      
      double quantizedHigh = QuantizePriceToGrid(high[rates_total - 1]); //--- Quantize high
      double quantizedLow = QuantizePriceToGrid(low[rates_total - 1]);   //--- Quantize low
      
      for(double price = quantizedLow; price <= quantizedHigh; price += tpoPriceGridStep) { //--- Loop through prices
         int levelIndex = GetOrCreatePriceLevel(activeSessionIndex, price); //--- Get or create level
         if(levelIndex >= 0) {                                    //--- Check valid level
            AddTpoCharacterToLevel(activeSessionIndex, levelIndex, periodIndex); //--- Add TPO character
         }
      }
      
      lastCompletedBarTime = currentBarTime;                      //--- Update last completed bar time
   }
   
   if(IsBarEligibleForProcessing(currentBarTime) && activeSessionIndex >= 0) { //--- Check if update session
      sessions[activeSessionIndex].sessionClose = close[rates_total - 1]; //--- Update close
      sessions[activeSessionIndex].sessionHigh = MathMax(sessions[activeSessionIndex].sessionHigh, high[rates_total - 1]); //--- Update high
      sessions[activeSessionIndex].sessionLow = MathMin(sessions[activeSessionIndex].sessionLow, low[rates_total - 1]); //--- Update low
   }
   
   for(int i = 0; i < ArraySize(sessions); i++) {                 //--- Loop through sessions
      RenderSessionProfile(i);                                    //--- Render profile
   }
   
   return rates_total;                                            //--- Return rates total
}

Die Hauptberechnung führen wir in der Funktion OnCalculate durch, die bei jeder Aktualisierung die Kursdaten-Arrays für den benutzerdefinierten Indikator verarbeitet. Dabei wird zunächst der Wert Null zurückgegeben, wenn weniger als zwei Kurse verfügbar sind, um sicherzustellen, dass ausreichende Daten vorliegen. Wir entnehmen die aktuelle Barzeit aus dem Zeit-Array am letzten Index und stellen fest, ob es sich um eine neue Bar handelt, indem wir sie mit der letzten abgeschlossenen Zeit vergleichen. Wird über IsNewSessionStarted eine neue Sitzung erkannt oder liegt kein vorheriger Zeitpunkt vor, schließen wir die aktive Sitzung ab, indem wir ihre Endzeit festlegen und sie mit RenderSessionProfile darstellen, sofern dies innerhalb der durch ArraySize vorgegebenen Grenzen gültig ist. Anschließend erstellen wir mit CreateNewSession eine neue Sitzung, initialisieren deren Startzeit sowie die Werte für Open, High und Low anhand der Daten der aktuellen Bar und aktualisieren die Zeit der zuletzt abgeschlossenen Sitzung.

Nachdem die Zeit der vorherigen Bar aktualisiert wurde, wird – sofern es sich um eine neue Bar handelt, der gemäß IsBarEligibleForProcessing für die Verarbeitung in Frage kommt, und eine aktive Sitzung vorliegt – aktualisieren wir das Hoch und das Tief der Sitzung mithilfe von MathMax und MathMin mit den aktuellen Werten der Bar, legen den Schlusskurs fest, erhöhen die Periodenzahl, nachdem wir die Eröffnungsnotierung der Periode angepasst und im Array gespeichert haben, quantisieren das Hoch und Tief der Bar über QuantizePriceToGrid und durchlaufen in Raster-Schritten von Tief nach Hoch, um mit GetOrCreatePriceLevel Preisniveaus abzurufen oder zu erstellen, bevor mit AddTpoCharacterToLevel Zeichen für die Zeit-Preis-Chance hinzugefügt werden, und schließen schließlich mit der Aktualisierung der zuletzt abgeschlossenen Zeit ab. Wenn die aktuelle Bar die Voraussetzungen erfüllt und die Sitzung aktiv ist, aktualisieren wir zudem kontinuierlich den Schlusskurs, Hoch und Tief, um die aktuellen Veränderungen widerzuspiegeln.

Abschließend durchlaufen wir alle Sitzungen, um jedes Profil darzustellen, und geben die Gesamtwerte zurück, um anzuzeigen, dass die Verarbeitung abgeschlossen ist. Nun müssen wir noch die manuelle Größenanpassung des Charts vornehmen, damit die Level neu gerendert werden und nicht mehr hängen bleiben, und unsere Objekte bei der Deinitialisierung des Indikators löschen. Um dies zu erreichen, haben wir den folgenden Ansatz verfolgt.

//+------------------------------------------------------------------+
//| Handle chart event                                               |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam) {
   if(id == CHARTEVENT_CHART_CHANGE) {               //--- Check chart change event
      for(int i = 0; i < ArraySize(sessions); i++) { //--- Loop through sessions
         RenderSessionProfile(i);                    //--- Render profile
      }
   }
}

//+------------------------------------------------------------------+
//| Deinitialize custom indicator                                    |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   DeleteAllIndicatorObjects();                      //--- Delete all indicator objects
}

//+------------------------------------------------------------------+
//| Delete all indicator objects                                     |
//+------------------------------------------------------------------+
void DeleteAllIndicatorObjects() {
   int total = ObjectsTotal(0, 0, -1);            //--- Get total number of objects
   for(int i = total - 1; i >= 0; i--) {          //--- Loop through objects in reverse
      string name = ObjectName(0, i, 0, -1);      //--- Get object name
      if(StringFind(name, objectPrefix) == 0)     //--- Check if name starts with prefix
         ObjectDelete(0, name);                   //--- Delete object
   }
}

Wir verarbeiten Interaktionen mit dem Chart im Ereignis-Handler OnChartEvent, der bei verschiedenen Chartereignissen ausgelöst wird. Dabei prüfen wir, ob die Ereignis-ID mit CHARTEVENT_CHART_CHANGE übereinstimmt, um Änderungen wie Zeitrahmenwechsel oder Größenanpassungen zu erkennen; ist dies der Fall, durchlaufen wir alle Sitzungen mithilfe von ArraySize, um die Anzahl zu ermitteln, und rendern jedes Profil mit RenderSessionProfile neu, um die Darstellungen entsprechend zu aktualisieren.

Beim Entfernen eines Indikators wird der Ereignis-Handler OnDeinit ausgeführt, der die Funktion DeleteAllIndicatorObjects aufruft, um alle benutzerdefinierten Chartelemente zu bereinigen und zu verhindern, dass Objekte zurückbleiben.

In DeleteAllIndicatorObjects rufen wir über die Funktion ObjectsTotal die Gesamtzahl der Chartobjekte über alle Fenster und Typen hinweg ab, durchlaufen dann die Liste rückwärts vom letzten Index bis Null, rufen den Namen jedes Objekts mit ObjectName ab und entfernen es mit ObjectDelete, sofern es mit dem Präfix beginnt, für das StringFind den Wert Null zurückgibt, um eine vollständige Bereinigung sicherzustellen. Wenn wir den Indikator ausführen, erhalten wir das folgende Ergebnis.

TEXT-MARKTPROFIL

Aus der Abbildung geht hervor, dass wir den Indikator berechnen und das Marktprofil mit Textbeschriftungen darstellen, wodurch wir unsere Ziele erreichen. Bleibt nur noch der Backtest des Programms, und der wird im nächsten Abschnitt behandelt.


Backtests

Wir haben die Tests durchgeführt, und unten sehen Sie die kompilierte Visualisierung als einzelne GIF-Animation.

BACKTEST-ANIMATION


Schlussfolgerung

Zusammenfassend lässt sich sagen, dass wir in MQL5 einen benutzerdefinierten Indikator für hybride Time Price Opportunity Marktprofile (TPO) entwickelt haben, der mehrere Handelszeitrahmen unterstützt, darunter Intraday, täglich, wöchentlich, monatlich sowie feste Zeitintervalle mit Zeitzonenanpassungen. Der Indikator unterteilt die Kurse in ein Raster, erfasst die Sitzungsdaten zu Höchst-, Tiefst-, Eröffnungs- und Schlusskursen, berechnet den Point of Control und den Wertebereich anhand von TPO-Zählwerten und stellt Profile im Chart dar, wobei die Farben für TPO-Buchstaben, Single Prints, Wertebereiche, den Point of Control und Schlusskursmarkierungen individuell angepasst werden können. Im folgenden Teil werden wir die Darstellung der quadratischen und punktförmigen Blasen einfügen, um das Marktprofil mit allen Beschriftungen zu kennzeichnen. Bleiben Sie dran!

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

Die Übertragung der Trading-Signale in einem universalen Expert Advisor. Die Übertragung der Trading-Signale in einem universalen Expert Advisor.
In diesem Artikel wurden die verschiedenen Möglichkeiten beschrieben, um die Trading-Signale von einem Signalmodul des universalen EAs zum Steuermodul der Positionen und Orders zu übertragen. Es wurden die seriellen und parallelen Interfaces betrachtet.
Neurostrukturelle Trading-Engine – NSTE (Teil I): Aufbau eines prop-firm-tauglichen Multi-Account-Systems Neurostrukturelle Trading-Engine – NSTE (Teil I): Aufbau eines prop-firm-tauglichen Multi-Account-Systems
Dieser Artikel beschreibt die Systemarchitektur für eine algorithmische Handelslösung mit mehreren Konten, die Kryptowährungs-CFDs auf MetaTrader 5 handelt und dabei die Vorgaben für Prop-Firmen berücksichtigt. Er definiert drei Kernprinzipien – festes Dollar-Risiko, ein Skript pro Konto und zentralisierte Konfiguration – und erläutert anschließend die Aufteilung zwischen Python und MQL5, die 60-Sekunden-Verarbeitungsschleife sowie die JSON-basierte Signalübermittlung. Die Leser erhalten praktische Anleitungen zur Berechnung der Lot-Größe, zu Sicherheitsprüfungen und zu praxisnahen Mustern für das Positionsmanagement und für einen zuverlässigen Live-Einsatz.
Eine alternative Log-datei mit der Verwendung der HTML und CSS Eine alternative Log-datei mit der Verwendung der HTML und CSS
In diesem Artikel werden wir eine sehr einfache, aber leistungsfähige Bibliothek zur Erstellung der HTML-Dateien schreiben, dabei lernen wir auch, wie man eine ihre Darstellung einstellen kann (nach seinem Geschmack) und sehen wir, wie man es leicht in seinem Expert Advisor oder Skript hinzufügen oder verwenden kann.
Swing-Extreme und Rücksetzer in MQL5 (Teil 1): Entwicklung eines Multi-Timeframe-Indikators Swing-Extreme und Rücksetzer in MQL5 (Teil 1): Entwicklung eines Multi-Timeframe-Indikators
In diesem Beitrag werden wir Swing Extremes und den Indikator für Rücksetzer automatisieren, der die rohe Kursbewegung in niedrigeren Zeitrahmen (LTF) in eine strukturierte Darstellung der Marktrichtung umwandelt und dabei Swing-Hochs, Swing-Tiefs und Korrekturphasen in Echtzeit präzise identifiziert. Durch die programmatische Verfolgung von Veränderungen in der Mikrostruktur antizipiert es potenzielle Umkehrungen, bevor diese sich vollständig entfalten – und verwandelt so Rauschen in handlungsrelevante Erkenntnisse.