Erstellen benutzerdefinierter Indikatoren in MQL5 (Teil 8): Integration von Volumendaten für eine tiefere Marktprofilanalyse
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:
- Einbindung von Handelsvolumen und erweiterten Funktionen in hybride Marktprofile für Time Price Opportunity (TPO)
- Implementierung in MQL5
- Backtests
- 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.

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:

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.

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
Warnung: Alle Rechte sind von MetaQuotes Ltd. vorbehalten. Kopieren oder Vervielfältigen untersagt.
Dieser Artikel wurde von einem Nutzer der Website verfasst und gibt dessen persönliche Meinung wieder. MetaQuotes Ltd übernimmt keine Verantwortung für die Richtigkeit der dargestellten Informationen oder für Folgen, die sich aus der Anwendung der beschriebenen Lösungen, Strategien oder Empfehlungen ergeben.
Die Übertragung der Trading-Signale in einem universalen Expert Advisor.
Larry Williams’ Marktgeheimnisse (Teil 12): Kontextbezogener Handel von „Smash Day“-Umkehrungen
Eine alternative Log-datei mit der Verwendung der HTML und CSS
Behebung von Barrierefreiheitsproblemen bei MQL5-Handelswerkzeugen (Teil II): Einen Expert Advisor per Pythons Text-to-Speech sprachfähig machen
- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.