Erstellen benutzerdefinierter Indikatoren in MQL5 (Teil 7): Hybride Time-Price-Opportunity-(TPO)-Marktprofile für die Analyse von Handelssitzungen
Einführung
In unserem vorherigen Artikel (Teil 6) haben wir ein weiterentwickeltes Berechnungssystem für den Relative Strength Index in MetaQuotes Language 5 (MQL5) entwickelt, das ein Glättungsverfahren, Farbverschiebungen zur visuellen Verbesserung sowie die Unterstützung mehrerer Zeitrahmen für eine umfassende Marktanalyse umfasst. In Teil 7 entwickeln wir den hybriden Marktprofil-Indikator Time-Price-Opportunity (TPO), der verschiedene Zeitrahmen für Handelssitzungen unterstützt, darunter Intraday, täglich, wöchentlich, monatlich sowie feste Zeitintervalle mit Zeitzonenanpassungen. Dieser Indikator unterteilt die Kurse in ein Raster, verwaltet Sitzungsdaten zu Höchst-, Tiefst-, Eröffnungs- und Schlusskursen, berechnet den Point of Control (POC) und den Wertebereich (value area) anhand von TPO-Zählwerten und stellt die Ergebnisse im Chart mit anpassbaren Farben dar, um eine detaillierte Analyse von Handelssitzungen zu ermöglichen. Wir werden die folgenden Themen behandeln:
- Eine Untersuchung des Konzepts eines hybriden TPO-Marktprofil-Indikator
- Implementierung in MQL5
- Backtests
- Schlussfolgerung
Am Ende werden Sie über einen funktionsfähigen hybriden TPO-Marktprofil-Indikator verfügen, der sich ganz nach Ihren Wünschen anpassen lässt – legen wir los!
Eine Untersuchung des Konzepts eines hybriden TPO-Marktprofil-Indikator
Das hybride Time Price Opportunity (TPO)-Marktprofil ist ein Visualisierungstool, das die Kursverteilung über die Zeit innerhalb definierter Handelssitzungen abbildet. Dabei werden Buchstaben (letters), Rechteckmarkierungen oder einfach Punkte verwendet, um Zeitintervalle auf bestimmten Kursniveaus darzustellen, wodurch Bereiche mit hoher Aktivität wie der Wertebereich und der Point of Control sichtbar werden, in denen der Großteil des Handels stattfand. Dieser Ansatz hilft uns dabei, Unterstützungs-, Widerstands- und Fair-Value-Zonen zu identifizieren, indem die Kursentwicklung in einem Profilhistogramm zusammengefasst wird, wobei dichtere TPO-Ansammlungen auf einen ausgeglichenen Handel hindeuten und weniger dichte Bereiche auf mögliche Ausbrüche oder Ungleichgewichte deuten. Typischerweise wenden wir sie sitzungsübergreifend an, um die Marktstimmung einzuschätzen, Positionen nahe den Rändern von Wertebereichen einzugehen oder Verschiebungen am Point of Control im Hinblick auf Trendfortsetzungen zu beobachten.
Wir planen, Sitzungen auf der Grundlage ausgewählter Zeiträume unter Berücksichtigung von Zeitzonenanpassungen festzulegen. Wir diskretisieren die Preise in einem Raster und erfassen Sitzungskennzahlen wie Höchst- und Tiefstwerte . Wir berechnen den Point of Control als das Kursniveau mit der höchsten TPO-Anzahl. Als Nächstes leiten wir den Wertebereich ab, der einen festgelegten Prozentsatz der gesamten TPOs abdeckt. Abschließend stellen wir das Profil mithilfe von farbcodierten Beschriftungen, Punkten und Quadraten dar, um die Chartanalyse zu erleichtern. Kurz gesagt: Hier ist eine visuelle Darstellung unserer Ziele.

Implementierung in MQL5
Um den Indikator in MQL5 zu erstellen, öffnen Sie einfach den MetaEditor, gehen zum Navigator, suchen den Ordner Indikatoren, klicken auf die Registerkarte Neu und folgen den Anweisungen, um die Datei zu erstellen. Sobald der Indikator erstellt ist, legen wir in der Programmierumgebung die Eigenschaften und Einstellungen des Indikators fest, wie beispielsweise die Anzahl der Puffer und Plots.
//+------------------------------------------------------------------+ //| Hybrid TPO Market Profile PART 1.mq5 | //| Copyright 2026, Allan Munene Mutiiria. | //| https://t.me/Forex_Algo_Trader | //+------------------------------------------------------------------+ #property copyright "Copyright 2026, Allan Munene Mutiiria." #property link "https://t.me/Forex_Algo_Trader" #property version "1.00" #property strict #property indicator_chart_window #property indicator_buffers 0 #property indicator_plots 0 //+------------------------------------------------------------------+ //| Enums | //+------------------------------------------------------------------+ enum MarketProfileTimeframe { // Define market profile timeframe enum INTRADAY, // Intraday DAILY, // Daily WEEKLY, // Weekly MONTHLY, // Monthly FIXED // Fixed }; //+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ sinput group "Settings" input double ticksPerTpoLetter = 10; // Ticks per letter input int valueAreaPercent = 70; // Value Area Percent sinput group "Time" input MarketProfileTimeframe profileTimeframe = DAILY; // Timeframe input string timezone = "Exchange"; // Timezone input string dailySessionRange = "0830-1500"; // Daily session input int intradayProfileLengthMinutes = 60; // Profile length in minutes (Intraday) input datetime fixedTimeRangeStart = D'2026.02.01 08:30'; // From (Fixed) input datetime fixedTimeRangeEnd = D'2026.02.02 15:00'; // Till (Fixed) sinput group "Rendering" input int labelFontSize = 10; // Font size sinput group "Colors" input color defaultTpoColor = clrGray; // Default input color singlePrintColor = 0xd56a6a; // Single Print input color valueAreaColor = clrBlack; // Value Area input color pointOfControlColor = 0x3f7cff; // POC input color closeColor = clrRed; // Close //+------------------------------------------------------------------+ //| Constants | //+------------------------------------------------------------------+ #define MAX_BARS_BACK 5000 #define TPO_CHARACTERS_STRING "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" //+------------------------------------------------------------------+ //| Structures | //+------------------------------------------------------------------+ struct TpoPriceLevel { // Define TPO price level structure double price; // Store price level string tpoString; // Store TPO string int tpoCount; // Store TPO count }; struct ProfileSessionData { // Define profile session data structure datetime startTime; // Store start time datetime endTime; // Store end time double sessionOpen; // Store session open price double sessionClose; // Store session close price double sessionHigh; // Store session high price double sessionLow; // Store session low price TpoPriceLevel levels[]; // Store array of price levels int periodCount; // Store period count double periodOpens[]; // Store array of period opens int pointOfControlIndex; // Store point of control index }; //+------------------------------------------------------------------+ //| Global Variables | //+------------------------------------------------------------------+ string objectPrefix = "HTMP_"; //--- Set object prefix ProfileSessionData sessions[]; //--- Declare sessions array int activeSessionIndex = -1; //--- Initialize active session index double tpoPriceGridStep = 0; //--- Initialize TPO price grid step string tpoCharacterSet[]; //--- Declare TPO character set array datetime previousBarTime = 0; //--- Initialize previous bar time datetime lastCompletedBarTime = 0; //--- Initialize last completed bar time int maxSessionHistory = 20; //--- Set maximum session history int timezoneOffsetSeconds = 0; //--- Initialize timezone offset in seconds
Wir beginnen die Implementierung damit, die Eigenschaften des Indikators mithilfe der Direktive #property zu konfigurieren, ihn über indicator_chart_window so einzustellen, dass er im Hauptchart angezeigt wird, und die Anzahl der Puffer und Plots auf null festzulegen, da sich dieser Indikator auf die Darstellung benutzerdefinierter Objekte konzentriert und nicht auf gezeichnete Linien oder Histogramme. Als Nächstes definieren wir die Enumeration MarketProfileTimeframe, um die Auswahl von Profilzeiträumen zu ermöglichen, darunter Optionen wie INTRADAY für kurzfristige Sitzungen, DAILY für Standard-Handelstage, WEEKLY, MONTHLY und FIXED für benutzerdefinierte Zeiträume, was Flexibilität bei der Analyse von Handelssitzungen bietet.
Anschließend definieren wir die Benutzereingabeparameter, die in Abschnitten mit Eingabegruppen zusammengefasst sind, beginnend mit den Einstellungen für ticksPerTpoLetter zur Steuerung der Kursauflösung pro Time Price Opportunity Buchstaben sowie valueAreaPercent, um den Prozentsatz der gesamten Time Price Opportunities festzulegen, die den Wertebereich definieren. In der Zeitgruppe umfassen die Eingaben die Auswahl des Profilzeitraums, die Zeitzonenangabe für Zeitverschiebungsanpassungen, den täglichen Handelszeitraum als Zeichenfolge wie 0830-1500, die Länge des Intraday-Profils in Minuten sowie Datums- und Zeitangaben für den Beginn und das Ende des festen Zeitraums. Die Rendering-Gruppe fügt labelFontSize für die Textanzeige hinzu, während die Farbgruppe anpassbare Farben definiert, wie beispielsweise defaultTpoColor für Standardbuchstaben, singlePrintColor für isolierte Prints sowie valueAreaColor, pointOfControlColor und closeColor, um Profilelemente optisch voneinander zu unterscheiden.
Um ein einheitliches Vorgehen zu gewährleisten, führen wir über #define Konstanten ein, darunter MAX_BARS_BACK zur Begrenzung der Verarbeitung historischer Bars und TPO_CHARACTERS_STRING als alphabetische Reihenfolge für die Zuordnung von Buchstaben zu den TPO-Perioden. Wir erstellen zwei Strukturen zur Datenorganisation: TpoPriceLevel zur Speicherung einzelner Preisniveau-Details mit Feldern für den Preis, einer Zeichenkette aus Time Price Opportunity-Zeichen und der Anzahl sowie ProfileSessionData zur Verwaltung sitzungsweiter Informationen, darunter Start- und Endzeitpunkte, Eröffnungs-, Schluss-, Höchst- und Tiefstkurse, ein Array von Kursniveaus, die Anzahl der Perioden, ein Array der Eröffnungskurse der Perioden und der Index für den Point of Control.
Abschließend initialisieren wir globale Variablen, die für die Laufzeitverwaltung unerlässlich sind, wie beispielsweise objectPrefix zur Benennung von Chartobjekten, ein Array sessions zur Speicherung von Profildaten, activeSessionIndex, beginnend bei -1, zur Verfolgung der aktuellen Sitzung, tpoPriceGridStep zur Preisquantisierung, das Array tpoCharacterSet für die Zuordnung von Buchstaben, Zeitstempel wie previousBarTime und lastCompletedBarTime, maxSessionHistory, um die Anzahl der gespeicherten Sitzungen auf 20 zu begrenzen, sowie timezoneOffsetSeconds für Zeitanpassungen. Nachdem das erledigt ist, können wir den Indikator initialisieren.
//+------------------------------------------------------------------+ //| Initialize custom indicator | //+------------------------------------------------------------------+ int OnInit() { IndicatorSetString(INDICATOR_SHORTNAME, "Hybrid TPO Market Profile - Part 1"); //--- Set indicator short name tpoPriceGridStep = ticksPerTpoLetter * _Point; //--- Calculate TPO price grid step ArrayResize(tpoCharacterSet, 52); //--- Resize TPO character set array for(int i = 0; i < 52; i++) { //--- Loop through characters tpoCharacterSet[i] = StringSubstr(TPO_CHARACTERS_STRING, i, 1); //--- Assign character to array } if(timezone != "Exchange") { //--- Check if timezone is not exchange string tzString = StringSubstr(timezone, 3); //--- Extract timezone string int offset = (int)StringToInteger(tzString); //--- Convert offset to integer timezoneOffsetSeconds = offset * 3600; //--- Calculate timezone offset in seconds } ArrayResize(sessions, 0); //--- Resize sessions array to zero return(INIT_SUCCEEDED); //--- Return initialization success }
Wir setzen die Implementierung mit OnInit fort, das ausgeführt wird, wenn der Indikator an ein Chart angehängt wird, und nehmen dabei wichtige Konfigurationen für das hybride Time Price Opportunity-Marktprofil vor. Zunächst weisen wir dem Indikator mithilfe der Funktion IndicatorSetString und dem Parameter INDICATOR_SHORTNAME einen Kurznamen zu, damit in der Benutzeroberfläche der Plattform Hybrid TPO Market Profile – Teil 1 angezeigt wird.
Anschließend berechnen wir die Schrittweite des Preisrasters, indem wir den vom Benutzer eingegebenen Wert ticksPerTpoLetter mit dem Punktwert _Point des Symbols multiplizieren und das Ergebnis in tpoPriceGridStep speichern, um den vertikalen Abstand für die Buchstaben der Time-Price-Opportunity auf der Grundlage der Preisinkremente zu bestimmen. Anschließend bereiten wir den Zeichensatz für die Kennzeichnung von Time Price Opportunities vor, indem wir die Größe des Arrays tpoCharacterSet auf 52 Elemente setzen und es in einer Schleife füllen, wobei wir jeden Buchstaben mithilfe von StringSubstr aus der Konstante TPO_CHARACTERS_STRING extrahieren, um sowohl Groß- als auch Kleinbuchstaben für die Erkennung von Zeiträumen zu berücksichtigen.
Um Zeitanpassungen zu berücksichtigen, prüfen wir, ob sich der Eingabewert timezone von Exchange unterscheidet; ist dies der Fall, extrahieren wir den Offset-Teil ab dem vierten Zeichen mit StringSubstr, wandeln ihn mit StringToInteger in eine Ganzzahl um und berechnen den Wert timezoneOffsetSeconds, indem wir den Offset mit 3600 multiplizieren, um die Stunden in Sekunden darzustellen. Wir setzen das Array sessions zurück, indem wir seine Größe mit ArrayResize auf Null ändern und alle bisherigen Daten löschen, um einen Neuanfang für neue Profilsitzungen zu ermöglichen. Abschließend geben wir INIT_SUCCEEDED zurück, um der Plattform die erfolgreiche Initialisierung zu signalisieren. Wir müssen die Berechnungen für die Indikatoren nun pro Tick durchführen. Um unseren Code modular und leicht wartbar zu gestalten, werden wir die Logik in Hilfsfunktionen organisieren. Beginnen wir damit, die Sitzung anzulegen und die Sitzungszeiträume zu analysieren.
//+------------------------------------------------------------------+ //| Create new session | //+------------------------------------------------------------------+ int CreateNewSession() { int size = ArraySize(sessions); //--- Get size of sessions array if(size >= maxSessionHistory) { //--- Check if size exceeds history limit for(int i = 0; i < size - 1; i++) { //--- Loop to shift sessions sessions[i] = sessions[i + 1]; //--- Copy next session to current } ArrayResize(sessions, size - 1); //--- Resize sessions array size = size - 1; //--- Update size } ArrayResize(sessions, size + 1); //--- Resize sessions array for new session int newIndex = size; //--- Set new index sessions[newIndex].startTime = 0; //--- Initialize start time sessions[newIndex].endTime = 0; //--- Initialize end time sessions[newIndex].sessionOpen = 0; //--- Initialize session open sessions[newIndex].sessionClose = 0; //--- Initialize session close sessions[newIndex].sessionHigh = 0; //--- Initialize session high sessions[newIndex].sessionLow = 0; //--- Initialize session low sessions[newIndex].periodCount = 0; //--- Initialize period count sessions[newIndex].pointOfControlIndex = -1; //--- Initialize point of control index ArrayResize(sessions[newIndex].levels, 0); //--- Resize levels array ArrayResize(sessions[newIndex].periodOpens, 0);//--- Resize period opens array return newIndex; //--- Return new index } //+------------------------------------------------------------------+ //| Quantize price to grid | //+------------------------------------------------------------------+ double QuantizePriceToGrid(double price) { return MathRound(price / tpoPriceGridStep) * tpoPriceGridStep; //--- Calculate and return quantized price } //+------------------------------------------------------------------+ //| Parse daily session time range | //+------------------------------------------------------------------+ bool ParseDailySessionTimeRange(int &startHour, int &startMinute, int &endHour, int &endMinute) { string parts[]; //--- Declare parts array int count = StringSplit(dailySessionRange, '-', parts); //--- Split daily session range if(count != 2) return false; //--- Return false if invalid count startHour = (int)StringToInteger(StringSubstr(parts[0], 0, 2)); //--- Parse start hour startMinute = (int)StringToInteger(StringSubstr(parts[0], 2, 2)); //--- Parse start minute endHour = (int)StringToInteger(StringSubstr(parts[1], 0, 2)); //--- Parse end hour endMinute = (int)StringToInteger(StringSubstr(parts[1], 2, 2)); //--- Parse end minute return true; //--- Return true }
Zunächst definieren wir die Funktion CreateNewSession, um dem Array eine neue Profilsitzung hinzuzufügen. Dazu ermitteln wir zunächst mit der Funktion ArraySize die aktuelle Größe. Wenn der Wert den maximalen Historiengrenzwert erreicht oder überschreitet, verschieben wir die bestehenden Sitzungen in einer Schleife nach vorn, um die älteste zu entfernen, und verkleinern anschließend das Array mithilfe von ArrayResize, bevor wir die Größe aktualisieren. Als Nächstes erhöhen wir die Größe des Arrays um eins, um die neue Sitzung aufzunehmen, legen deren Index fest und initialisieren alle Felder mit Standardwerten wie Null oder -1, wobei wir auch die Arrays für die Levels und die Eröffnungskurse der Perioden auf leer zurücksetzen. Um sicherzustellen, dass die Preise mit dem Time Price Opportunity-Raster übereinstimmen, setzen wir die Funktion QuantizePriceToGrid ein, die den Eingabepreis durch die Rasterschrittweite teilt, ihn mithilfe der Funktion MathRound rundet und anschließend wieder multipliziert, um ihn auf den nächsten Rasterpunkt auszurichten.
Zur Verarbeitung der täglichen Sitzungen zerlegt die Funktion ParseDailySessionTimeRange die eingegebene Zeitangabe mithilfe der Funktion StringSplit und unter Verwendung eines Bindestrichs als Trennzeichen in einzelne Teile. Werden genau zwei Teile gefunden, extrahieren wir mit StringSubstr und StringToInteger die Stunden und Minuten aus jedem Teil und weisen sie den Referenzparametern zu; andernfalls wird false zurückgegeben, um einen Fehler beim Parsen anzuzeigen. Als Nächstes benötigen wir Funktionen, um die Bars innerhalb der Handelszeit zu filtern und die Kursniveaus innerhalb einer Sitzung zu verwalten.
//+------------------------------------------------------------------+ //| Check if bar is within daily session | //+------------------------------------------------------------------+ bool IsBarWithinDailySession(datetime barTime) { if(profileTimeframe != DAILY) return true; //--- Return true if not daily timeframe int startHour, startMinute, endHour, endMinute; //--- Declare time variables if(!ParseDailySessionTimeRange(startHour, startMinute, endHour, endMinute)) return true; //--- Parse and return true if fail MqlDateTime dateTimeStruct; //--- Declare date time struct TimeToStruct(barTime + timezoneOffsetSeconds, dateTimeStruct); //--- Convert time to struct int barMinutes = dateTimeStruct.hour * 60 + dateTimeStruct.min; //--- Calculate bar minutes int startMinutes = startHour * 60 + startMinute; //--- Calculate start minutes int endMinutes = endHour * 60 + endMinute; //--- Calculate end minutes if(endMinutes > startMinutes) { //--- Check if end after start return barMinutes >= startMinutes && barMinutes <= endMinutes; //--- Return if within range } else { //--- Handle overnight case return barMinutes >= startMinutes || barMinutes <= endMinutes; //--- Return if within range } } //+------------------------------------------------------------------+ //| Check if new session started | //+------------------------------------------------------------------+ bool IsNewSessionStarted(datetime currentTime, datetime previousTime) { if(previousTime == 0) return true; //--- Return true if no previous time datetime adjustedCurrent = currentTime + timezoneOffsetSeconds; //--- Adjust current time datetime adjustedPrevious = previousTime + timezoneOffsetSeconds; //--- Adjust previous time MqlDateTime currentDateTime, previousDateTime; //--- Declare date time structs TimeToStruct(adjustedCurrent, currentDateTime); //--- Convert current to struct TimeToStruct(adjustedPrevious, previousDateTime); //--- Convert previous to struct switch(profileTimeframe) { //--- Switch on profile timeframe case DAILY: { //--- Handle daily case int startHour, startMinute, endHour, endMinute; //--- Declare time variables if(!ParseDailySessionTimeRange(startHour, startMinute, endHour, endMinute)) return false; //--- Parse and return false if fail datetime sessionStart = StringToTime(TimeToString(adjustedCurrent, TIME_DATE) + " " + IntegerToString(startHour, 2, '0') + ":" + IntegerToString(startMinute, 2, '0')); //--- Calculate session start datetime prevSessionStart = StringToTime(TimeToString(adjustedPrevious, TIME_DATE) + " " + IntegerToString(startHour, 2, '0') + ":" + IntegerToString(startMinute, 2, '0')); //--- Calculate previous session start return adjustedCurrent >= sessionStart && adjustedPrevious < prevSessionStart; //--- Return if new session } case WEEKLY: //--- Handle weekly case return currentDateTime.day_of_week < previousDateTime.day_of_week || currentDateTime.day_of_year < previousDateTime.day_of_year; //--- Return if new week case MONTHLY: //--- Handle monthly case return currentDateTime.mon != previousDateTime.mon; //--- Return if new month case FIXED: //--- Handle fixed case return currentTime >= fixedTimeRangeStart && previousTime < fixedTimeRangeStart; //--- Return if new fixed range case INTRADAY: { //--- Handle intraday case long currentMinute = (adjustedCurrent / 60) * 60; //--- Calculate current minute long prevMinute = (adjustedPrevious / 60) * 60; //--- Calculate previous minute return (currentMinute % (intradayProfileLengthMinutes * 60)) == 0 && currentMinute != prevMinute; //--- Return if new intraday profile } } return false; //--- Return false } //+------------------------------------------------------------------+ //| Check if bar is eligible for processing | //+------------------------------------------------------------------+ bool IsBarEligibleForProcessing(datetime barTime) { if(profileTimeframe == FIXED) { //--- Check fixed timeframe return barTime >= fixedTimeRangeStart && barTime <= fixedTimeRangeEnd; //--- Return if within fixed range } if(profileTimeframe == DAILY) { //--- Check daily timeframe return IsBarWithinDailySession(barTime); //--- Return if within daily session } return true; //--- Return true } //+------------------------------------------------------------------+ //| Get or create price level | //+------------------------------------------------------------------+ int GetOrCreatePriceLevel(int sessionIndex, double price) { if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return -1; //--- Return invalid if index out of range int size = ArraySize(sessions[sessionIndex].levels); //--- Get levels size for(int i = 0; i < size; i++) { //--- Loop through levels if(MathAbs(sessions[sessionIndex].levels[i].price - price) < _Point / 2) //--- Check if price matches return i; //--- Return index } ArrayResize(sessions[sessionIndex].levels, size + 1); //--- Resize levels array sessions[sessionIndex].levels[size].price = price; //--- Set new price sessions[sessionIndex].levels[size].tpoString = ""; //--- Initialize TPO string sessions[sessionIndex].levels[size].tpoCount = 0; //--- Initialize TPO count return size; //--- Return new index }
Anschließend implementieren wir die Funktion IsBarWithinDailySession, um festzustellen, ob eine bestimmte Bar innerhalb der festgelegten täglichen Handelszeiten liegt, wobei für nicht-tägliche Zeitrahmen sofort true zurückgegeben wird. Wenn das Parsen des Sitzungszeitraums über die Funktion ParseDailySessionTimeRange fehlschlägt, wird standardmäßig true gesetzt; andernfalls konvertieren wir die angepasste Barzeit mithilfe von TimeToStruct in eine MqlDateTime-Struktur, berechnen die Gesamtminuten für die Start- und Endzeit der Bar und prüfen, ob die Minuten der Bar innerhalb des Bereichs liegen, wobei wir sowohl Standard- als auch Übernacht-Sitzungen mit bedingter Logik behandeln.
Anschließend prüft die Funktion IsNewSessionStarted den Beginn einer neuen Profilsitzung, indem sie die angepasste aktuelle Zeit mit der vorherigen Zeit vergleicht, und gibt true zurück, wenn keine vorherige Zeit vorliegt. Wir passen die Zeitstempel an den Zeitzonenversatz an, wandeln sie in MqlDateTime-Strukturen um und verwenden eine switch-Anweisung für den Profilzeitrahmen: Bei täglich wird der Sitzungszeitraum analysiert und die Startzeiten werden mit StringToTime, TimeToString und IntegerToString umgewandelt, um den Übergang in einen neuen Tag zu überprüfen; bei wöchentlichen Profilen werden Wochentag und Jahr verglichen; bei monatlichen Profilen werden Monatsunterschiede geprüft; bei festen Profilen erfolgt ein Vergleich mit der Startangabe; und bei intraday-Profilen wird überprüft, ob die aktuelle Minute mit dem Modulo der Profillänge übereinstimmt, ohne mit der vorherigen übereinzustimmen.
Um die zu berücksichtigenden Bars zu filtern, definieren wir die Funktion IsBarEligibleForProcessing, die für festgelegte Zeiträume prüft, ob der Zeitpunkt der Bar zwischen den Start- und Endwerten liegt, für tägliche Abfragen die Funktion IsBarWithinDailySession verwendet und ansonsten für alle Bars den Wert true zurückgibt. Schließlich verwaltet die Funktion GetOrCreatePriceLevel die Preisstufen innerhalb einer Sitzung, indem sie den Sitzungsindex mithilfe von ArraySize anhand der Größe des Sitzungs-Arrays überprüft und im Falle einer Ungültigkeit den Wert -1 zurückgibt. Die Funktion durchläuft die vorhandenen Niveaus, um mithilfe von MathAbs mit einer Toleranz von einem halben Punkt eine möglichst genaue Übereinstimmung zu finden, und gibt den Index zurück, falls eine Übereinstimmung gefunden wird; andernfalls passt sie die Größe des Niveaus-Arrays an, initialisiert den Preis des neuen Niveaus, setzt die Zeichenkette Time Price Opportunity auf leer und setzt den Zähler auf Null, bevor sie den neuen Index zurückgibt. Da wir die Buchstaben für die Profile zunächst in einer bestimmten Reihenfolge darstellen möchten, müssen wir einen Bubble-Sort-Algorithmus definieren, um die Buchstaben vor der Darstellung entsprechend anzuordnen.
//+------------------------------------------------------------------+ //| Add TPO character to level | //+------------------------------------------------------------------+ void AddTpoCharacterToLevel(int sessionIndex, int levelIndex, int periodIndex) { if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return; //--- Return if session index invalid if(levelIndex < 0 || levelIndex >= ArraySize(sessions[sessionIndex].levels)) return; //--- Return if level index invalid string tpoCharacter = tpoCharacterSet[periodIndex % 52]; //--- Get TPO character sessions[sessionIndex].levels[levelIndex].tpoString += tpoCharacter; //--- Append character to TPO string sessions[sessionIndex].levels[levelIndex].tpoCount++; //--- Increment TPO count } //+------------------------------------------------------------------+ //| Sort price levels descending | //+------------------------------------------------------------------+ void SortPriceLevelsDescending(int sessionIndex) { if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return; //--- Return if index invalid int size = ArraySize(sessions[sessionIndex].levels); //--- Get levels size for(int i = 0; i < size - 1; i++) { //--- Outer loop for sorting for(int j = 0; j < size - i - 1; j++) { //--- Inner loop for comparison if(sessions[sessionIndex].levels[j].price < sessions[sessionIndex].levels[j + 1].price) { //--- Check if swap needed TpoPriceLevel temp = sessions[sessionIndex].levels[j]; //--- Store temporary level sessions[sessionIndex].levels[j] = sessions[sessionIndex].levels[j + 1]; //--- Swap levels sessions[sessionIndex].levels[j + 1] = temp; //--- Assign temporary back } } } } //+------------------------------------------------------------------+ //| Calculate point of control | //+------------------------------------------------------------------+ void CalculatePointOfControl(int sessionIndex) { if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return; //--- Return if index invalid int size = ArraySize(sessions[sessionIndex].levels); //--- Get levels size if(size == 0) return; //--- Return if no levels int maxTpoCount = 0; //--- Initialize max TPO count int pointOfControlIndex = 0; //--- Initialize POC index for(int i = 0; i < size; i++) { //--- Loop through levels if(sessions[sessionIndex].levels[i].tpoCount > maxTpoCount) { //--- Check if higher TPO count maxTpoCount = sessions[sessionIndex].levels[i].tpoCount; //--- Update max TPO count pointOfControlIndex = i; //--- Update POC index } } sessions[sessionIndex].pointOfControlIndex = pointOfControlIndex; //--- Set POC index } //+------------------------------------------------------------------+ //| Get total TPO count | //+------------------------------------------------------------------+ int GetTotalTpoCount(int sessionIndex) { if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return 0; //--- Return zero if index invalid int total = 0; //--- Initialize total int size = ArraySize(sessions[sessionIndex].levels); //--- Get levels size for(int i = 0; i < size; i++) { //--- Loop through levels total += sessions[sessionIndex].levels[i].tpoCount; //--- Accumulate TPO count } return total; //--- Return total }
Zunächst implementieren wir die Funktion AddTpoCharacterToLevel, um den bestimmten Kursniveaus Buchstaben für Time Price Opportunity zuzuweisen. Dabei überprüfen wir zunächst die Indizes für die Sitzung und das Niveau anhand der Array-Größen mit ArraySize, um Fehler zu vermeiden, und brechen den Vorgang vorzeitig ab, falls diese ungültig sind. Wir entnehmen dem Satz das entsprechende Zeichen, indem wir den Periodenindex mit Modulo 52 berechnen, fügen es an die Zeichenkette der Stufe an und erhöhen den Zählwert, um die Aktivität bei diesem Kurs zu verfolgen. Um die Preisstufen vom höchsten zum niedrigsten Preis zu sortieren, überprüft die Funktion SortPriceLevelsDescending die Gültigkeit des Sitzungsindex, ermittelt die Größe der Preisstufen und wendet in verschachtelten Schleifen einen Blasensortieralgorithmus an, bei dem benachbarte Elemente vertauscht werden, wenn der aktuelle Preis niedriger ist als der des nächsten Elements. Dabei wird eine temporäre TpoPriceLevel-Struktur für den Austausch verwendet.
Die Funktion CalculatePointOfControl ermittelt das Kursniveau mit den meisten Time Price Opportunities, indem sie die Sitzung validiert, die maximale Anzahl und den Index auf Null setzt und die einzelnen Niveaus durchläuft, um diese zu aktualisieren, sobald eine höhere Anzahl gefunden wird, und schließlich den Index in den Sitzungsdaten speichert. Wir fügen die Funktion GetTotalTpoCount hinzu, um alle Time Price Opportunity-Zählwerte über alle Ebenen einer Sitzung hinweg zu summieren. Bei einem ungültigen Index gibt die Funktion Null zurück; andernfalls initialisiert sie eine Gesamtsumme und sammelt die Zählwerte in einer Schleife, bevor sie das Aggregat für Berechnungen im Wertebereich zurückgibt. Wir können nun mit der Logik zur visuellen Darstellung dieser Komponenten des Marktprofils beginnen. Zunächst definieren wir die Logik für die Darstellung der TPO-Highlight-Markierung für den Schlusskurs sowie der Markierungen für Kurseröffnung und -schluss.
//+------------------------------------------------------------------+ //| Render close TPO highlight | //+------------------------------------------------------------------+ void RenderCloseTpoHighlight(int sessionIndex, int closeLevelIndex, string &displayStrings[]) { if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return; //--- Return if session invalid if(closeLevelIndex < 0 || closeLevelIndex >= ArraySize(sessions[sessionIndex].levels)) return; //--- Return if level invalid string fullString = displayStrings[closeLevelIndex]; //--- Get full display string int stringLength = StringLen(fullString); //--- Get string length if(stringLength == 0) return; //--- Return if empty string string closeCharacter = StringSubstr(fullString, stringLength - 1, 1); //--- Extract close character string remainingCharacters = StringSubstr(fullString, 0, stringLength - 1); //--- Extract remaining characters string objectName = objectPrefix + "CloseTPO_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create object name int barIndex = iBarShift(_Symbol, _Period, sessions[sessionIndex].startTime); //--- Get bar index if(barIndex < 0) return; //--- Return if invalid bar index datetime labelTime = iTime(_Symbol, _Period, barIndex); //--- Get label time int x, y; //--- Declare coordinates ChartTimePriceToXY(0, 0, labelTime, sessions[sessionIndex].levels[closeLevelIndex].price, x, y); //--- Convert to XY int characterWidth = 8; //--- Set character width int offsetX = (stringLength - 1) * characterWidth; //--- Calculate offset X if(ObjectFind(0, objectName) < 0) { //--- Check if object not found ObjectCreate(0, objectName, OBJ_LABEL, 0, 0, 0); //--- Create label object } ObjectSetInteger(0, objectName, OBJPROP_XDISTANCE, x + offsetX); //--- Set X distance ObjectSetInteger(0, objectName, OBJPROP_YDISTANCE, y); //--- Set Y distance ObjectSetInteger(0, objectName, OBJPROP_CORNER, CORNER_LEFT_UPPER); //--- Set corner ObjectSetInteger(0, objectName, OBJPROP_ANCHOR, ANCHOR_LEFT); //--- Set anchor ObjectSetInteger(0, objectName, OBJPROP_COLOR, closeColor); //--- Set color ObjectSetInteger(0, objectName, OBJPROP_FONTSIZE, labelFontSize); //--- Set font size ObjectSetString(0, objectName, OBJPROP_FONT, "Arial"); //--- Set font ObjectSetString(0, objectName, OBJPROP_TEXT, closeCharacter + "◄"); //--- Set text ObjectSetInteger(0, objectName, OBJPROP_SELECTABLE, false); //--- Set selectable false ObjectSetInteger(0, objectName, OBJPROP_HIDDEN, true); //--- Set hidden true displayStrings[closeLevelIndex] = remainingCharacters; //--- Update display string } //+------------------------------------------------------------------+ //| Render open close markers | //+------------------------------------------------------------------+ void RenderOpenCloseMarkers(int sessionIndex, int barIndex) { if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return; //--- Return if index invalid if(sessions[sessionIndex].sessionOpen == 0) return; //--- Return if no open datetime startTime = iTime(_Symbol, _Period, barIndex); //--- Get start time string openObjectName = objectPrefix + "Open_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create open object name if(ObjectFind(0, openObjectName) < 0) { //--- Check if not found ObjectCreate(0, openObjectName, OBJ_TREND, 0, startTime, sessions[sessionIndex].sessionOpen, startTime, sessions[sessionIndex].sessionOpen); //--- Create trend object ObjectSetInteger(0, openObjectName, OBJPROP_RAY_RIGHT, false); //--- Set ray right false ObjectSetInteger(0, openObjectName, OBJPROP_SELECTABLE, false); //--- Set selectable false ObjectSetInteger(0, openObjectName, OBJPROP_HIDDEN, true); //--- Set hidden true } ObjectSetInteger(0, openObjectName, OBJPROP_COLOR, clrDodgerBlue); //--- Set color ObjectSetInteger(0, openObjectName, OBJPROP_WIDTH, 2); //--- Set width string closeObjectName = objectPrefix + "Close_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create close object name if(ObjectFind(0, closeObjectName) < 0) { //--- Check if not found ObjectCreate(0, closeObjectName, OBJ_TREND, 0, startTime, sessions[sessionIndex].sessionClose, startTime, sessions[sessionIndex].sessionClose); //--- Create trend object ObjectSetInteger(0, closeObjectName, OBJPROP_RAY_RIGHT, false); //--- Set ray right false ObjectSetInteger(0, closeObjectName, OBJPROP_SELECTABLE, false); //--- Set selectable false ObjectSetInteger(0, closeObjectName, OBJPROP_HIDDEN, true); //--- Set hidden true } ObjectSetInteger(0, closeObjectName, OBJPROP_COLOR, closeColor); //--- Set color ObjectSetInteger(0, closeObjectName, OBJPROP_WIDTH, 2); //--- Set width }
Wir definieren die Funktion RenderCloseTpoHighlight, um das TPO-Zeichen auf dem Schlusskursniveau hervorzuheben. Dabei beginnen wir mit Indexüberprüfungen für die Sitzung und die Ebene und verwenden dabei ArraySize, um ungültige Fälle zu überspringen. Wir rufen die Anzeigezeichenfolge auf der Schließebene ab, berechnen ihre Länge mit StringLen und extrahieren, sofern sie nicht leer ist, das letzte Zeichen mit StringSubstr zur Hervorhebung, während der Rest gespeichert wird.
Ein Objektname wird gebildet, indem das Präfix mit der über IntegerToString konvertierten Startzeit der Sitzung kombiniert wird, und der Barindex wird mit iBarShift ermittelt; ist dieser ungültig, wird die Funktion vorzeitig beendet. Als Nächstes rufen wir die Zeitangabe mithilfe von iTime ab, wandeln den Preis und die Zeit mit ChartTimePriceToXY in Chartkoordinaten um und berechnen einen X-Versatz auf der Grundlage der Zeichenbreite und der Zeichenfolgenlänge. Wenn das Objekt laut ObjectFind nicht existiert, erstellen wir mit ObjectCreate und dem Parameter OBJ_LABEL eine Beschriftung und legen anschließend deren Position, Ecke, Ankerpunkt, Farbe, Schriftgröße, Schriftart, Text (mit angehängter Markierung) sowie die Eigenschaften nicht auswählbar und ausgeblendet mithilfe von ObjectSetInteger und ObjectSetString fest, bevor wir die Anzeigekette aktualisieren, um das markierte Zeichen auszuschließen.
Um die Sitzungsgrenzen darzustellen, implementieren wir die Funktion RenderOpenCloseMarkers, die den Sitzungsindex überprüft und den Schritt überspringt, wenn kein Eröffnungskurs festgelegt ist. Wir ermitteln die Startzeit mit iTime anhand des Barindex und bilden dann auf ähnliche Weise Namen für die Open- und Close-Objekte. Für den Eröffnungsmarker erstellen wir, falls er nicht über ObjectFind gefunden wird, mit ObjectCreate unter Verwendung von OBJ_TREND ein Trendlinienobjekt zum Eröffnungskurs, deaktivieren die Verlängerung des rechten Strahls, machen es nicht auswählbar und unsichtbar und weisen ihm eine blaue Farbe mit einer Breite von 2 zu. Ebenso erstellen oder aktualisieren wir für die Schlussmarkierung eine Trendlinie am Schlusskurs, wobei wir ihr die Farbe des Eingabeschlusskurses und dieselbe Breite zuweisen, um sicherzustellen, dass beide als kurze horizontale Linien im Chart an der Bar des Sitzungsbeginns erscheinen. Wir können nun all diese Logik kombinieren, um das vollständige Marktprofil darzustellen.
//+------------------------------------------------------------------+ //| Render session profile | //+------------------------------------------------------------------+ void RenderSessionProfile(int sessionIndex) { if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return; //--- Return if index invalid int size = ArraySize(sessions[sessionIndex].levels); //--- Get levels size if(size == 0 || sessions[sessionIndex].startTime == 0) return; //--- Return if no levels or no start time int barIndex = iBarShift(_Symbol, _Period, sessions[sessionIndex].startTime); //--- Get bar index if(barIndex < 0) return; //--- Return if invalid SortPriceLevelsDescending(sessionIndex); //--- Sort levels descending CalculatePointOfControl(sessionIndex); //--- Calculate POC int totalTpoCount = GetTotalTpoCount(sessionIndex); //--- Get total TPO count int pointOfControlIndex = sessions[sessionIndex].pointOfControlIndex; //--- Get POC index int valueAreaUpperIndex = pointOfControlIndex; //--- Initialize value area upper index int valueAreaLowerIndex = pointOfControlIndex; //--- Initialize value area lower index if(pointOfControlIndex >= 0) { //--- Check valid POC index int targetTpoCount = (int)(totalTpoCount * valueAreaPercent / 100.0); //--- Calculate target TPO count int currentTpoCount = sessions[sessionIndex].levels[pointOfControlIndex].tpoCount; //--- Set current TPO count while(currentTpoCount < targetTpoCount && (valueAreaUpperIndex > 0 || valueAreaLowerIndex < size - 1)) { //--- Loop to expand value area int upperTpoCount = (valueAreaUpperIndex > 0) ? sessions[sessionIndex].levels[valueAreaUpperIndex - 1].tpoCount : 0; //--- Get upper TPO count int lowerTpoCount = (valueAreaLowerIndex < size - 1) ? sessions[sessionIndex].levels[valueAreaLowerIndex + 1].tpoCount : 0; //--- Get lower TPO count if(upperTpoCount >= lowerTpoCount && valueAreaUpperIndex > 0) { //--- Check upper expansion valueAreaUpperIndex--; //--- Decrement upper index currentTpoCount += upperTpoCount; //--- Add upper TPO } else if(valueAreaLowerIndex < size - 1) { //--- Check lower expansion valueAreaLowerIndex++; //--- Increment lower index currentTpoCount += lowerTpoCount; //--- Add lower TPO } else if(valueAreaUpperIndex > 0) { //--- Fallback upper expansion valueAreaUpperIndex--; //--- Decrement upper index currentTpoCount += upperTpoCount; //--- Add upper TPO } else { //--- Break if no more break; //--- Exit loop } } } string displayStrings[]; //--- Declare display strings array ArrayResize(displayStrings, size); //--- Resize display strings for(int i = 0; i < size; i++) { //--- Loop through levels displayStrings[i] = sessions[sessionIndex].levels[i].tpoString; //--- Copy TPO string } int closeLevelIndex = -1; //--- Initialize close level index double closePrice = sessions[sessionIndex].sessionClose; //--- Get close price for(int i = 0; i < size; i++) { //--- Loop to find close level if(MathAbs(sessions[sessionIndex].levels[i].price - closePrice) < tpoPriceGridStep / 2) { //--- Check price match closeLevelIndex = i; //--- Set close level index break; //--- Exit loop } } RenderCloseTpoHighlight(sessionIndex, closeLevelIndex, displayStrings); //--- Render close highlight for(int i = 0; i < size; i++) { //--- Loop to render levels string objectName = objectPrefix + "TPO_" + IntegerToString(sessions[sessionIndex].startTime) + "_" + IntegerToString(i); //--- Create object name color textColor = defaultTpoColor; //--- Set default color if(sessions[sessionIndex].levels[i].tpoCount == 1) { //--- Check single print textColor = singlePrintColor; //--- Set single print color } if(i >= valueAreaUpperIndex && i <= valueAreaLowerIndex) { //--- Check value area textColor = valueAreaColor; //--- Set value area color } if(i == sessions[sessionIndex].pointOfControlIndex) { //--- Check POC textColor = pointOfControlColor; //--- Set POC color } if(ObjectFind(0, objectName) < 0) { //--- Check if object not found ObjectCreate(0, objectName, OBJ_LABEL, 0, 0, 0); //--- Create label ObjectSetInteger(0, objectName, OBJPROP_XDISTANCE, 0); //--- Set X distance ObjectSetInteger(0, objectName, OBJPROP_YDISTANCE, 0); //--- Set Y distance } datetime labelTime = iTime(_Symbol, _Period, barIndex); //--- Get label time int x, y; //--- Declare coordinates ChartTimePriceToXY(0, 0, labelTime, sessions[sessionIndex].levels[i].price, x, y); //--- Convert to XY ObjectSetInteger(0, objectName, OBJPROP_XDISTANCE, x); //--- Set X distance ObjectSetInteger(0, objectName, OBJPROP_YDISTANCE, y); //--- Set Y distance ObjectSetInteger(0, objectName, OBJPROP_CORNER, CORNER_LEFT_UPPER); //--- Set corner ObjectSetInteger(0, objectName, OBJPROP_ANCHOR, ANCHOR_LEFT); //--- Set anchor ObjectSetInteger(0, objectName, OBJPROP_COLOR, textColor); //--- Set color ObjectSetInteger(0, objectName, OBJPROP_FONTSIZE, labelFontSize); //--- Set font size ObjectSetString(0, objectName, OBJPROP_FONT, "Arial"); //--- Set font ObjectSetString(0, objectName, OBJPROP_TEXT, displayStrings[i]); //--- Set text ObjectSetInteger(0, objectName, OBJPROP_SELECTABLE, false); //--- Set selectable false ObjectSetInteger(0, objectName, OBJPROP_HIDDEN, true); //--- Set hidden true } RenderOpenCloseMarkers(sessionIndex, barIndex); //--- Render open close markers }
Hier implementieren wir die Funktion RenderSessionProfile, um das vollständige Marktprofil für eine bestimmte Sitzung im Chart darzustellen. Dabei beginnen wir mit einer Indexüberprüfung anhand der Größe des Sitzungs-Arrays mithilfe von ArraySize, um die Funktion bei Ungültigkeit vorzeitig zu beenden, und prüfen anschließend, ob die Ebenen nicht leer sind und eine gültige Startzeit vorliegt. Wir ermitteln den Startbarindex mit iBarShift anhand des Symbols, des Zeitraums und der Sitzungsstartzeit; bei Ungültigkeit kehren wir zurück. Anschließend rufen wir SortPriceLevelsDescending auf, um die Kursniveaus vom höchsten zum niedrigsten zu sortieren, und CalculatePointOfControl, um das Niveau mit der höchsten Anzahl an Time Price Opportunity-Werten zu ermitteln.
Als Nächstes rufen wir über GetTotalTpoCount die Gesamtanzahl der Time-Price-Opportunities sowie den Index des Point of Control ab und initialisieren damit die oberen und unteren Indizes des Wertebereichs. Wenn der Point of Control gültig ist, berechnen wir eine Zielanzahl als Prozentsatz der Gesamtzahl unter Verwendung des Eingabewerts Bereichsprozentsatz, ausgehend von der aktuellen Anzahl auf dieser Ebene, und erweitern den Wertebereich in einer while-Schleife, indem wir die Anzahlen benachbarter oberer oder unterer Ebenen vergleichen und addieren – vorzugsweise je nachdem, welche mehr Time-Price-Opportunities aufweist –, wobei wir die Indizes verringern oder erhöhen, bis das Ziel oder die Grenzen erreicht sind.
Zur Vorbereitung des Renderings deklarieren wir ein Array für die Anzeigezeichenfolgen und passen dessen Größe mit ArrayResize an die Größe des Levels an, wobei wir die Zeichenfolge Time Price Opportunity jedes Levels über eine Schleife in das Array kopieren. Anschließend suchen wir nach dem Schlusskurs-Index, indem wir die Kursstufen durchlaufen und mithilfe von MathAbs die größte Übereinstimmung mit dem Schlusskurs der Sitzung innerhalb der Hälfte der Rastertoleranz ermitteln; sobald wir diese gefunden haben, brechen wir den Vorgang ab.
Nachdem wir RenderCloseTpoHighlight mit dem Sitzungsindex, dem Schließebene-Index und den Anzeigezeichenfolgen aufgerufen haben, um die Hervorhebung beim Schließen zu handhaben, durchlaufen wir jede Ebene in einer Schleife, um Beschriftungsobjekte zu erstellen oder zu aktualisieren: Dabei bilden wir mithilfe von IntegerToString einen eindeutigen Namen aus dem Präfix, der Startzeit und dem Ebenenindex. Wir legen eine Standardtextfarbe fest, überschreiben diese Farbe für Single Prints, den Wertebereich oder Point of Control basierend auf den Eingabefarben und überprüfen das Vorhandensein mit ObjectFind. Bei Bedarf erstellen wir mit ObjectCreate ein OBJ_LABEL, wobei die Abstände zunächst auf Null gesetzt werden. Wir rufen die Beschriftungszeit über iTime und die Umwandlung von Zeit-Preis in Koordinaten mithilfe von ChartTimePriceToXY ab und legen Position, Ecke, Ankerpunkt, Farbe, Schriftgröße, Schriftart, Text aus der Anzeigekette und nicht auswählbare bzw. ausgeblendete Eigenschaften mithilfe der Funktionen ObjectSetInteger und ObjectSetString fest.
Abschließend rufen wir RenderOpenCloseMarkers mit dem Sitzungsindex und dem Barindex auf, um die visuellen Indikatoren für Eröffnungs- und Schlusskurse im Chart hinzuzufügen. Wir können diese Funktion nun im Ereignis-Handler für die Tick-Berechnung aufrufen, um die rechenintensiven Aufgaben bedingt auszuführen.
//+------------------------------------------------------------------+ //| Calculate custom indicator | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { if(rates_total < 2) return 0; //--- Return if insufficient rates datetime currentBarTime = time[rates_total - 1]; //--- Get current bar time bool isNewBar = (currentBarTime != lastCompletedBarTime); //--- Check if new bar if(IsNewSessionStarted(currentBarTime, previousBarTime) || previousBarTime == 0) { //--- Check new session if(activeSessionIndex >= 0 && activeSessionIndex < ArraySize(sessions)) { //--- Check active session sessions[activeSessionIndex].endTime = previousBarTime; //--- Set end time RenderSessionProfile(activeSessionIndex); //--- Render session profile } activeSessionIndex = CreateNewSession(); //--- Create new session sessions[activeSessionIndex].startTime = currentBarTime; //--- Set start time sessions[activeSessionIndex].sessionOpen = open[rates_total - 1]; //--- Set session open sessions[activeSessionIndex].sessionHigh = high[rates_total - 1]; //--- Set session high sessions[activeSessionIndex].sessionLow = low[rates_total - 1]; //--- Set session low lastCompletedBarTime = currentBarTime; //--- Update last completed bar time } previousBarTime = currentBarTime; //--- Update previous bar time if(isNewBar && IsBarEligibleForProcessing(currentBarTime) && activeSessionIndex >= 0) { //--- Check if process bar sessions[activeSessionIndex].sessionHigh = MathMax(sessions[activeSessionIndex].sessionHigh, high[rates_total - 1]); //--- Update session high sessions[activeSessionIndex].sessionLow = MathMin(sessions[activeSessionIndex].sessionLow, low[rates_total - 1]); //--- Update session low sessions[activeSessionIndex].sessionClose = close[rates_total - 1]; //--- Update session close int periodIndex = sessions[activeSessionIndex].periodCount; //--- Get period index ArrayResize(sessions[activeSessionIndex].periodOpens, periodIndex + 1); //--- Resize period opens sessions[activeSessionIndex].periodOpens[periodIndex] = open[rates_total - 1]; //--- Set period open sessions[activeSessionIndex].periodCount++; //--- Increment period count double quantizedHigh = QuantizePriceToGrid(high[rates_total - 1]); //--- Quantize high double quantizedLow = QuantizePriceToGrid(low[rates_total - 1]); //--- Quantize low for(double price = quantizedLow; price <= quantizedHigh; price += tpoPriceGridStep) { //--- Loop through prices int levelIndex = GetOrCreatePriceLevel(activeSessionIndex, price); //--- Get or create level if(levelIndex >= 0) { //--- Check valid level AddTpoCharacterToLevel(activeSessionIndex, levelIndex, periodIndex); //--- Add TPO character } } lastCompletedBarTime = currentBarTime; //--- Update last completed bar time } if(IsBarEligibleForProcessing(currentBarTime) && activeSessionIndex >= 0) { //--- Check if update session sessions[activeSessionIndex].sessionClose = close[rates_total - 1]; //--- Update close sessions[activeSessionIndex].sessionHigh = MathMax(sessions[activeSessionIndex].sessionHigh, high[rates_total - 1]); //--- Update high sessions[activeSessionIndex].sessionLow = MathMin(sessions[activeSessionIndex].sessionLow, low[rates_total - 1]); //--- Update low } for(int i = 0; i < ArraySize(sessions); i++) { //--- Loop through sessions RenderSessionProfile(i); //--- Render profile } return rates_total; //--- Return rates total }
Die Hauptberechnung führen wir in der Funktion OnCalculate durch, die bei jeder Aktualisierung die Kursdaten-Arrays für den benutzerdefinierten Indikator verarbeitet. Dabei wird zunächst der Wert Null zurückgegeben, wenn weniger als zwei Kurse verfügbar sind, um sicherzustellen, dass ausreichende Daten vorliegen. Wir entnehmen die aktuelle Barzeit aus dem Zeit-Array am letzten Index und stellen fest, ob es sich um eine neue Bar handelt, indem wir sie mit der letzten abgeschlossenen Zeit vergleichen. Wird über IsNewSessionStarted eine neue Sitzung erkannt oder liegt kein vorheriger Zeitpunkt vor, schließen wir die aktive Sitzung ab, indem wir ihre Endzeit festlegen und sie mit RenderSessionProfile darstellen, sofern dies innerhalb der durch ArraySize vorgegebenen Grenzen gültig ist. Anschließend erstellen wir mit CreateNewSession eine neue Sitzung, initialisieren deren Startzeit sowie die Werte für Open, High und Low anhand der Daten der aktuellen Bar und aktualisieren die Zeit der zuletzt abgeschlossenen Sitzung.
Nachdem die Zeit der vorherigen Bar aktualisiert wurde, wird – sofern es sich um eine neue Bar handelt, der gemäß IsBarEligibleForProcessing für die Verarbeitung in Frage kommt, und eine aktive Sitzung vorliegt – aktualisieren wir das Hoch und das Tief der Sitzung mithilfe von MathMax und MathMin mit den aktuellen Werten der Bar, legen den Schlusskurs fest, erhöhen die Periodenzahl, nachdem wir die Eröffnungsnotierung der Periode angepasst und im Array gespeichert haben, quantisieren das Hoch und Tief der Bar über QuantizePriceToGrid und durchlaufen in Raster-Schritten von Tief nach Hoch, um mit GetOrCreatePriceLevel Preisniveaus abzurufen oder zu erstellen, bevor mit AddTpoCharacterToLevel Zeichen für die Zeit-Preis-Chance hinzugefügt werden, und schließen schließlich mit der Aktualisierung der zuletzt abgeschlossenen Zeit ab. Wenn die aktuelle Bar die Voraussetzungen erfüllt und die Sitzung aktiv ist, aktualisieren wir zudem kontinuierlich den Schlusskurs, Hoch und Tief, um die aktuellen Veränderungen widerzuspiegeln.
Abschließend durchlaufen wir alle Sitzungen, um jedes Profil darzustellen, und geben die Gesamtwerte zurück, um anzuzeigen, dass die Verarbeitung abgeschlossen ist. Nun müssen wir noch die manuelle Größenanpassung des Charts vornehmen, damit die Level neu gerendert werden und nicht mehr hängen bleiben, und unsere Objekte bei der Deinitialisierung des Indikators löschen. Um dies zu erreichen, haben wir den folgenden Ansatz verfolgt.
//+------------------------------------------------------------------+ //| Handle chart event | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if(id == CHARTEVENT_CHART_CHANGE) { //--- Check chart change event for(int i = 0; i < ArraySize(sessions); i++) { //--- Loop through sessions RenderSessionProfile(i); //--- Render profile } } } //+------------------------------------------------------------------+ //| Deinitialize custom indicator | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { DeleteAllIndicatorObjects(); //--- Delete all indicator objects } //+------------------------------------------------------------------+ //| Delete all indicator objects | //+------------------------------------------------------------------+ void DeleteAllIndicatorObjects() { int total = ObjectsTotal(0, 0, -1); //--- Get total number of objects for(int i = total - 1; i >= 0; i--) { //--- Loop through objects in reverse string name = ObjectName(0, i, 0, -1); //--- Get object name if(StringFind(name, objectPrefix) == 0) //--- Check if name starts with prefix ObjectDelete(0, name); //--- Delete object } }
Wir verarbeiten Interaktionen mit dem Chart im Ereignis-Handler OnChartEvent, der bei verschiedenen Chartereignissen ausgelöst wird. Dabei prüfen wir, ob die Ereignis-ID mit CHARTEVENT_CHART_CHANGE übereinstimmt, um Änderungen wie Zeitrahmenwechsel oder Größenanpassungen zu erkennen; ist dies der Fall, durchlaufen wir alle Sitzungen mithilfe von ArraySize, um die Anzahl zu ermitteln, und rendern jedes Profil mit RenderSessionProfile neu, um die Darstellungen entsprechend zu aktualisieren.
Beim Entfernen eines Indikators wird der Ereignis-Handler OnDeinit ausgeführt, der die Funktion DeleteAllIndicatorObjects aufruft, um alle benutzerdefinierten Chartelemente zu bereinigen und zu verhindern, dass Objekte zurückbleiben.
In DeleteAllIndicatorObjects rufen wir über die Funktion ObjectsTotal die Gesamtzahl der Chartobjekte über alle Fenster und Typen hinweg ab, durchlaufen dann die Liste rückwärts vom letzten Index bis Null, rufen den Namen jedes Objekts mit ObjectName ab und entfernen es mit ObjectDelete, sofern es mit dem Präfix beginnt, für das StringFind den Wert Null zurückgibt, um eine vollständige Bereinigung sicherzustellen. Wenn wir den Indikator ausführen, erhalten wir das folgende Ergebnis.

Aus der Abbildung geht hervor, dass wir den Indikator berechnen und das Marktprofil mit Textbeschriftungen darstellen, wodurch wir unsere Ziele erreichen. Bleibt nur noch der Backtest des Programms, und der wird im nächsten Abschnitt behandelt.
Backtests
Wir haben die Tests durchgeführt, und unten sehen Sie die kompilierte Visualisierung als einzelne GIF-Animation.

Schlussfolgerung
Zusammenfassend lässt sich sagen, dass wir in MQL5 einen benutzerdefinierten Indikator für hybride Time Price Opportunity Marktprofile (TPO) entwickelt haben, der mehrere Handelszeitrahmen unterstützt, darunter Intraday, täglich, wöchentlich, monatlich sowie feste Zeitintervalle mit Zeitzonenanpassungen. Der Indikator unterteilt die Kurse in ein Raster, erfasst die Sitzungsdaten zu Höchst-, Tiefst-, Eröffnungs- und Schlusskursen, berechnet den Point of Control und den Wertebereich anhand von TPO-Zählwerten und stellt Profile im Chart dar, wobei die Farben für TPO-Buchstaben, Single Prints, Wertebereiche, den Point of Control und Schlusskursmarkierungen individuell angepasst werden können. Im folgenden Teil werden wir die Darstellung der quadratischen und punktförmigen Blasen einfügen, um das Marktprofil mit allen Beschriftungen zu kennzeichnen. Bleiben Sie dran!
Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/21388
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.
Neurostrukturelle Trading-Engine – NSTE (Teil I): Aufbau eines prop-firm-tauglichen Multi-Account-Systems
Eine alternative Log-datei mit der Verwendung der HTML und CSS
Swing-Extreme und Rücksetzer in MQL5 (Teil 1): Entwicklung eines Multi-Timeframe-Indikators
- 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.