English Русский 日本語
preview
Erstellen benutzerdefinierter Indikatoren in MQL5 (Teil 8): Integration von Volumendaten für eine tiefere Marktprofilanalyse

Erstellen benutzerdefinierter Indikatoren in MQL5 (Teil 8): Integration von Volumendaten für eine tiefere Marktprofilanalyse

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

Einführung

In unserem vorherigen Artikel (Teil 7) haben wir den hybriden Marktprofil-Indikator Time Price Opportunity (TPO) in MetaQuotes Language 5 (MQL5) entwickelt, der mehrere Zeitrahmen unterstützt, darunter Intraday, täglich, wöchentlich, monatlich und feste Zeiträume mit Zeitzonenanpassungen, Preise in ein Raster quantisiert, die Sitzungsdaten Hoch, Tief, Eröffnungs- und Schlusskurse, die Berechnung des Kontrollpunkts und des Wertbereichs anhand der TPO-Anzahl sowie die visuelle Darstellung im Chart mit anpassbaren Farben für eine detaillierte Sitzungsanalyse.

In Teil 8 erweitern wir diesen Indikator um eine Volumenintegration, um tiefere Einblicke in das Marktprofil zu ermöglichen, darunter volumenbasierte Kontrollpunkte, Wertbereiche und Berechnungen des volumengewichteten Durchschnittspreises mit Hervorhebungsoptionen. Dieses Upgrade umfasst erweiterte Funktionen wie die Erkennung der Initial Balance, Projektionslinien wichtiger Kursniveaus, geteilte Profile, alternative TPO-Zeichen wie Quadrate oder Kreise, Begrenzungslinien, Hintergrundrechtecke für feste Bereiche sowie dynamische Volumenbeschriftungen – und das alles bei gleichbleibender Flexibilität über alle Zeitrahmen hinweg. Wir werden die folgenden Themen behandeln:

  1. Einbindung von Handelsvolumen und erweiterten Funktionen in hybride Marktprofile für Time Price Opportunity (TPO)
  2. Implementierung in MQL5
  3. Backtests
  4. Schlussfolgerung

Am Ende verfügen Sie über einen fortschrittlichen MQL5-Indikator für hybride Time Price Opportunity-Marktprofile mit volumenbasierter Analyse, der sich ganz nach Ihren Wünschen anpassen lässt – legen wir los!


Einbindung von Handelsvolumen und erweiterten Funktionen in hybride Marktprofile für Time Price Opportunity (TPO)

Die Einbindung von Volumendaten in die Marktprofile von Time Price Opportunity (TPO) erweitert die traditionelle Preis-Zeit-Analyse, indem sie das Handelsvolumen auf jeder Preisstufe einbezieht. Dies ermöglicht die Identifizierung volumenbasierter Kontrollpunkte, an denen das höchste Volumen auftrat, sowie die Anpassung von Wertbereichen, um die tatsächliche Marktbeteiligung widerzuspiegeln und nicht nur die verstrichene Zeit. Diese Erweiterung liefert uns Einblicke in die Überzeugung des Marktes, beispielsweise durch die Unterscheidung zwischen Fair-Value-Zonen mit hohem Volumen, die als potenzielle Unterstützungs- oder Widerstandsniveaus gelten, und Bereichen mit geringem Volumen, die auf schwaches Interesse oder mögliche Ausbrüche hindeuten, während Kennzahlen wie der volumengewichtete Durchschnittskurs einen dynamischen Referenzwert für den Intraday-Bias liefern.

Wir werden das Kern-Sitzungsmanagement um die Volumensummierung pro Preisniveau erweitern, optional den Volumen-Kontrollpunkt und den gewichteten Durchschnittspreis berechnen, die Initial Balance (Eröffnungsrange) aus früheren Perioden ermitteln, eine erweiterte Darstellung für wichtige Niveaus wie Höchststände, Tiefststände, Grenzen des Wertbereichs und den Mittelpunkt, Zeichenvariationen und Markierungsoptionen für Eröffnungsnotierungen integrieren sowie geteilte Profile mit Auffüllung/Leerzeichen zur besseren alphabetischen Darstellung ermöglichen – und das alles einschließlich Hintergrundflächen für feste Zeitbereiche und Volumenbeschriftungen für eine umfassende Profilvisualisierung. Kurz gesagt: Hier ist eine visuelle Darstellung unserer Ziele.

ROADMAP ZUM VOLUMEN-MARKTPROFIL


Implementierung in MQL5

Um mit der Umsetzung der Erweiterungen zu beginnen, werden wir zunächst die Indikator-Aufzählungen, Eingabewerte und globalen Variablen erweitern, um die neuen Volumenkennzahlen zu unterstützen.

//+------------------------------------------------------------------+
//|                             Hybrid TPO Market Profile PART 2.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
};

enum TpoCharacterType {      // Define TPO character type enum
   SQUARE,                   // ■ Square
   CIRCLE,                   // ● Circle
   ALPHABETIC                // A-Za-z
};

enum MidpointAlgorithm {     // Define midpoint algorithm enum
   HIGH_LOW_MID,             // High/Low mid
   TPO_COUNT_BASED           // Number of TPOs
};

enum MarkPeriodOpens {       // Define mark period opens enum
   NONE,                     // No
   SWAP_CASE,                // Swap case
   USE_ZERO                  // Use '0'
};

enum TextSize {              // Define text size enum
   TINY,                     // Tiny
   SMALL,                    // Small
   NORMAL                    // Normal
};

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
sinput group "Settings"
input double ticksPerTpoLetter = 10;             // Ticks per letter
input bool highlightVolumeProfilePoc = true;     // Highlight POC based on VP?
input bool useVolumeProfilePocForValueArea = true; // Use VP POC for Value Area?
input bool highlightSessionVwap = false;         // Highlight session VWAP?
input bool showExtensionLines = false;           // Show extension lines?
input bool splitProfile = false;                 // Split MP?

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)
input bool renderVolumes = true;                 // Render volume numbers? (Fixed)

sinput group "Rendering"
input TpoCharacterType tpoCharacterType = SQUARE; // TPO characters
input int valueAreaPercent = 70;                 // Value Area Percent
input int initialBalancePeriods = 2;             // IB periods
input int initialBalanceLineWidth = 2;           // IB line width
input int priceMarkerWidth = 2;                  // Price marker width
input int priceMarkerLength = 1;                 // Price marker length
input TextSize textSize = NORMAL;                // Font size
input MarkPeriodOpens markPeriodOpens = NONE;    // Mark period open?
input MidpointAlgorithm midpointAlgorithm = HIGH_LOW_MID; // Midpoint algo

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 volumeProfilePocColor = 0x87c74c;    // VP POC
input color openColor = clrDodgerBlue;           // Open
input color closeColor = clrRed;                 // Close
input color initialBalanceHighlightColor = clrDodgerBlue; // IB
input color initialBalanceBackgroundColor = 0x606D79;     // IB background
input color sessionVwapColor = 0xFF9925;                  // Session VWAP
input color pocExtensionColor = 0x87c74c;                 // POC extension
input color valueAreaHighExtensionColor = clrBlack;       // VAH extension
input color valueAreaLowExtensionColor = clrBlack;        // VAL extension
input color highExtensionColor = clrRed;                  // High extension
input color lowExtensionColor = clrGreen;                 // Low extension
input color midpointExtensionColor = 0x7649ff;            // Midpoint extension
input color fixedRangeBackgroundColor = 0x3179f5;         // Fixed range background

//+------------------------------------------------------------------+
//| 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
   double volume;            // Store volume
};

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
   double initialBalanceHigh;// Store initial balance high
   double initialBalanceLow; // Store initial balance low
   double vwap;              // Store VWAP
   int pointOfControlIndex;  // Store point of control index
   int volumeProfilePocIndex;// Store volume profile POC index
   TpoPriceLevel levels[];   // Store array of price levels
   int periodCount;          // Store period count
   double periodHighs[];     // Store array of period highs
   double periodLows[];      // Store array of period lows
   double periodOpens[];     // Store array of period opens
   double volumeProfilePrices[]; // Store array of volume profile prices
   double volumeProfileVolumes[]; // Store array of volume profile volumes
};

//+------------------------------------------------------------------+
//| 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
bool isNewSession = false;         //--- Initialize new session flag
int labelFontSize = 10;            //--- Set label font size
int maxSessionHistory = 20;        //--- Set maximum session history
int timezoneOffsetSeconds = 0;     //--- Initialize timezone offset in seconds

Wir beginnen mit der Umsetzung, indem wir zusätzliche Enumerationen definieren, um die Konfigurationsoptionen für das erweiterte hybride Time Price Opportunity-Marktprofil zu erweitern. Die Enumeration TpoCharacterType bietet Optionen wie SQUARE für Blocksymbole, CIRCLE für Punktdarstellungen und ALPHABETIC für die Beschriftung mit Buchstaben, je nach visueller Präferenz – im Gegensatz zu der einzigen Beschriftungsoption, die wir bisher hatten. Anschließend bietet MidpointAlgorithm Algorithmen zur Berechnung des Mittelpunkts, wie beispielsweise HIGH_LOW_MID auf Basis der Spanne innerhalb einer Handelssitzung oder TPO_COUNT_BASED unter Verwendung der kumulierten Anzahl von Time-Price-Opportunities. Wir fügen MarkPeriodOpens mit folgenden Optionen hinzu: NONE, um die Markierung zu überspringen, SWAP_CASE, um die Groß-/Kleinschreibung bei Eröffnungen umzuschalten, und USE_ZERO, um sie mit einem Nullzeichen zu kennzeichnen. Schließlich ermöglicht TextSize die Skalierung von Text mit den Einstellungen TINY, SMALL oder NORMAL.

Wir erweitern die nach Abschnitten gruppierten Benutzereingaben und integrieren neue Einstellungen wie highlightVolumeProfilePoc und useVolumeProfilePocForValueArea, um volumenbasierte Kontrollpunktfunktionen zu aktivieren bzw. zu deaktivieren, highlightSessionVwap zur Hervorhebung des gewichteten Durchschnittskurses, showExtensionLines zur Projektion wichtiger Niveaus und splitProfile für eine getrennte alphabetische Darstellung. In der Zeitgruppe kommt renderVolumes hinzu, um Volumenbeschriftungen mit festem Bereich zu steuern. Bei der Darstellung berücksichtigen wir die Auswahlmöglichkeiten für den Charaktertyp Time Price Opportunity, die Perioden für die Initial Balance sowie die Linienbreite, die Abmessungen der Kursmarkierungen, die Textgröße, die Markierung der Periodeneröffnung und die Mittelpunktmethode. Die Farbgruppe wird um Eingaben für den Volumenprofil-Kontrollpunkt, Markierungen für Eröffnungskurse, Hervorhebungen und den Hintergrund der Initial Balance, den sitzungsbezogenen VWAP, verschiedene Erweiterungslinien sowie Hintergründe für feste Bereiche erweitert.

Wir aktualisieren Strukturen, um die Volumenintegration zu unterstützen: TpoPriceLevel enthält nun neben dem Kurs, der Zeichenfolge Time Price Opportunity und der Anzahl auch ein Volumenfeld; ProfileSessionData wurde um Initial Balance-Hoch und -Tief, den gewichteten Durchschnittskurs, den Kontrollpunktindex des Volumenprofils sowie Arrays für die Höchst- und Tiefststände der Periode und separate Arrays für die Kurse und Volumina des Volumenprofils erweitert. Globale Variablen werden entsprechend angepasst, wobei isNewSession als Flag eingeführt und labelFontSize später dynamisch festgelegt wird, während Elemente wie Objektpräfix, Sitzungsarray, aktiver Index, Rasterabstand, Zeichensatz, Zeitstempel, Verlaufsbegrenzung und Zeitzonenversatz beibehalten werden. Als Nächstes werden wir den Ereignis-Handler für die Initialisierung aktualisieren, indem wir die Schriftgröße wie folgt festlegen. Wir haben die spezifischen Änderungen zur Verdeutlichung hervorgehoben.

//+------------------------------------------------------------------+
//| Initialize custom indicator                                      |
//+------------------------------------------------------------------+
int OnInit() {
   IndicatorSetString(INDICATOR_SHORTNAME, "Hybrid TPO Market Profile"); //--- 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
   }
   
   switch(textSize) {                             //--- Switch on text size
      case TINY: labelFontSize = 7; break;        //--- Set tiny font size
      case SMALL: labelFontSize = 9; break;       //--- Set small font size
      case NORMAL: labelFontSize = 11; break;     //--- Set normal font size
   }
   
   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
}

Wenn wir hier die Schriftgröße festlegen, können wir die Textgröße anpassen, um die Lesbarkeit in verschiedenen Charts zu verbessern. Als Nächstes werden wir die Funktion zur Erstellung von Sitzungen so anpassen, dass sie die neuen Speicher-Arrays verarbeitet und die abgelaufenen löscht, um die Verwaltung zu vereinfachen.

//+------------------------------------------------------------------+
//| Delete session objects                                           |
//+------------------------------------------------------------------+
void DeleteSessionObjects(datetime sessionTime) {
   string sessionString = IntegerToString(sessionTime); //--- Convert session time to string
   
   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 && StringFind(name, sessionString) > 0) //--- Check if matches session
         ObjectDelete(0, name);                   //--- Delete object
   }
}

//+------------------------------------------------------------------+
//| Create new session                                               |
//+------------------------------------------------------------------+
int CreateNewSession() {
   int size = ArraySize(sessions);                //--- Get size of sessions array
   
   if(size >= maxSessionHistory) {                //--- Check if size exceeds history limit
      DeleteSessionObjects(sessions[0].startTime); //--- Delete old session objects
      
      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].initialBalanceHigh = 0;     //--- Initialize initial balance high
   sessions[newIndex].initialBalanceLow = 0;      //--- Initialize initial balance low
   sessions[newIndex].vwap = 0;                   //--- Initialize VWAP
   sessions[newIndex].pointOfControlIndex = -1;   //--- Initialize point of control index
   sessions[newIndex].volumeProfilePocIndex = -1; //--- Initialize volume profile POC index
   sessions[newIndex].periodCount = 0;            //--- Initialize period count
   ArrayResize(sessions[newIndex].levels, 0);     //--- Resize levels array
   ArrayResize(sessions[newIndex].periodHighs, 0);//--- Resize period highs array
   ArrayResize(sessions[newIndex].periodLows, 0); //--- Resize period lows array
   ArrayResize(sessions[newIndex].periodOpens, 0);//--- Resize period opens array
   ArrayResize(sessions[newIndex].volumeProfilePrices, 0); //--- Resize volume profile prices array
   ArrayResize(sessions[newIndex].volumeProfileVolumes, 0); //--- Resize volume profile volumes array
   
   return newIndex;                               //--- Return new index
}

Zunächst führen wir die Funktion DeleteSessionObjects ein, um Chartobjekte zu entfernen, die mit einer bestimmten Sitzung verknüpft sind. Dabei wandeln wir die Sitzungszeit mit IntegerToString in eine Zeichenkette um und durchlaufen dann rückwärts alle Objekte, die über ObjectsTotal abgerufen wurden, rufen die Namen mit ObjectName ab und löschen diejenigen, die mit dem Präfix beginnen und die Sitzungszeichenfolge enthalten, wie von der Funktion StringFind überprüft.

Anschließend erweitern wir die Funktion CreateNewSession zur Verwaltung neuer Profile, indem wir mit ArraySize die aktuelle Größe des Sitzungs-Arrays abfragen; wenn das Verlaufslimit erreicht ist, rufen wir DeleteSessionObjects für die Startzeit der ältesten Sitzung auf, um die Visualisierungen zu löschen, die verbleibenden Sitzungen nach vorn zu verschieben und die Größe nach unten anzupassen. Anschließend erweitern wir das Array um den neuen Eintrag, initialisieren dessen Felder – einschließlich neuer Felder wie Initial Balance, Höchst- und Tiefststände, gewichteter Durchschnittspreis und Kontrollpunktindex des Volumenprofils – und setzen Zählwerte und Indizes zurück, während wir gleichzeitig die Größe weiterer Arrays für Periodenhöchst- und -tiefststände, Preise des Volumenprofils und Volumina anpassen, bevor wir den neuen Index zurückgeben. Nachdem das erledigt ist, benötigen wir Hilfsfunktionen zum Ändern der Groß-/Kleinschreibung, um die Periodeneröffnungen zu markieren.

//+------------------------------------------------------------------+
//| Convert string to upper case                                     |
//+------------------------------------------------------------------+
string ConvertToUpperCase(string str) {
   string result = str;                           //--- Copy string
   StringToUpper(result);                         //--- Convert to upper case
   return result;                                 //--- Return result
}

//+------------------------------------------------------------------+
//| Convert string to lower case                                     |
//+------------------------------------------------------------------+
string ConvertToLowerCase(string str) {
   string result = str;                           //--- Copy string
   StringToLower(result);                         //--- Convert to lower case
   return result;                                 //--- Return result
}

//+------------------------------------------------------------------+
//| Check if character is upper case                                 |
//+------------------------------------------------------------------+
bool IsUpperCaseCharacter(string character) {
   return character == ConvertToUpperCase(character) && character != ConvertToLowerCase(character); //--- Check and return if upper case
}

Hier fügen wir Hilfsfunktionen zur Behandlung der Groß-/Kleinschreibung von Zeichenfolgen hinzu, um Funktionen wie die Markierung von Periodeneröffnungen durch Umkehrung der Groß-/Kleinschreibung zu unterstützen. Die Funktion ConvertToUpperCase kopiert die Eingabezeichenfolge und wendet StringToUpper an, um sie vollständig in Großbuchstaben umzuwandeln, bevor sie den Wert zurückgibt. Ebenso dupliziert ConvertToLowerCase die Zeichenkette und verwendet StringToLower, um sie vollständig in Kleinbuchstaben umzuwandeln. Um die Groß-/Kleinschreibung zu erkennen, prüft IsUpperCaseCharacter, ob ein einzelnes Zeichen über ConvertToUpperCase mit seiner Großbuchstabenform übereinstimmt und sich gleichzeitig über ConvertToLowerCase von seiner Kleinbuchstabenform unterscheidet, wobei für Großbuchstaben true zurückgegeben wird. Als Nächstes werden wir die Art und Weise ändern, wie die TPO-Zeichen in die Level eingefügt werden, da wir nun verschiedene Optionen für die Punkte und Quadrate haben.

//+------------------------------------------------------------------+
//| 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 = "";                      //--- Initialize TPO character
   
   switch(tpoCharacterType) {                     //--- Switch on TPO character type
      case SQUARE:                                //--- Handle square case
         tpoCharacter = "■";                      //--- Set square character
         break;                                   //--- Exit case
      case CIRCLE:                                //--- Handle circle case
         tpoCharacter = "●";                      //--- Set circle character
         break;                                   //--- Exit case
      case ALPHABETIC:                            //--- Handle alphabetic case
         tpoCharacter = tpoCharacterSet[periodIndex % 52]; //--- Get alphabetic character
         
         if(markPeriodOpens != NONE && periodIndex < ArraySize(sessions[sessionIndex].periodOpens)) { //--- Check if mark opens and valid index
            double periodOpen = sessions[sessionIndex].periodOpens[periodIndex]; //--- Get period open
            double levelPrice = sessions[sessionIndex].levels[levelIndex].price; //--- Get level price
            
            if(MathAbs(levelPrice - periodOpen) < tpoPriceGridStep / 2) { //--- Check if matches open
               if(markPeriodOpens == SWAP_CASE) {                         //--- Check swap case
                  if(IsUpperCaseCharacter(tpoCharacter))                  //--- Check if upper
                     tpoCharacter = ConvertToLowerCase(tpoCharacter);     //--- Convert to lower
                  else                                                    //--- Handle lower
                     tpoCharacter = ConvertToUpperCase(tpoCharacter);     //--- Convert to upper
               } else if(markPeriodOpens == USE_ZERO) {                   //--- Check use zero
                  tpoCharacter = "0";                                     //--- Set zero character
               }
            }
         }
         break;                                                           //--- Exit case
   }
   
   sessions[sessionIndex].levels[levelIndex].tpoString += tpoCharacter;   //--- Append character to TPO string
   sessions[sessionIndex].levels[levelIndex].tpoCount++;                  //--- Increment TPO count
}

Hier erweitern wir die Funktion AddTpoCharacterToLevel, um anpassbare Darstellungen von Time Price Opportunity (TPO) und die Markierung von Periodeneröffnungen zu integrieren. Dabei beginnen wir mit Indexvalidierungen für die Sitzung und die Ebene, wobei wir ArraySize verwenden, um bei Ungültigkeit vorzeitig abzubrechen. Wir initialisieren eine leere Zeichenkette für Time Price Opportunity und verwenden dann eine switch-Anweisung für die Enumeration tpoCharacterType: Für SQUARE oder CIRCLE weisen wir feste Symbole wie einen Block oder einen Punkt zu; bei ALPHABETIC wird aus dem Zeichensatz mit Modulo 52 auf den Periodenindex ausgewählt. Was die Zeichen angeht, haben wir uns für die fest codierten entschieden, da sie besser dargestellt werden als die aus Wingdings-Symbole, sie können aber weiterhin für eine dynamischere Symbolwahl verwendet werden.

Wenn es sich um einen alphabetischen Wert handelt und markPeriodOpens nicht NONE ist und eine gültige Größe des Arrays für die Eröffnungswerte der Periode vorliegt, vergleichen wir den Level-Preis mit dem Eröffnungswert der Periode unter Verwendung von MathAbs innerhalb der Hälfte der Rasterschritt-Toleranz. Bei Übereinstimmung wenden wir SWAP_CASE an, indem wir mit IsUpperCaseCharacter prüfen und über ConvertToLowerCase oder ConvertToUpperCase umschalten, oder setzen den Wert bei USE_ZERO auf Null. Abschließend fügen wir das Zeichen an die Zeichenkette der Stufe an und erhöhen den Zählwert für Time Price Opportunity, um das Profil zu erstellen. Wir werden nun eine weitere Hilfsfunktion definieren, um TPO-Zeichenfolgen zur Aufteilung von Profilen – sofern diese Funktion aktiviert ist – mit Leerzeichen zu versehen. Dadurch wird eine geteilte Ansicht des Profils erstellt, um die Perioden bei der Verwendung von Buchstaben klarer voneinander zu trennen.

//+------------------------------------------------------------------+
//| Pad levels for split profile                                     |
//+------------------------------------------------------------------+
void PadLevelsForSplitProfile(int sessionIndex) {
   if(!splitProfile || tpoCharacterType != ALPHABETIC) return;         //--- Return if not split or not alphabetic
   if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return; //--- Return if index invalid
   
   int periodCount = sessions[sessionIndex].periodCount;               //--- Get period count
   int levelCount = ArraySize(sessions[sessionIndex].levels);          //--- Get level count
   
   for(int i = 0; i < levelCount; i++) {                               //--- Loop through levels
      int currentLength = StringLen(sessions[sessionIndex].levels[i].tpoString); //--- Get current length
      if(currentLength < periodCount) {                                //--- Check if needs padding
         for(int j = currentLength; j < periodCount; j++) {            //--- Loop to pad
            sessions[sessionIndex].levels[i].tpoString += " ";         //--- Add space
         }
      }
   }
}

Wir führen die Funktion PadLevelsForSplitProfile ein, um alphabetische Time Price Opportunity-Zeichenfolgen für Split-Profil-Ansichten vorzubereiten. Die Funktion bricht vorzeitig ab, wenn der Split-Modus deaktiviert ist, der Zeichentyp nicht alphabetisch ist oder der Sitzungsindex im Hinblick auf die Größe des Sitzungs-Arrays ungültig ist. Wir ermitteln die Anzahl der Perioden und die Anzahl der Ebenen und durchlaufen dann jede Ebene, um die aktuelle Zeichenfolgenlänge mit StringLen zu überprüfen; ist sie kürzer als die Anzahl der Perioden, füllen wir sie in einer inneren Schleife durch Anhängen von Leerzeichen auf, um alle Zeichenfolgen auf dieselbe Länge zu bringen und so einen einheitlichen Abstand für die visuelle Trennung im gerenderten Profil zu gewährleisten. Als Nächstes müssen wir eine Funktion erstellen, mit der wir aus den Kursniveaus ein Volumenprofil erstellen und den volumenbasierten POC ermitteln können.

//+------------------------------------------------------------------+
//| Build volume profile and find POC                                |
//+------------------------------------------------------------------+
void BuildVolumeProfileAndFindPoc(int sessionIndex) {
   if(!highlightVolumeProfilePoc) {                //--- Check if not highlight VP POC
      if(sessionIndex >= 0 && sessionIndex < ArraySize(sessions)) //--- Check valid session
         sessions[sessionIndex].volumeProfilePocIndex = -1; //--- Reset VP POC index
      return;                                     //--- Return
   }
   
   if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return; //--- Return if index invalid
   if(sessions[sessionIndex].startTime == 0) return; //--- Return if no start time
   
   ENUM_TIMEFRAMES lowerTimeframe = PERIOD_M1;    //--- Set default lower timeframe
   int currentTimeframeSeconds = PeriodSeconds(_Period); //--- Get current timeframe seconds
   
   if(currentTimeframeSeconds < 30 * 60) {        //--- Check for M1 lower
      lowerTimeframe = PERIOD_M1;                 //--- Set M1
   } else if(currentTimeframeSeconds < 60 * 60) { //--- Check for M3 lower
      lowerTimeframe = PERIOD_M3;                 //--- Set M3
   } else if(currentTimeframeSeconds == 60 * 60) {//--- Check for M10 lower
      lowerTimeframe = PERIOD_M10;                //--- Set M10
   } else {                                       //--- Default to H1
      lowerTimeframe = PERIOD_H1;                 //--- Set H1
   }
   
   if(PeriodSeconds(_Period) <= 60) {             //--- Check for 1-minute timeframe
      int size = ArraySize(sessions[sessionIndex].levels); //--- Get levels size
      for(int i = 0; i < size; i++) {             //--- Loop through levels
         int volumeProfileIndex = -1;             //--- Initialize VP index
         for(int j = 0; j < ArraySize(sessions[sessionIndex].volumeProfilePrices); j++) { //--- Loop through VP prices
            if(MathAbs(sessions[sessionIndex].volumeProfilePrices[j] - sessions[sessionIndex].levels[i].price) < _Point / 2) { //--- Check match
               volumeProfileIndex = j;            //--- Set index
               break;                             //--- Exit loop
            }
         }
         
         if(volumeProfileIndex == -1) {         //--- Check if new
            int volumeProfileSize = ArraySize(sessions[sessionIndex].volumeProfilePrices); //--- Get VP size
            ArrayResize(sessions[sessionIndex].volumeProfilePrices, volumeProfileSize + 1); //--- Resize prices
            ArrayResize(sessions[sessionIndex].volumeProfileVolumes, volumeProfileSize + 1); //--- Resize volumes
            sessions[sessionIndex].volumeProfilePrices[volumeProfileSize] = sessions[sessionIndex].levels[i].price; //--- Set price
            sessions[sessionIndex].volumeProfileVolumes[volumeProfileSize] = sessions[sessionIndex].levels[i].volume; //--- Set volume
         } else {                                 //--- Handle existing
            sessions[sessionIndex].volumeProfileVolumes[volumeProfileIndex] += sessions[sessionIndex].levels[i].volume; //--- Add volume
         }
      }
   } else {                                       //--- Handle higher timeframes
      datetime sessionEnd = (sessions[sessionIndex].endTime > 0) ? sessions[sessionIndex].endTime : TimeCurrent(); //--- Get session end
      
      int startBar = iBarShift(_Symbol, lowerTimeframe, sessions[sessionIndex].startTime); //--- Get start bar
      int endBar = iBarShift(_Symbol, lowerTimeframe, sessionEnd); //--- Get end bar
      
      if(startBar < 0 || endBar < 0) return;      //--- Return if invalid bars
      
      int barCount = startBar - endBar + 1;       //--- Calculate bar count
      if(barCount <= 0) return;                   //--- Return if no bars
      
      double highs[], lows[];                     //--- Declare highs and lows arrays
      long volumes[];                             //--- Declare volumes array
      
      ArraySetAsSeries(highs, true);              //--- Set highs as series
      ArraySetAsSeries(lows, true);               //--- Set lows as series
      ArraySetAsSeries(volumes, true);            //--- Set volumes as series
      
      if(CopyHigh(_Symbol, lowerTimeframe, endBar, barCount, highs) <= 0) return; //--- Copy highs
      if(CopyLow(_Symbol, lowerTimeframe, endBar, barCount, lows) <= 0) return; //--- Copy lows
      if(CopyTickVolume(_Symbol, lowerTimeframe, endBar, barCount, volumes) <= 0) return; //--- Copy volumes
      
      for(int i = 0; i < barCount; i++) {         //--- Loop through bars
         double quantizedHigh = QuantizePriceToGrid(highs[i]); //--- Quantize high
         double quantizedLow = QuantizePriceToGrid(lows[i]); //--- Quantize low
         
         int priceCount = (int)MathMax(1, (quantizedHigh - quantizedLow) / tpoPriceGridStep + 1); //--- Calculate price count
         double volumePerLevel = (double)volumes[i] / priceCount; //--- Calculate volume per level
         
         for(double price = quantizedLow; price <= quantizedHigh; price += tpoPriceGridStep) { //--- Loop through prices
            int volumeProfileIndex = -1;          //--- Initialize VP index
            for(int j = 0; j < ArraySize(sessions[sessionIndex].volumeProfilePrices); j++) { //--- Loop through VP prices
               if(MathAbs(sessions[sessionIndex].volumeProfilePrices[j] - price) < _Point / 2) { //--- Check match
                  volumeProfileIndex = j;         //--- Set index
                  break;                          //--- Exit loop
               }
            }
            
            if(volumeProfileIndex == -1) {      //--- Check if new
               int volumeProfileSize = ArraySize(sessions[sessionIndex].volumeProfilePrices); //--- Get VP size
               ArrayResize(sessions[sessionIndex].volumeProfilePrices, volumeProfileSize + 1); //--- Resize prices
               ArrayResize(sessions[sessionIndex].volumeProfileVolumes, volumeProfileSize + 1); //--- Resize volumes
               sessions[sessionIndex].volumeProfilePrices[volumeProfileSize] = price; //--- Set price
               sessions[sessionIndex].volumeProfileVolumes[volumeProfileSize] = volumePerLevel; //--- Set volume
            } else {                              //--- Handle existing
               sessions[sessionIndex].volumeProfileVolumes[volumeProfileIndex] += volumePerLevel; //--- Add volume
            }
         }
      }
   }
   
   double maxVolume = 0;                          //--- Initialize max volume
   double volumeProfilePocPrice = 0;              //--- Initialize VP POC price
   
   for(int i = 0; i < ArraySize(sessions[sessionIndex].volumeProfileVolumes); i++) { //--- Loop through volumes
      if(sessions[sessionIndex].volumeProfileVolumes[i] > maxVolume) { //--- Check if max
         maxVolume = sessions[sessionIndex].volumeProfileVolumes[i]; //--- Update max
         volumeProfilePocPrice = sessions[sessionIndex].volumeProfilePrices[i]; //--- Update price
      }
   }
   
   sessions[sessionIndex].volumeProfilePocIndex = -1; //--- Reset VP POC index
   for(int i = 0; i < ArraySize(sessions[sessionIndex].levels); i++) { //--- Loop through levels
      if(MathAbs(sessions[sessionIndex].levels[i].price - volumeProfilePocPrice) < tpoPriceGridStep / 2) { //--- Check match
         sessions[sessionIndex].volumeProfilePocIndex = i; //--- Set index
         break;                                    //--- Exit loop
      }
   }
}

Wir implementieren die Funktion BuildVolumeProfileAndFindPoc, um ein separates Volumenprofil zur Hervorhebung des volumenbasierten Kontrollpunkts zu erstellen. Zunächst prüfen wir mit highlightVolumeProfilePoc, ob die Funktion aktiviert ist; ist dies nicht der Fall, setzen wir den Index für gültige Sitzungen zurück und beenden die Funktion vorzeitig. Nachdem wir den Sitzungsindex mit ArraySize überprüft und sichergestellt haben, dass eine Startzeit vorliegt, wählen wir dynamisch einen niedrigeren Zeitrahmen aus, indem wir PeriodSeconds auf den aktuellen Zeitraum anwenden: Wir setzen den Wert auf PERIOD_M1 für weniger als 30 Minuten, PERIOD_M3 für weniger als eine Stunde, PERIOD_M10 für genau eine Stunde oder andernfalls PERIOD_H1, um auf detailliertere Daten für eine genaue Volumenverteilung zugreifen zu können. Wenn der aktuelle Zeitraum 60 Sekunden oder weniger beträgt, durchlaufen wir die vorhandenen Kursniveaus und suchen in den Volumenprofil-Arrays nach übereinstimmenden Kursen mit einer Toleranz durch MathAbs von einem halben Punkt; werden keine gefunden, passen wir die Größe der Arrays über ArrayResize an und fügen den Kurs und das Volumen hinzu oder summieren das Volumen, falls eine Übereinstimmung vorliegt.

Für höhere Zeitrahmen legen wir das Ende der Sitzung als festgelegte Endzeit oder als aktuelle Zeit aus TimeCurrent fest, ermitteln die Indizes für den Start- und Endbars mit iBarShift im niedrigeren Zeitrahmen und deklarieren und setzen, sofern gültig und der Zählwert positiv ist, die Serien-Arrays für Höchst-, Tiefst- und Volumendaten. Wir kopieren die Daten mithilfe von CopyHigh, CopyLow und CopyTickVolume ab der letzten Bar, anschließend iterieren wir durch die Bars: Hoch- und Tiefstwerte über QuantizePriceToGrid quantisieren. Wir berechnen die Kursstufen so, dass mindestens eine vorhanden ist, verteilen das Balkenvolumen gleichmäßig auf die einzelnen Stufen und durchlaufen den Kursbereich, um die Werte unter Berücksichtigung von Toleranzprüfungen in die Volumenprofil-Arrays hinzuzufügen bzw. zu akkumulieren.

Um den Kontrollpunkt für das Volumen zu ermitteln, setzen wir das maximale Volumen und den maximalen Kurs zunächst auf Null, durchsuchen das Volumen-Array, um den höchsten Wert zu ermitteln, setzen den Index zurück und ordnen den maximalen Kurs unter Berücksichtigung einer Toleranz dem nächstgelegenen Level-Index zu. Wird dieser gefunden, wird er gespeichert, wodurch die volumenbasierte Hervorhebung bei der Darstellung aktiviert wird. Wir können nun den VWAP anhand der Handelsvolumina der jeweiligen Kursniveaus berechnen. So wie die Ermittlung des Durchschnittspreises, gewichtet nach dem Handelsvolumen.

//+------------------------------------------------------------------+
//| Calculate session VWAP                                           |
//+------------------------------------------------------------------+
void CalculateSessionVwap(int sessionIndex) {
   if(!highlightSessionVwap) return;              //--- Return if not highlight VWAP
   if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return; //--- Return if index invalid
   
   double sumPriceVolume = 0;                     //--- Initialize sum price volume
   double sumVolume = 0;                          //--- Initialize sum volume
   
   int size = ArraySize(sessions[sessionIndex].levels); //--- Get levels size
   for(int i = 0; i < size; i++) {                //--- Loop through levels
      sumPriceVolume += sessions[sessionIndex].levels[i].price * sessions[sessionIndex].levels[i].volume; //--- Accumulate price volume
      sumVolume += sessions[sessionIndex].levels[i].volume; //--- Accumulate volume
   }
   
   if(sumVolume > 0)                              //--- Check if volume positive
      sessions[sessionIndex].vwap = sumPriceVolume / sumVolume; //--- Calculate VWAP
}

Um den volumengewichteten Durchschnittskurs für eine Handelssitzung zu ermitteln, implementieren wir die Funktion CalculateSessionVwap, die vorzeitig beendet wird, wenn die VWAP-Hervorhebung deaktiviert ist oder der Sitzungsindex ungültig ist. Wir richten Akkumulatoren für die Summe der Kurs-Volumen-Produkte und das Gesamtvolumen ein, durchlaufen dann die einzelnen Ebenen, um jeden Kurs mit seinem Volumen zu multiplizieren und so die Produktsumme zu ermitteln, und addieren schließlich die Volumina. Sofern das Gesamtvolumen größer als null ist, berechnen wir den gewichteten Durchschnitt und speichern ihn im Feld vwap der Sitzung als Quotient aus der Produktsumme und der Volumensumme. Damit stehen uns nun alle Hilfsfunktionen für die Berechnungen zur Verfügung; jetzt müssen wir uns nur noch um die visuelle Darstellung der Daten kümmern. Zunächst fügen wir eine Funktion hinzu, die das offene TPO-Zeichen hervorhebt, sofern es sich um einen Buchstaben handelt. So, als würde man den Anfangsbuchstaben ins Rampenlicht rücken. Nicht unbedingt erforderlich, erleichtert jedoch die schnelle visuelle Identifizierung.

//+------------------------------------------------------------------+
//| Render open TPO highlight                                        |
//+------------------------------------------------------------------+
void RenderOpenTpoHighlight(int sessionIndex, int openLevelIndex, string &displayStrings[]) {
   if(tpoCharacterType != ALPHABETIC) return;                          //--- Return if not alphabetic
   if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return; //--- Return if session invalid
   if(openLevelIndex < 0 || openLevelIndex >= ArraySize(sessions[sessionIndex].levels)) return; //--- Return if level invalid
   
   string fullString = displayStrings[openLevelIndex];                 //--- Get full display string
   if(StringLen(fullString) == 0) return;                              //--- Return if empty string
   
   string openCharacter = StringSubstr(fullString, 0, 1);              //--- Extract open character
   string remainingCharacters = StringSubstr(fullString, 1);           //--- Extract remaining characters
   
   string objectName = objectPrefix + "OpenTPO_" + 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[openLevelIndex].price, x, y); //--- Convert to XY
   
   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);              //--- 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, openColor);          //--- Set color
   ObjectSetInteger(0, objectName, OBJPROP_FONTSIZE, labelFontSize);   //--- Set font size
   ObjectSetString(0, objectName, OBJPROP_FONT, "Arial");              //--- Set font
   ObjectSetString(0, objectName, OBJPROP_TEXT, openCharacter);        //--- Set text
   ObjectSetInteger(0, objectName, OBJPROP_SELECTABLE, false);         //--- Set selectable false
   ObjectSetInteger(0, objectName, OBJPROP_HIDDEN, true);              //--- Set hidden true
   
   if(StringLen(remainingCharacters) > 0) {                            //--- Check if remaining
      displayStrings[openLevelIndex] = " " + remainingCharacters;      //--- Prepend space
   } else {                                                            //--- Handle no remaining
      displayStrings[openLevelIndex] = " ";                            //--- Set space
   }
}

Hier fügen wir die Funktion RenderOpenTpoHighlight hinzu, um das Zeichen Time Price Opportunity im alphabetischen Modus visuell hervorzuheben. Die Funktion bricht vorzeitig ab, wenn es sich nicht um ein alphabetisches Zeichen handelt oder wenn die Indizes für die Sitzung und den Eröffnungskurs im Hinblick auf die Array-Größen ungültig sind. Wir holen die Anzeigezeichenfolge für die offene Ebene ab und brechen ab, wenn sie gemäß StringLen leer ist; anschließend extrahieren wir mit StringSubstr das erste Zeichen als Öffnungsmarker und den Rest als verbleibenden Text. Ein Objektname wird gebildet, indem das Präfix mit der Startzeit der Sitzung über IntegerToString kombiniert wird, und wir erhalten den Bar-Index mit iBarShift, wobei im Falle einer Ungültigkeit ein Fehler zurückgegeben wird. Wir ermitteln die Beschriftungszeit über iTime, wandeln den Zeit-Preis mithilfe von ChartTimePriceToXY in Koordinaten um und erstellen, falls das Objekt laut ObjectFind fehlt, mit ObjectCreate und OBJ_LABEL eine Beschriftung.

Wir legen die Position, die Ecke, den Ankerpunkt, die Farbe für die Eröffnungsmarkierung, die dynamische Schriftgröße, die Schriftart Arial, den Text des Eröffnungszeichens sowie die Eigenschaften nicht auswählbar und ausgeblendet über ObjectSetInteger und ObjectSetString fest. Aktualisieren Sie abschließend die Anzeigezeichenfolge, indem Sie den verbleibenden Zeichen (sofern vorhanden) ein Leerzeichen voranstellen oder andernfalls ein einzelnes Leerzeichen setzen, um die Ausrichtung bei der Darstellung des Profils beizubehalten. Als Nächstes werden wir die Profilrandlinien, die Initial Balance-Linien und die Schlüsselpegelverlängerungen rendern.

//+------------------------------------------------------------------+
//| Render profile border line                                       |
//+------------------------------------------------------------------+
void RenderProfileBorderLine(int sessionIndex, int barIndex) {
   if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return; //--- Return if index invalid
   if(sessions[sessionIndex].sessionHigh == 0 || sessions[sessionIndex].sessionLow == 0) return; //--- Return if no high low
   
   datetime startTime = iTime(_Symbol, _Period, barIndex);             //--- Get start time
   
   color backgroundEdgeColor = 0x606D79;                               //--- Set background edge color
   color initialBalanceEdgeColor = initialBalanceHighlightColor;       //--- Set IB edge color
   
   if(initialBalancePeriods > 0 && sessions[sessionIndex].initialBalanceHigh > 0 && sessions[sessionIndex].initialBalanceLow > 0) { //--- Check IB
      
      string edge1Name = objectPrefix + "Edge1_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create edge1 name
      if(ObjectFind(0, edge1Name) < 0) {                               //--- Check if not found
         ObjectCreate(0, edge1Name, OBJ_TREND, 0, startTime, sessions[sessionIndex].sessionLow, 
                      startTime, sessions[sessionIndex].initialBalanceLow); //--- Create trend
         ObjectSetInteger(0, edge1Name, OBJPROP_RAY_RIGHT, false);     //--- Set ray right false
         ObjectSetInteger(0, edge1Name, OBJPROP_SELECTABLE, false);    //--- Set selectable false
         ObjectSetInteger(0, edge1Name, OBJPROP_HIDDEN, true);         //--- Set hidden true
      }
      ObjectSetDouble(0, edge1Name, OBJPROP_PRICE, 0, sessions[sessionIndex].sessionLow); //--- Set price 0
      ObjectSetDouble(0, edge1Name, OBJPROP_PRICE, 1, sessions[sessionIndex].initialBalanceLow); //--- Set price 1
      ObjectSetInteger(0, edge1Name, OBJPROP_COLOR, backgroundEdgeColor); //--- Set color
      ObjectSetInteger(0, edge1Name, OBJPROP_WIDTH, 3);                //--- Set width
      ObjectSetInteger(0, edge1Name, OBJPROP_STYLE, STYLE_SOLID);      //--- Set style
      
      string edge2Name = objectPrefix + "Edge2_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create edge2 name
      if(ObjectFind(0, edge2Name) < 0) {                               //--- Check if not found
         ObjectCreate(0, edge2Name, OBJ_TREND, 0, startTime, sessions[sessionIndex].initialBalanceLow, 
                      startTime, sessions[sessionIndex].initialBalanceHigh); //--- Create trend
         ObjectSetInteger(0, edge2Name, OBJPROP_RAY_RIGHT, false);     //--- Set ray right false
         ObjectSetInteger(0, edge2Name, OBJPROP_SELECTABLE, false);    //--- Set selectable false
         ObjectSetInteger(0, edge2Name, OBJPROP_HIDDEN, true);         //--- Set hidden true
      }
      ObjectSetDouble(0, edge2Name, OBJPROP_PRICE, 0, sessions[sessionIndex].initialBalanceLow);  //--- Set price 0
      ObjectSetDouble(0, edge2Name, OBJPROP_PRICE, 1, sessions[sessionIndex].initialBalanceHigh); //--- Set price 1
      ObjectSetInteger(0, edge2Name, OBJPROP_COLOR, initialBalanceEdgeColor);                     //--- Set color
      ObjectSetInteger(0, edge2Name, OBJPROP_WIDTH, 3);                //--- Set width
      ObjectSetInteger(0, edge2Name, OBJPROP_STYLE, STYLE_SOLID);      //--- Set style
      
      string edge3Name = objectPrefix + "Edge3_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create edge3 name
      if(ObjectFind(0, edge3Name) < 0) {                               //--- Check if not found
         ObjectCreate(0, edge3Name, OBJ_TREND, 0, startTime, sessions[sessionIndex].initialBalanceHigh, 
                      startTime, sessions[sessionIndex].sessionHigh);  //--- Create trend
         ObjectSetInteger(0, edge3Name, OBJPROP_RAY_RIGHT, false);     //--- Set ray right false
         ObjectSetInteger(0, edge3Name, OBJPROP_SELECTABLE, false);    //--- Set selectable false
         ObjectSetInteger(0, edge3Name, OBJPROP_HIDDEN, true);         //--- Set hidden true
      }
      ObjectSetDouble(0, edge3Name, OBJPROP_PRICE, 0, sessions[sessionIndex].initialBalanceHigh);   //--- Set price 0
      ObjectSetDouble(0, edge3Name, OBJPROP_PRICE, 1, sessions[sessionIndex].sessionHigh);          //--- Set price 1
      ObjectSetInteger(0, edge3Name, OBJPROP_COLOR, backgroundEdgeColor);                           //--- Set color
      ObjectSetInteger(0, edge3Name, OBJPROP_WIDTH, 3);                                             //--- Set width
      ObjectSetInteger(0, edge3Name, OBJPROP_STYLE, STYLE_SOLID);                                   //--- Set style
      
   } else {                                                                                         //--- Handle no IB
      string edgeName = objectPrefix + "Edge_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create edge name
      if(ObjectFind(0, edgeName) < 0) {                                                             //--- Check if not found
         ObjectCreate(0, edgeName, OBJ_TREND, 0, startTime, sessions[sessionIndex].sessionLow, 
                      startTime, sessions[sessionIndex].sessionHigh);                               //--- Create trend
         ObjectSetInteger(0, edgeName, OBJPROP_RAY_RIGHT, false);                                   //--- Set ray right false
         ObjectSetInteger(0, edgeName, OBJPROP_SELECTABLE, false);                                  //--- Set selectable false
         ObjectSetInteger(0, edgeName, OBJPROP_HIDDEN, true);                                       //--- Set hidden true
      }
      ObjectSetDouble(0, edgeName, OBJPROP_PRICE, 0, sessions[sessionIndex].sessionLow);            //--- Set price 0
      ObjectSetDouble(0, edgeName, OBJPROP_PRICE, 1, sessions[sessionIndex].sessionHigh);           //--- Set price 1
      ObjectSetInteger(0, edgeName, OBJPROP_COLOR, backgroundEdgeColor);                            //--- Set color
      ObjectSetInteger(0, edgeName, OBJPROP_WIDTH, 3);                                              //--- Set width
      ObjectSetInteger(0, edgeName, OBJPROP_STYLE, STYLE_SOLID);                                    //--- Set style
   }
}

//+------------------------------------------------------------------+
//| Render initial balance lines                                     |
//+------------------------------------------------------------------+
void RenderInitialBalanceLines(int sessionIndex, int barIndex) {
   if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return;       //--- Return if index invalid
   if(sessions[sessionIndex].initialBalanceHigh == 0 || sessions[sessionIndex].initialBalanceLow == 0) return; //--- Return if no IB
   
   datetime startTime = iTime(_Symbol, _Period, barIndex);                   //--- Get start time
   
   string initialBalanceHighName = objectPrefix + "IB_High_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create IB high name
   if(ObjectFind(0, initialBalanceHighName) < 0) { //--- Check if not found
      ObjectCreate(0, initialBalanceHighName, OBJ_TREND, 0, startTime, sessions[sessionIndex].initialBalanceHigh, 
                   startTime, sessions[sessionIndex].initialBalanceHigh);     //--- Create trend
      ObjectSetInteger(0, initialBalanceHighName, OBJPROP_RAY_RIGHT, false);  //--- Set ray right false
      ObjectSetInteger(0, initialBalanceHighName, OBJPROP_SELECTABLE, false); //--- Set selectable false
      ObjectSetInteger(0, initialBalanceHighName, OBJPROP_HIDDEN, true);      //--- Set hidden true
   }
   ObjectSetInteger(0, initialBalanceHighName, OBJPROP_COLOR, initialBalanceHighlightColor); //--- Set color
   ObjectSetInteger(0, initialBalanceHighName, OBJPROP_WIDTH, initialBalanceLineWidth);      //--- Set width
   
   string initialBalanceLowName = objectPrefix + "IB_Low_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create IB low name
   if(ObjectFind(0, initialBalanceLowName) < 0) { //--- Check if not found
      ObjectCreate(0, initialBalanceLowName, OBJ_TREND, 0, startTime, sessions[sessionIndex].initialBalanceLow, 
                   startTime, sessions[sessionIndex].initialBalanceLow);      //--- Create trend
      ObjectSetInteger(0, initialBalanceLowName, OBJPROP_RAY_RIGHT, false);   //--- Set ray right false
      ObjectSetInteger(0, initialBalanceLowName, OBJPROP_SELECTABLE, false);  //--- Set selectable false
      ObjectSetInteger(0, initialBalanceLowName, OBJPROP_HIDDEN, true);       //--- Set hidden true
   }
   ObjectSetInteger(0, initialBalanceLowName, OBJPROP_COLOR, initialBalanceHighlightColor); //--- Set color
   ObjectSetInteger(0, initialBalanceLowName, OBJPROP_WIDTH, initialBalanceLineWidth);      //--- Set width
}

//+------------------------------------------------------------------+
//| Render key level extensions                                      |
//+------------------------------------------------------------------+
void RenderKeyLevelExtensions(int sessionIndex, int barIndex, int valueAreaUpperIndex, int valueAreaLowerIndex) {
   if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return; //--- Return if index invalid
   if(ArraySize(sessions[sessionIndex].levels) == 0) return;           //--- Return if no levels
   
   datetime startTime = iTime(_Symbol, _Period, barIndex);             //--- Get start time
   
   bool isCurrentSession = (sessionIndex == ArraySize(sessions) - 1);  //--- Check if current session
   
   datetime endTime;                                                   //--- Declare end time
   bool rayRight;                                                      //--- Declare ray right
   
   if(isCurrentSession) {                                              //--- Handle current session
      endTime = startTime + PeriodSeconds(_Period) * 100;              //--- Set end time
      rayRight = true;                                                 //--- Set ray right true
   } else {                                                            //--- Handle past session
      if(sessionIndex + 1 < ArraySize(sessions)) {                     //--- Check next session
         int nextBarIndex = iBarShift(_Symbol, _Period, sessions[sessionIndex + 1].startTime); //--- Get next bar
         endTime = iTime(_Symbol, _Period, nextBarIndex);              //--- Set end time
      } else {                                                         //--- Handle last
         endTime = startTime + PeriodSeconds(_Period) * 100;           //--- Set end time
      }
      rayRight = false;                                                //--- Set ray right false
   }
   
   if(sessions[sessionIndex].volumeProfilePocIndex >= 0 && sessions[sessionIndex].volumeProfilePocIndex < ArraySize(sessions[sessionIndex].levels)) { //--- Check VP POC
      double pocPrice = sessions[sessionIndex].levels[sessions[sessionIndex].volumeProfilePocIndex].price; //--- Get POC price
      string pocExtensionName = objectPrefix + "POC_Ext_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create POC ext name
      if(ObjectFind(0, pocExtensionName) < 0) {                             //--- Check if not found
         ObjectCreate(0, pocExtensionName, OBJ_TREND, 0, startTime, pocPrice, endTime, pocPrice); //--- Create trend
         ObjectSetInteger(0, pocExtensionName, OBJPROP_SELECTABLE, false);  //--- Set selectable false
         ObjectSetInteger(0, pocExtensionName, OBJPROP_HIDDEN, true);       //--- Set hidden true
      } else {                                    //--- Handle existing
         ObjectSetInteger(0, pocExtensionName, OBJPROP_TIME, 0, startTime); //--- Set time 0
         ObjectSetDouble(0, pocExtensionName, OBJPROP_PRICE, 0, pocPrice);  //--- Set price 0
         ObjectSetInteger(0, pocExtensionName, OBJPROP_TIME, 1, endTime);   //--- Set time 1
         ObjectSetDouble(0, pocExtensionName, OBJPROP_PRICE, 1, pocPrice);  //--- Set price 1
      }
      ObjectSetInteger(0, pocExtensionName, OBJPROP_RAY_RIGHT, rayRight);   //--- Set ray right
      ObjectSetInteger(0, pocExtensionName, OBJPROP_COLOR, pocExtensionColor); //--- Set color
      ObjectSetInteger(0, pocExtensionName, OBJPROP_WIDTH, 2);              //--- Set width
      ObjectSetInteger(0, pocExtensionName, OBJPROP_STYLE, STYLE_SOLID);    //--- Set style
   }
   
   if(valueAreaUpperIndex >= 0 && valueAreaUpperIndex < ArraySize(sessions[sessionIndex].levels)) { //--- Check VAH
      double valueAreaHighPrice = sessions[sessionIndex].levels[valueAreaUpperIndex].price;         //--- Get VAH price
      string valueAreaHighExtensionName = objectPrefix + "VAH_Ext_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create VAH ext name
      if(ObjectFind(0, valueAreaHighExtensionName) < 0) {                                           //--- Check if not found
         ObjectCreate(0, valueAreaHighExtensionName, OBJ_TREND, 0, startTime, valueAreaHighPrice, endTime, valueAreaHighPrice); //--- Create trend
         ObjectSetInteger(0, valueAreaHighExtensionName, OBJPROP_SELECTABLE, false);                //--- Set selectable false
         ObjectSetInteger(0, valueAreaHighExtensionName, OBJPROP_HIDDEN, true);                     //--- Set hidden true
      } else {                                    //--- Handle existing
         ObjectSetInteger(0, valueAreaHighExtensionName, OBJPROP_TIME, 0, startTime);               //--- Set time 0
         ObjectSetDouble(0, valueAreaHighExtensionName, OBJPROP_PRICE, 0, valueAreaHighPrice);      //--- Set price 0
         ObjectSetInteger(0, valueAreaHighExtensionName, OBJPROP_TIME, 1, endTime);                 //--- Set time 1
         ObjectSetDouble(0, valueAreaHighExtensionName, OBJPROP_PRICE, 1, valueAreaHighPrice);      //--- Set price 1
      }
      ObjectSetInteger(0, valueAreaHighExtensionName, OBJPROP_RAY_RIGHT, rayRight);                 //--- Set ray right
      ObjectSetInteger(0, valueAreaHighExtensionName, OBJPROP_COLOR, valueAreaHighExtensionColor);  //--- Set color
      ObjectSetInteger(0, valueAreaHighExtensionName, OBJPROP_WIDTH, 1);                            //--- Set width
      ObjectSetInteger(0, valueAreaHighExtensionName, OBJPROP_STYLE, STYLE_DOT);                    //--- Set style
   }
   
   if(valueAreaLowerIndex >= 0 && valueAreaLowerIndex < ArraySize(sessions[sessionIndex].levels)) { //--- Check VAL
      double valueAreaLowPrice = sessions[sessionIndex].levels[valueAreaLowerIndex].price;          //--- Get VAL price
      string valueAreaLowExtensionName = objectPrefix + "VAL_Ext_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create VAL ext name
      if(ObjectFind(0, valueAreaLowExtensionName) < 0) {                                            //--- Check if not found
         ObjectCreate(0, valueAreaLowExtensionName, OBJ_TREND, 0, startTime, valueAreaLowPrice, endTime, valueAreaLowPrice); //--- Create trend
         ObjectSetInteger(0, valueAreaLowExtensionName, OBJPROP_SELECTABLE, false);                 //--- Set selectable false
         ObjectSetInteger(0, valueAreaLowExtensionName, OBJPROP_HIDDEN, true);                      //--- Set hidden true
      } else {                                                                                      //--- Handle existing
         ObjectSetInteger(0, valueAreaLowExtensionName, OBJPROP_TIME, 0, startTime);                //--- Set time 0
         ObjectSetDouble(0, valueAreaLowExtensionName, OBJPROP_PRICE, 0, valueAreaLowPrice);        //--- Set price 0
         ObjectSetInteger(0, valueAreaLowExtensionName, OBJPROP_TIME, 1, endTime);                  //--- Set time 1
         ObjectSetDouble(0, valueAreaLowExtensionName, OBJPROP_PRICE, 1, valueAreaLowPrice);        //--- Set price 1
      }
      ObjectSetInteger(0, valueAreaLowExtensionName, OBJPROP_RAY_RIGHT, rayRight);                  //--- Set ray right
      ObjectSetInteger(0, valueAreaLowExtensionName, OBJPROP_COLOR, valueAreaLowExtensionColor);    //--- Set color
      ObjectSetInteger(0, valueAreaLowExtensionName, OBJPROP_WIDTH, 1);                             //--- Set width
      ObjectSetInteger(0, valueAreaLowExtensionName, OBJPROP_STYLE, STYLE_DOT);                     //--- Set style
   }
   
   string highExtensionName = objectPrefix + "High_Ext_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create high ext name
   if(ObjectFind(0, highExtensionName) < 0) {                                                       //--- Check if not found
      ObjectCreate(0, highExtensionName, OBJ_TREND, 0, startTime, sessions[sessionIndex].sessionHigh, 
                   endTime, sessions[sessionIndex].sessionHigh);                                    //--- Create trend
      ObjectSetInteger(0, highExtensionName, OBJPROP_SELECTABLE, false);                            //--- Set selectable false
      ObjectSetInteger(0, highExtensionName, OBJPROP_HIDDEN, true);                                 //--- Set hidden true
      ObjectSetInteger(0, highExtensionName, OBJPROP_STYLE, STYLE_DOT);                             //--- Set style
   } else {                                                                                         //--- Handle existing
      ObjectSetInteger(0, highExtensionName, OBJPROP_TIME, 0, startTime);                           //--- Set time 0
      ObjectSetDouble(0, highExtensionName, OBJPROP_PRICE, 0, sessions[sessionIndex].sessionHigh);  //--- Set price 0
      ObjectSetInteger(0, highExtensionName, OBJPROP_TIME, 1, endTime);                             //--- Set time 1
      ObjectSetDouble(0, highExtensionName, OBJPROP_PRICE, 1, sessions[sessionIndex].sessionHigh);  //--- Set price 1
   }
   ObjectSetInteger(0, highExtensionName, OBJPROP_RAY_RIGHT, rayRight);       //--- Set ray right
   ObjectSetInteger(0, highExtensionName, OBJPROP_COLOR, highExtensionColor); //--- Set color
   ObjectSetInteger(0, highExtensionName, OBJPROP_WIDTH, 1);                  //--- Set width
   
   string lowExtensionName = objectPrefix + "Low_Ext_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create low ext name
   if(ObjectFind(0, lowExtensionName) < 0) {                                                                //--- Check if not found
      ObjectCreate(0, lowExtensionName, OBJ_TREND, 0, startTime, sessions[sessionIndex].sessionLow, 
                   endTime, sessions[sessionIndex].sessionLow);                                             //--- Create trend
      ObjectSetInteger(0, lowExtensionName, OBJPROP_SELECTABLE, false);                                     //--- Set selectable false
      ObjectSetInteger(0, lowExtensionName, OBJPROP_HIDDEN, true);                                          //--- Set hidden true
      ObjectSetInteger(0, lowExtensionName, OBJPROP_STYLE, STYLE_DOT);                                      //--- Set style
   } else {                                                                                                 //--- Handle existing
      ObjectSetInteger(0, lowExtensionName, OBJPROP_TIME, 0, startTime);                                    //--- Set time 0
      ObjectSetDouble(0, lowExtensionName, OBJPROP_PRICE, 0, sessions[sessionIndex].sessionLow);            //--- Set price 0
      ObjectSetInteger(0, lowExtensionName, OBJPROP_TIME, 1, endTime);                                      //--- Set time 1
      ObjectSetDouble(0, lowExtensionName, OBJPROP_PRICE, 1, sessions[sessionIndex].sessionLow);            //--- Set price 1
   }
   ObjectSetInteger(0, lowExtensionName, OBJPROP_RAY_RIGHT, rayRight);      //--- Set ray right
   ObjectSetInteger(0, lowExtensionName, OBJPROP_COLOR, lowExtensionColor); //--- Set color
   ObjectSetInteger(0, lowExtensionName, OBJPROP_WIDTH, 1);                 //--- Set width
   
   double midpointPrice = 0;                      //--- Initialize midpoint price
   if(midpointAlgorithm == HIGH_LOW_MID) {        //--- Check high low mid
      midpointPrice = sessions[sessionIndex].sessionHigh - (sessions[sessionIndex].sessionHigh - sessions[sessionIndex].sessionLow) / 2; //--- Calculate midpoint
   } else {                                       //--- Handle TPO count based
      int totalTpoCount = GetTotalTpoCount(sessionIndex); //--- Get total TPO
      int targetTpoCount = totalTpoCount / 2;     //--- Calculate target
      int currentTpoCount = 0;                    //--- Initialize current
      
      for(int i = 0; i < ArraySize(sessions[sessionIndex].levels); i++) { //--- Loop through levels
         currentTpoCount += sessions[sessionIndex].levels[i].tpoCount; //--- Accumulate TPO
         if(currentTpoCount >= targetTpoCount) {  //--- Check if reached
            midpointPrice = sessions[sessionIndex].levels[i].price; //--- Set midpoint
            break;                                //--- Exit loop
         }
      }
   }
   
   if(midpointPrice > 0) {                        //--- Check if midpoint set
      string midpointExtensionName = objectPrefix + "Mid_Ext_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create mid ext name
      if(ObjectFind(0, midpointExtensionName) < 0) { //--- Check if not found
         ObjectCreate(0, midpointExtensionName, OBJ_TREND, 0, startTime, midpointPrice, endTime, midpointPrice); //--- Create trend
         ObjectSetInteger(0, midpointExtensionName, OBJPROP_SELECTABLE, false);  //--- Set selectable false
         ObjectSetInteger(0, midpointExtensionName, OBJPROP_HIDDEN, true);       //--- Set hidden true
         ObjectSetInteger(0, midpointExtensionName, OBJPROP_STYLE, STYLE_DOT);   //--- Set style
      } else {                                                                   //--- Handle existing
         ObjectSetInteger(0, midpointExtensionName, OBJPROP_TIME, 0, startTime); //--- Set time 0
         ObjectSetDouble(0, midpointExtensionName, OBJPROP_PRICE, 0, midpointPrice); //--- Set price 0
         ObjectSetInteger(0, midpointExtensionName, OBJPROP_TIME, 1, endTime);   //--- Set time 1
         ObjectSetDouble(0, midpointExtensionName, OBJPROP_PRICE, 1, midpointPrice); //--- Set price 1
      }
      ObjectSetInteger(0, midpointExtensionName, OBJPROP_RAY_RIGHT, rayRight);   //--- Set ray right
      ObjectSetInteger(0, midpointExtensionName, OBJPROP_COLOR, midpointExtensionColor); //--- Set color
      ObjectSetInteger(0, midpointExtensionName, OBJPROP_WIDTH, 1);              //--- Set width
   }
}

Wir führen die Funktion RenderProfileBorderLine ein, um vertikale Begrenzungslinien zu zeichnen, die die Kursspanne der Sitzung im Chart umrahmen, den Sitzungsindex zu überprüfen und sicherzustellen, dass Höchst- und Tiefstkurse vorliegen, bevor wir fortfahren. Wir ermitteln die Startzeit mit iTime und legen die Farben für die Hintergrundränder und die Hervorhebungen der Initial Balance fest. Wenn die Initial Balanceperioden positiv sind und Werte festgelegt wurden, erstellen oder aktualisieren wir mithilfe von ObjectCreate mit OBJ_TREND drei Trendliniensegmente: eines vom Sitzungstief bis zur Untergrenze der Initial Balance mit Hintergrundfarbe, ein weiteres vom Tiefststand der Initial Balance zum Höchststand mit der Hervorhebungsfarbe und das letzte vom Höchststand der Initial Balance zum Sitzungshöchststand mit Hintergrundfarbe, jeweils konfiguriert über ObjectFind, ObjectSetDouble für Kurse, ObjectSetInteger für keine Verlängerung nach rechts, nicht auswählbar, ausgeblendet, durchgezogener Stil und Breite 3. Andernfalls, wenn keine Initial Balance vorliegt, zeichnen wir eine einzelne Trendlinie vom Tiefstwert zum Höchstwert mit der Hintergrundfarbe und denselben Eigenschaften.

Anschließend visualisiert die Funktion RenderInitialBalanceLines den Bereich der Initial Balance. Dazu wird der Startzeitpunkt abgerufen und es werden mithilfe von ObjectCreate mit OBJ_TREND an denselben Koordinaten zwei horizontale Trendlinien für Höchst- und Tiefstwerte erstellt bzw. aktualisiert, ohne Strahlverlängerung, die Linien nicht auswählbar und ausgeblendet sind, Eingabe-Hervorhebungsfarbe und Linienbreite.

Um wichtige Niveaus in die Zukunft zu projizieren, implementieren wir die Funktion RenderKeyLevelExtensions: Zunächst prüfen wir die Gültigkeit und ermitteln die Startzeit. Anschließend stellen wir fest, ob es sich um die aktuelle Sitzung handelt, um festzulegen, ob die Linie nach rechts verlängert werden soll, oder ob es sich um vergangene Sitzungen ohne Linienprojektion handelt, für die wir den Start der nächsten Sitzung als Endzeit verwenden. Für die Berechnungen nutzen wir dabei PeriodSeconds, TimeCurrent und iBarShift. Für den Kontrollpunkt des Volumenprofils zeichnen wir, sofern dieser gültig ist, eine durchgezogene Linie auf dem entsprechenden Kurs mit der Farbe der Eingabeverlängerung und einer Breite von 2. Ebenso gilt für die oberen und unteren Wertbereiche: Befinden sich die Indizes innerhalb des Bereichs, werden gepunktete Linien in den entsprechenden Farben und mit einer Breite von 1 erstellt.

Wir fügen bei den Eingabefarben stets gepunktete Erweiterungen für die höchsten und tiefsten Werte der Sitzung hinzu. Den Mittelpunkt berechnen wir anhand der Enumeration midpointAlgorithm – entweder als einfachen Bereichsmittelpunkt oder durch Summieren der Time Price Opportunity-Zählwerte über GetTotalTpoCount, um das Halbwegsniveau zu ermitteln – und wenn das Ergebnis positiv ist, wird eine gepunktete Linie in der Farbe des Mittelpunkts gezeichnet, wobei Sie für Zeiten ObjectSetInteger und ObjectSetDouble für Preise, Strahlen, Stile und andere Eigenschaften verwenden, nachdem diese nach dem gleichen Ansatz wie die anderen überprüft oder erstellt wurden. Als Nächstes müssen wir eine Logik definieren, um im festen Modus die Volumenwerte neben den Pegeln anzuzeigen, wobei die Farbe von der Intensität abhängt. So wie die Angabe, wie gefragt das Preisniveau war.

//+------------------------------------------------------------------+
//| Render fixed range volume labels                                 |
//+------------------------------------------------------------------+
void RenderFixedRangeVolumeLabels(int sessionIndex, int barIndex) {
   if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return;     //--- Return if index invalid
   if(ArraySize(sessions[sessionIndex].volumeProfilePrices) == 0) return;  //--- Return if no VP prices
   
   double maxVolume = 0;                                                   //--- Initialize max volume
   for(int i = 0; i < ArraySize(sessions[sessionIndex].volumeProfileVolumes); i++) { //--- Loop through volumes
      if(sessions[sessionIndex].volumeProfileVolumes[i] > maxVolume)       //--- Check if max
         maxVolume = sessions[sessionIndex].volumeProfileVolumes[i];       //--- Update max
   }
   
   if(maxVolume == 0) return;                                              //--- Return if no volume
   
   datetime labelTime = iTime(_Symbol, _Period, barIndex);                 //--- Get label time
   
   for(int i = 0; i < ArraySize(sessions[sessionIndex].volumeProfilePrices); i++) { //--- Loop through VP prices
      double price = sessions[sessionIndex].volumeProfilePrices[i];        //--- Get price
      double volumeValue = sessions[sessionIndex].volumeProfileVolumes[i]; //--- Get volume
      double ratio = volumeValue / maxVolume;     //--- Calculate ratio
      
      color volumeColor;                          //--- Declare color
      int percent = (int)(100 * ratio);           //--- Calculate percent
      
      if(percent <= 8) {                          //--- Check low percent
         volumeColor = singlePrintColor;          //--- Set single print color
      } else {                                    //--- Handle higher
         int transparency = (int)MathMax(0, 80 - percent);                     //--- Calculate transparency
         volumeColor = ApplyTransparencyToColor(valueAreaColor, transparency); //--- Apply transparency
      }
      
      string objectName = objectPrefix + "Vol_" + IntegerToString(sessions[sessionIndex].startTime) + "_" + IntegerToString(i); //--- Create object name
      
      int x, y;                                           //--- Declare coordinates
      ChartTimePriceToXY(0, 0, labelTime, price, x, y);   //--- Convert to XY
      
      if(ObjectFind(0, objectName) < 0) {                 //--- Check if not found
         ObjectCreate(0, objectName, OBJ_LABEL, 0, 0, 0); //--- Create label
      }
      
      ObjectSetInteger(0, objectName, OBJPROP_XDISTANCE, x - 50);         //--- 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_RIGHT);      //--- Set anchor
      ObjectSetInteger(0, objectName, OBJPROP_COLOR, volumeColor);        //--- Set color
      ObjectSetInteger(0, objectName, OBJPROP_FONTSIZE, labelFontSize);   //--- Set font size
      ObjectSetString(0, objectName, OBJPROP_FONT, "Arial");              //--- Set font
      ObjectSetString(0, objectName, OBJPROP_TEXT, IntegerToString((int)MathRound(volumeValue))); //--- Set text
      ObjectSetInteger(0, objectName, OBJPROP_SELECTABLE, false);         //--- Set selectable false
      ObjectSetInteger(0, objectName, OBJPROP_HIDDEN, true);              //--- Set hidden true
   }
}

//+------------------------------------------------------------------+
//| Apply transparency to color                                      |
//+------------------------------------------------------------------+
color ApplyTransparencyToColor(color baseColor, int transparency) {
   int red = (int)(baseColor & 0xFF);             //--- Extract red
   int green = (int)((baseColor >> 8) & 0xFF);    //--- Extract green
   int blue = (int)((baseColor >> 16) & 0xFF);    //--- Extract blue
   
   transparency = (int)MathMin(100, MathMax(0, transparency)); //--- Clamp transparency
   int alpha = 255 - (transparency * 255 / 100); //--- Calculate alpha
   
   return (color)((alpha << 24) | (blue << 16) | (green << 8) | red); //--- Return color with alpha
}

Wir implementieren die Funktion RenderFixedRangeVolumeLabels, um im Modus mit festen Zeitrahmen Volumenwerte als Beschriftungen neben dem Profil anzuzeigen. Dabei überprüfen wir zunächst den Sitzungsindex und stellen sicher, dass Preise für das Volumenprofil vorhanden sind, bevor wir fortfahren. Wir ermitteln das maximale Volumen, indem wir das Volumen-Array durchlaufen und den höchsten Wert aktualisieren; bei Null brechen wir den Vorgang ab, um Probleme bei der Division zu vermeiden. Nachdem die Zeitangabe über iTime abgerufen wurde, durchlaufen wir jeden Eintrag des Volumenprofils: berechnen das Verhältnis zum Maximalwert, leiten daraus einen Prozentsatz ab und legen die Beschriftungsfarbe fest – dabei wird bei 8 % oder weniger die Farbe für Single Prints verwendet, bei höheren Werten wird mit ApplyTransparencyToColor Transparenz auf die Farbe des Wertebereichs angewendet, wobei die Transparenz basierend auf dem Prozentsatz über die Funktion MathMax umgekehrt proportional von 0 bis 80 skaliert wird.

Für jedes Objekt erstellen wir einen eindeutigen Objektnamen, der aus Präfix, Startzeit und Index besteht, und zwar mithilfe von IntegerToString; wir wandeln Zeit-Preis-Werte mithilfe von ChartTimePriceToXY in Koordinaten um; wir erstellen eine Beschriftung mit ObjectCreate und OBJ_LABEL (falls laut ObjectFind noch nicht vorhanden) und konfigurieren es mit einem X-Versatz nach links, der oberen linken Ecke als Ankerpunkt, einer berechneten Farbe, dynamischer Schriftgröße, der Schriftart Arial, gerundetem Volumentext aus MathRound und IntegerToString sowie nicht auswählbaren/ausgeblendeten Eigenschaften, die über die Funktionen ObjectSetInteger und ObjectSetString festgelegt werden.

Um eine variable Deckkraft in Grafiken zu unterstützen, definieren wir die Funktion ApplyTransparencyToColor, die mithilfe von bitweiser UND und shifts die Rot-, Grün- und Blaukomponenten aus der Grundfarbe extrahiert. Wir begrenzen die Eingabetransparenz mit MathMin und MathMax auf einen Wert zwischen 0 und 100, berechnen den Alpha-Wert als 255 minus des skalierten Prozentsatzes und geben eine neue Farbe zurück, indem wir den Alpha-Wert um 24 Bit nach links verschieben und mit der Blau-Grün-Rot-Anordnung ODER-verknüpfen. Schließlich können wir für festgelegte Zeiträume ein farbiges Hintergrundrechteck einzeichnen, um Profile mit festem Bereich im Chart optisch hervorzuheben.

//+------------------------------------------------------------------+
//| Render fixed range background rectangle                          |
//+------------------------------------------------------------------+
void RenderFixedRangeBackgroundRectangle(int sessionIndex) {
   if(profileTimeframe != FIXED) return;                               //--- Return if not fixed
   if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return; //--- Return if index invalid
   
   string objectName = objectPrefix + "FixedBG_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create object name
   
   if(ObjectFind(0, objectName) < 0) {                                 //--- Check if not found
      ObjectCreate(0, objectName, OBJ_RECTANGLE, 0, sessions[sessionIndex].startTime, sessions[sessionIndex].sessionHigh,
                   sessions[sessionIndex].endTime > 0 ? sessions[sessionIndex].endTime : TimeCurrent(),
                   sessions[sessionIndex].sessionLow);                 //--- Create rectangle
      ObjectSetInteger(0, objectName, OBJPROP_SELECTABLE, false);      //--- Set selectable false
      ObjectSetInteger(0, objectName, OBJPROP_HIDDEN, true);           //--- Set hidden true
      ObjectSetInteger(0, objectName, OBJPROP_BACK, true);             //--- Set back true
   }
   
   ObjectSetInteger(0, objectName, OBJPROP_COLOR, fixedRangeBackgroundColor); //--- Set color
   ObjectSetInteger(0, objectName, OBJPROP_FILL, true); //--- Set fill true
}

Wir fügen die Funktion RenderFixedRangeBackgroundRectangle hinzu, um für Profile mit festem Zeitrahmen einen ausgefüllten Hintergrund zu zeichnen und so die visuelle Unterscheidung im Chart zu verbessern. Die Funktion bricht vorzeitig ab, wenn der Zeitrahmen nicht FIXED ist oder der Sitzungsindex gemäß den Überprüfungen der Array-Größe ungültig ist. Wir bilden den Objektnamen aus dem Präfix und der Startzeit der Sitzung mithilfe von IntegerToString; falls laut ObjectFind noch kein Objekt vorhanden ist, erstellen wir mit ObjectCreate und OBJ_RECTANGLE ein Rechteck von der Startzeit der Sitzung auf Höhe des Sitzungshochs bis zur Endzeit auf Höhe des Sitzungstiefs (mit Ausweichwert TimeCurrent, falls die Endzeit Null ist) am unteren Rand der Sitzung, wobei wir es als nicht auswählbar, ausgeblendet und hinter anderen Elementen gezeichnet festlegen. Unabhängig davon legen wir die Hintergrundfarbe für den festgelegten Eingabebereich fest und aktivieren die einheitliche Füllung mit ObjectSetInteger, um einen farbigen Hintergrund für den Profilbereich zu schaffen. Wir können diese Funktionen nun in der Master-Funktion für die Profil-Darstellung aufrufen, um die rechenintensiven Aufgaben zu erledigen, sodass wir die gesamte Logik einfach im Ereignis-Handler für die Tick-Berechnung aufrufen.

//+------------------------------------------------------------------+
//| 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
   
   PadLevelsForSplitProfile(sessionIndex);        //--- Pad levels for split
   SortPriceLevelsDescending(sessionIndex);       //--- Sort levels descending
   CalculatePointOfControl(sessionIndex);         //--- Calculate POC
   BuildVolumeProfileAndFindPoc(sessionIndex);    //--- Build VP and find POC
   
   int totalTpoCount = GetTotalTpoCount(sessionIndex); //--- Get total TPO count
   int pointOfControlIndex = (useVolumeProfilePocForValueArea && sessions[sessionIndex].volumeProfilePocIndex >= 0) ? 
                             sessions[sessionIndex].volumeProfilePocIndex : sessions[sessionIndex].pointOfControlIndex; //--- Select 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 openLevelIndex  = -1;                      //--- Initialize open level index
   int closeLevelIndex = -1;                      //--- Initialize close level index
   
   if(tpoCharacterType == ALPHABETIC) {           //--- Check alphabetic
      double openPrice  = sessions[sessionIndex].sessionOpen; //--- Get open price
      double closePrice = sessions[sessionIndex].sessionClose; //--- Get close price
      
      for(int i = 0; i < size; i++) {             //--- Loop to find levels
         if(openLevelIndex < 0  && MathAbs(sessions[sessionIndex].levels[i].price - openPrice)  < tpoPriceGridStep / 2) //--- Check open match
            openLevelIndex = i;                   //--- Set open index
         if(closeLevelIndex < 0 && MathAbs(sessions[sessionIndex].levels[i].price - closePrice) < tpoPriceGridStep / 2) //--- Check close match
            closeLevelIndex = i;                  //--- Set close index
      }
      
      RenderOpenTpoHighlight(sessionIndex, openLevelIndex, displayStrings); //--- Render open highlight
      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 && sessions[sessionIndex].volumeProfilePocIndex != sessions[sessionIndex].pointOfControlIndex) { //--- Check TPO POC
         textColor = pointOfControlColor;         //--- Set POC color
      }
      
      if(highlightVolumeProfilePoc && i == sessions[sessionIndex].volumeProfilePocIndex) { //--- Check VP POC
         textColor = volumeProfilePocColor;       //--- Set VP POC color
      }
      
      if(highlightSessionVwap && MathAbs(sessions[sessionIndex].levels[i].price - sessions[sessionIndex].vwap) < tpoPriceGridStep / 2) { //--- Check VWAP
         textColor = sessionVwapColor;            //--- Set VWAP 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
   RenderProfileBorderLine(sessionIndex, barIndex); //--- Render profile border
   
   if(initialBalancePeriods > 0)                    //--- Check if IB periods
      RenderInitialBalanceLines(sessionIndex, barIndex); //--- Render IB lines
   
   if(showExtensionLines)                           //--- Check if show extensions
      RenderKeyLevelExtensions(sessionIndex, barIndex, valueAreaUpperIndex, valueAreaLowerIndex); //--- Render extensions
   
   if(profileTimeframe == FIXED && renderVolumes)   //--- Check fixed and render volumes
      RenderFixedRangeVolumeLabels(sessionIndex, barIndex); //--- Render volume labels
   
   RenderFixedRangeBackgroundRectangle(sessionIndex); //--- Render fixed background
}

Hier erweitern wir die Funktion RenderSessionProfile, um erweiterte Funktionen zur Darstellung des Marktprofils zu integrieren. Zunächst werden Index- und Datenvalidierungen durchgeführt, um den Vorgang abzubrechen, falls die Daten ungültig sind, keine Preisstufen vorliegen oder keine Startzeit angegeben wurde. Anschließend wird der Bar-Index über die Funktion iBarShift abgerufen. Wir bereiten das Profil vor, indem wir PadLevelsForSplitProfile für die Ausrichtung im Split-Modus aufrufen, die Preisniveaus mit SortPriceLevelsDescending absteigend sortieren, berechnen den Time-Price-Opportunity-Kontrollpunkt mit CalculatePointOfControl und erstellen das Volumenprofil, um dessen Kontrollpunkt über BuildVolumeProfileAndFindPoc zu ermitteln.

Nachdem wir die Gesamtanzahl der Time-Price-Opportunities über GetTotalTpoCount ermittelt haben, wählen wir den Kontrollpunktindex bedingt aus – wenn useVolumeProfilePocForValueArea wahr ist und ein Volumen-Kontrollpunkt vorhanden ist, wird dieser verwendet; andernfalls greifen wir auf den Time Price Opportunity-Kontrollpunkt zurück – anschließend initialisieren und erweitern wir die Indizes des Wertbereichs um diesen herum und akkumulieren die Zählwerte in einer Schleife, wobei wir die benachbarte Ebene mit höheren „Time Price Opportunities“ bevorzugen, bis das auf der Grundlage von valueAreaPercent festgelegte Ziel erreicht ist. Um die grafische Darstellung einzurichten, passen wir die Länge der Time Price Opportunity-Zeichenfolgen an und kopieren sie mit ArrayResize in ein Anzeige-Array. Im alphabetischen Modus werden die Indizes für offene und geschlossene Ebenen ermittelt, indem die Ebenen mit einer Toleranz MathAbs in Höhe der halben Rasterschrittweite abgetastet werden, und die Hervorhebungen werden durch Aufruf von RenderOpenTpoHighlight und RenderCloseTpoHighlight gerendert.

In der Haupt-Rendering-Schleife für jedes Level erstellen wir einen eindeutigen Objektnamen, beginnen mit der Standardfarbe und überschreiben diese bei Single Prints, für den Wertebereich, den Kontrollpunkt Time Price Opportunity, den Kontrollpunkt Highlighted Volume oder bei engen Übereinstimmungen mit dem gewichteten Durchschnittspreis unter Verwendung von Toleranzprüfungen. Wir erstellen oder aktualisieren Beschriftungen mit ObjectCreate und OBJ_LABEL, falls diese gemäß ObjectFind fehlen, wandeln den Zeit-Preis über ChartTimePriceToXY in Koordinaten um, nachdem wir die Beschriftungszeit aus iTime abgerufen haben, und wenden Einstellungen für Position, Ecken, Anker, Farbe, Schriftgröße, Schriftart und Anzeigetext sowie Nicht-auswählbar/Ausgeblendet an.

Abschließend fügen wir zusätzliche Elemente hinzu, indem wir RenderOpenCloseMarkers, RenderProfileBorderLine, RenderInitialBalanceLines (sofern Zeiträume festgelegt sind), RenderKeyLevelExtensions mit Indizes des Wertbereichs (sofern Erweiterungen aktiviert sind), RenderFixedRangeVolumeLabels für feste Zeitrahmen mit Volumina und RenderFixedRangeBackgroundRectangle für Hintergründe verwenden. Nun müssen wir nur noch die Ereignisbehandlung für die Tick-Berechnung anpassen, um H/L/O und den IB der Periode zu speichern, sofern diese innerhalb der Perioden liegen, das Volumen zu den Kursen hinzuzufügen und den VWAP für die gesamte Berechnung aufzurufen.

//+------------------------------------------------------------------+
//| 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].periodHighs, periodIndex + 1); //--- Resize period highs
      ArrayResize(sessions[activeSessionIndex].periodLows, periodIndex + 1);  //--- Resize period lows
      ArrayResize(sessions[activeSessionIndex].periodOpens, periodIndex + 1); //--- Resize period opens
      
      sessions[activeSessionIndex].periodHighs[periodIndex] = high[rates_total - 1]; //--- Set period high
      sessions[activeSessionIndex].periodLows[periodIndex] = low[rates_total - 1];   //--- Set period low
      sessions[activeSessionIndex].periodOpens[periodIndex] = open[rates_total - 1]; //--- Set period open
      sessions[activeSessionIndex].periodCount++; //--- Increment period count
      
      if(periodIndex < initialBalancePeriods) {   //--- Check if within IB periods
         if(periodIndex == 0) {                   //--- Handle first period
            sessions[activeSessionIndex].initialBalanceHigh = high[rates_total - 1]; //--- Set IB high
            sessions[activeSessionIndex].initialBalanceLow = low[rates_total - 1];   //--- Set IB low
         } else {                                 //--- Handle subsequent
            sessions[activeSessionIndex].initialBalanceHigh = MathMax(sessions[activeSessionIndex].initialBalanceHigh, high[rates_total - 1]); //--- Update IB high
            sessions[activeSessionIndex].initialBalanceLow = MathMin(sessions[activeSessionIndex].initialBalanceLow, low[rates_total - 1]); //--- Update IB low
         }
      }
      
      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
            sessions[activeSessionIndex].levels[levelIndex].volume += (double)tick_volume[rates_total - 1] / 
                                                       MathMax(1, (quantizedHigh - quantizedLow) / tpoPriceGridStep + 1); //--- Add volume
         }
      }
      
      CalculateSessionVwap(activeSessionIndex);   //--- Calculate VWAP
      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
}

Im Ereignis-Handler OnCalculatepassen wir die Logik an, um die neuen Funktionen wie die Erfassung der Initial Balance und die Volumenakkumulation während der Datenverarbeitung zu integrieren. Dabei geben wir zunächst den Wert Null zurück, wenn nicht genügend Kursdaten/Bars vorliegen. Wir extrahieren die aktuelle Barzeit und prüfen, ob eine neue Bar vorliegt. Wenn dann gemäß IsNewSessionStarted eine neue Sitzung beginnt oder keine vorherige Zeit vorliegt, legen wir die Endzeit der aktiven Sitzung fest und rendern sie mit RenderSessionProfile, sofern sie gemäß ArraySize gültig ist. Anschließend erstellen wir mit CreateNewSession eine neue Sitzung, initialisieren die Kernfelder anhand der aktuellen Bars und aktualisieren die Zeit der zuletzt abgeschlossenen Sitzung – genau wie bei der vorherigen Version.

Nachdem die Zeit der vorherigen Kerze festgelegt wurde, aktualisieren wir – sofern eine neue, über IsBarEligibleForProcessing als verarbeitungsfähig erkannte Kerze und eine aktive Sitzung vorhanden sind – die Sitzungshöchst- und -tiefstwerte mit MathMax und MathMin sowie den Schlusskurs, passen die Größe an und speichern die Periodenhöchst- und Tiefst- und Eröffnungskurse in ihren Arrays, erhöhen die Periodenzahl und legen – sofern dies innerhalb von initialBalancePeriods liegt – den Initial Balancehöchst- und -tiefstwert basierend auf dem Periodenindex fest oder aktualisieren ihn. Wir quantisieren das Hoch und das Tief der Bar, durchlaufen den Bereich, um mit GetOrCreatePriceLevel Preisniveaus abzurufen oder zu erstellen, fügen über AddTpoCharacterToLevel Zeit-Preis-Charakteristika hinzu und akkumulieren das Volumen proportional, indem wir das Tick-Volumen durch die Kursschritte dividieren – wobei mit MathMax sichergestellt wird, dass mindestens ein Wert vorhanden ist –, bevor wir den gewichteten Durchschnittskurs über CalculateSessionVwap berechnen und den Zeitpunkt des letzten Abschlusses aktualisieren. Wenn die Bar die Voraussetzungen erfüllt, aktualisieren wir den Schlusskurs, den Höchstkurs und den Tiefstkurs der Sitzung für Echtzeit-Updates, stellen anschließend alle Sitzungen in einer Schleife dar und geben die Gesamtkurse zurück. Nach dem Kompilieren erhalten wir folgendes Ergebnis:

HYBRIDES VOLUMENBASIERTES MARKTPROFIL – PUNKTZEICHEN

Aus der Abbildung geht hervor, dass wir den Indikator durch Hinzufügen der volumenbasierten Profil-Logik und einer verbesserten Visualisierung aktualisiert haben, wodurch eine Hybridversion entstanden ist und wir somit unsere Ziele erreicht haben. Bleibt nur noch der Backtest des Programms, und das wird im nächsten Abschnitt behandelt.


Backtests

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

BACKTEST-GIF


Schlussfolgerung

Zusammenfassend lässt sich sagen, dass wir den hybriden Indikator Time Price Opportunity (TPO) in MQL5 durch die Integration von Volumendaten erweitert haben, um volumenbasierte Kontrollpunkte, Wertbereiche und den volumengewichteten Durchschnittspreis mit anpassbarer Hervorhebung zu berechnen. Das System bietet erweiterte Funktionen wie die Erkennung der Initial Balance, Projektionslinien wichtiger Kursniveaus, geteilte Profile, alternative TPO-Symbole wie Quadrate oder Kreise, Begrenzungslinien, Hintergründe mit festem Bereich sowie dynamische Volumenbeschriftungen für eine umfassende visuelle Analyse über verschiedene Zeiträume hinweg. Mit diesem verbesserten hybriden Time Price Opportunity-Marktprofilindikator sind Sie in der Lage, tiefere Einblicke in die Marktstruktur und die Volumendynamik zu gewinnen – und damit bestens gerüstet für die weitere Optimierung Ihrer Trading-Strategie. Viel Spaß beim Handeln!

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

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.
Larry Williams’ Marktgeheimnisse (Teil 12): Kontextbezogener Handel von „Smash Day“-Umkehrungen Larry Williams’ Marktgeheimnisse (Teil 12): Kontextbezogener Handel von „Smash Day“-Umkehrungen
Dieser Artikel zeigt, wie man die Umkehrmuster nach Larry Williams’ Smash Day in MQL5 innerhalb eines strukturierten Kontexts automatisieren kann. Wir implementieren einen Expert Advisor, der Setups über ein begrenztes Zeitfenster validiert, Einstiegspunkte anhand der auf dem Supertrend basierenden Trendrichtung und Wochentagsfiltern abstimmt und Einstiege entweder beim Durchbruch eines Niveaus oder nach bestätigtem Schlusskurs der Bar unterstützt. Der Code erlaubt jeweils nur eine offene Position und unterstützt sowohl risikobasierte als auch feste Positionsgrößen. Es werden eine schrittweise Entwicklung, ein Backtesting-Verfahren sowie reproduzierbare Einstellungen bereitgestellt.
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.
Behebung von Barrierefreiheitsproblemen bei MQL5-Handelswerkzeugen (Teil II): Einen Expert Advisor per Pythons Text-to-Speech sprachfähig machen Behebung von Barrierefreiheitsproblemen bei MQL5-Handelswerkzeugen (Teil II): Einen Expert Advisor per Pythons Text-to-Speech sprachfähig machen
Lassen Sie uns besprechen, wie wir unsere Expert Advisors mithilfe von Text-to-Speech-Technologie sprachfähig machen können, indem wir Python und MQL5 kombinieren. Nachdem Sie diesen Artikel gelesen haben, verfügen Sie über ein funktionierendes Beispiel für einen EA, der dynamische Marktinformationen ausgibt. Sie werden den Einsatz von TTS und der WebRequest-Funktion beherrschen und lernen, wie sich Python-Bibliotheken in die Sprache MQL5 integrieren lassen, um ein wirklich sprachunterstütztes Trading-Tool zu entwickeln.