English Русский 中文 Español Português
preview
Analyse von Kurs-Zeit-Lücken in MQL5 (Teil II): Erstellung einer Heatmap der Liquiditätsverteilung über die Zeit

Analyse von Kurs-Zeit-Lücken in MQL5 (Teil II): Erstellung einer Heatmap der Liquiditätsverteilung über die Zeit

MetaTrader 5Indikatoren |
23 4
Yevgeniy Koshtenko
Yevgeniy Koshtenko

Im ersten Teil haben wir das Konzept der Zeitlücken und deren Verbindung mit institutionellen Handelsaktivitäten untersucht. Das Erkennen dieser Lücken ist jedoch nur ein Teil der Aufgabe. Um effektiv zu handeln, muss ein Trader das Gesamtbild sehen: wo der Kurs viel Zeit verbringt, wo er wenig Zeit verbringt und wie diese Zonen miteinander interagieren.

Hier kommt unser Indikator ins Spiel – ein Werkzeug, das unsichtbare Zeitmuster in eine visuelle Heatmap verwandelt. Während wir im ersten Teil nach Anomalien (Lücken) gesucht haben, konstruieren wir nun eine vollständige Heatmap des normalen Kursverhaltens.

Die Grundidee ist einfach: Der gesamte Kursbereich wird als Menge von Mikrozonen dargestellt, und für jede Zone wird berechnet, wie lange sich der Kurs darin aufgehalten hat. Je länger die Zeit, desto „heißer“ die Zone, desto wichtiger ist sie für den Markt. Das Ergebnis wird durch ein Farbschema von kühlem Rot bis heißem Blau visualisiert.


Mathematische Grundlage: Vom Chaos zur Ordnung

Jeder Markt kann als ein kontinuierlicher Kampf zwischen Angebot und Nachfrage betrachtet werden. Dort, wo dieser Kampf am intensivsten ist, neigt der Kurs dazu, länger zu verweilen. Mathematisch wird dies durch die Zeitdichtefunktion ausgedrückt:

T(p) = Σt_i, wobei der Preis in dem Bereich [p-δ, p+δ] liegt.

Hier ist p das untersuchte Kursniveau, δ ist die Größe der Analysezone, während t_i die Dauer jedes Zeitraums ist, in dem sich der Kurs in dieser Zone befindet.

Aber die Rohdaten sagen wenig aus. Wir benötigen eine Normalisierung, die absolute Zeitwerte in relative Prozentsätze umwandelt. Wir verwenden die Formel:

P(p) = ((T(p) - T_min) / (T_max - T_min)) × 99% + 1%

Diese Formel stellt sicher, dass den „kältesten“ Zonen ein Präsenzwert von 1 % zugewiesen wird, während die „heißesten“ 100 % erreichen. Alle anderen Zonen werden proportional zu ihrer Bedeutung zwischen diesen Extremen verteilt.


Lösungsarchitektur: Modularität als Grundlage für Zuverlässigkeit

Die Erstellung eines solchen Indikators erfordert eine gut durchdachte Architektur. Im Kern steht die Struktur PriceLevel, die alle Informationen zu jedem Kursniveau kapselt:

struct PriceLevel
{
    double      price;             // Central price level
    double      price_high;        // Zone upper boundary
    double      price_low;         // Zone lower boundary
    long        time_spent;        // Accumulated time in bars
    double      presence_percent;  // Presence percentage
    color       level_color;       // Dynamic color
    string      object_name;       // Unique ID
};

Jedes Level hat seine eigene Dynamik: Es akkumuliert Verweildauer, berechnet Prozentsätze neu und ändert seine Farbe. Es ist nicht nur eine Datenstruktur – es ist die lebendige Essenz des Marktes.

Die wichtigste Neuerung war die Verwendung eines gleitenden Analysefensters. Statt die gesamte verfügbare Historie zu verarbeiten, analysieren wir innerhalb der letzten MaxHistory Bars nur ein gleitendes Fenster mit der Größe AnalysisPeriod. Dies stellt sicher, dass die Ergebnisse relevant und die Leistung akzeptabel sind.


Algorithmus: Wo Mathe auf Realität trifft

Der Prozess beginnt mit der Bestimmung der Kursspanne für die Analyse. Der Algorithmus findet automatisch das Maximum und das Minimum für den untersuchten Zeitraum, wodurch er sich an die Volatilität jedes Instruments anpassen kann.

Dieser Bereich wird dann in gleiche Zonen unterteilt. Die Anzahl der Zonen wird dynamisch berechnet: Wenn die Tick-Größe angegeben ist, wird sie verwendet; wenn nicht, wird der minimale Point-Wert des Instruments genommen. Gleichzeitig balanciert das System zwischen Detailgenauigkeit (mindestens 50 Niveaus) und Leistung (nicht mehr als 1000 Ebenen).

Der ressourcenintensivste Teil ist die Erfassung bzw. Zählung der Verweildauer auf jedem Level. Ein naiver Ansatz würde erfordern, jede Bar mit jedem Niveau zu vergleichen (O(n²)-Komplexität). Wir haben dies auf O(n×k) optimiert, wobei k die durchschnittliche Anzahl der Niveaus ist, die von einer Bar betroffen sind.

// Optimization: find only relevant levels for each bar
int startLevel = MathMax(0, (int)((lowPrice - minPrice) / realTickSize));
int endLevel = MathMin(totalPriceLevels - 1, (int)((highPrice - minPrice) / realTickSize) + 1);

for(int levelIdx = startLevel; levelIdx <= endLevel; levelIdx++)
{
    if(DoesBarTouchLevel(highPrice, lowPrice, levels[levelIdx]))
    {
        levels[levelIdx].time_spent++;
    }
}

Die Funktion DoesBarTouchLevel prüft die Überschneidung des High-Low-Bereichs der Bar mit den Grenzen des Kursniveaus. Die Logik ist einfach: Wenn das Maximum der Bar über der unteren Grenze des Niveaus liegt und das Minimum der Bar unter der oberen Grenze liegt, gibt es eine Überschneidung.


Farbalchemie: Zahlen in Bilder verwandeln

Nachdem die Zeit berechnet wurde, beginnt der kreativste Teil: die Umwandlung der Rohdaten in ein Farbschema. Wir verwenden ein Fünf-Stufen-System: Rot (1 %), Orange (25 %), Gelb (50 %), Hellblau (75 %), Blau (100 %).

Zwischen den Eckpunkten erfolgt eine glatte Interpolation. Beispielsweise erhält ein Niveau mit 37 % Präsenz eine Farbe zwischen Orange und Gelb. Die Interpolation erfolgt im RGB-Farbraum:

color InterpolateColor(color color1, color color2, double factor)
{
    // Decomposition into RGB components
    int r1 = (color1 >> 16) & 0xFF;
    int g1 = (color1 >> 8) & 0xFF;
    int b1 = color1 & 0xFF;
    
    // Linear interpolation of each channel
    int r = (int)(r1 + (r2 - r1) * factor);
    int g = (int)(g1 + (g2 - g1) * factor);
    int b = (int)(b1 + (b2 - b1) * factor);
    
    return (r << 16) | (g << 8) | b;
}

Das Ergebnis sind weiche Farbübergänge, die eine natürliche Heatmap des Marktes erzeugen.


Visualisierung: Vom Algorithmus zum Chart

Jedes Kursniveau wird als Rechteck im Chart dargestellt. Das Erstellen von Tausenden von grafischen Objekten ist eine technisch anspruchsvolle Aufgabe, die Optimierung erfordert.

Die Rechtecke werden von der Startzeit der Analyse bis zum aktuellen Zeitpunkt platziert und decken den gesamten relevanten Bereich ab. Jedes Objekt ist so konfiguriert, dass es im Hintergrund arbeitet: Es stört die Chartanalyse nicht, wird beim Anklicken nicht hervorgehoben und wird bei einer Änderung der Zoomstufe automatisch neu gezeichnet.

Das Objektverwaltungssystem enthält einen Mechanismus zum Löschen früherer Ergebnisse, bevor neue gerendert werden. Dies verhindert die Ansammlung von „visuellem Ballast“ im Chart und stellt eine korrekte Aktualisierung der Visualisierung sicher.

In Echtzeit ist die Leistung entscheidend. Der Indikator verwendet mehrere Optimierungsniveaus:

  • Das erste Niveau sind bedarfsgesteuerte Berechnungen. Eine Neuberechnung erfolgt nur, wenn eine neue Bar erscheint. Das System verfolgt den Zeitpunkt der letzten Aktualisierung und startet Berechnungen nur, wenn eine Änderung eintritt.
  • Das zweite Niveau ist die Speicheroptimierung. Daten-Arrays werden einmal zugewiesen und wiederverwendet. Datenstrukturen sind auf minimalen Speicherverbrauch ausgelegt: Für Zähler werden Longs anstelle von Doubles verwendet und unnötige String-Variablen werden vermieden.
  • Das dritte Niveau ist die algorithmische Optimierung. Das gleitende Analysefenster begrenzt die Menge der verarbeiteten Daten. Das adaptive Raster der Niveaus verhindert die Erstellung einer übermäßigen Anzahl von Zonen.


Interpretation der Ergebnisse: Was die Farben bedeuten

Rote Zonen (1-25 % Präsenz) kennzeichnen Bereiche, die der Kurs schnell durchquert. Dies sind die Zonen potenzieller Zeitlücken, die im vorherigen Artikel beschrieben wurden. In roten Zonen kommt es häufig zu Abprallern und Fehlausbrüchen, daher erfordern sie ein vorsichtiges Vorgehen.

Orangefarbene und gelbe Zonen (25-75 %) stellen Bereiche mit moderater Aktivität dar. Hier verweilt der Kurs periodisch, jedoch ohne offensichtliche Dominanz. Dies sind Übergangszonen, die je nach Marktumfeld zu Unterstützung oder Widerstand werden können. Dies sind die Zonen, in denen trendfolgende Trades oft am besten funktionieren.

Die blauen und hellblauen Zonen (75-100 %) stehen im Mittelpunkt unserer Analyse. Hier verbringt der Kurs die meiste Zeit, was auf eine hohe Handelsaktivität hindeutet. Diese Niveaus haben eine starke magnetische Kraft: Der Kurs kehrt regelmäßig zu ihnen zurück und nutzt sie als Unterstützung für die weitere Bewegung oder als Barrieren, die überwunden werden müssen.

Die effektivste Strategie ist das Trading von Abprallern an blauen Zonen. Wenn sich der Kurs dem Bereich der maximalen Präsenz nähert, ist die Wahrscheinlichkeit einer Umkehr deutlich höher als im Durchschnitt. Dies funktioniert besonders gut in Seitwärtsmärkten, in denen die blauen Zonen die Kanalgrenzen klar definieren.

Ausbrüche durch gelbe Zonen signalisieren oft eine fortgesetzte Bewegung. Wenn der Kurs den Bereich mittlerer Präsenz bei solidem bzw. bestätigendem Volumen leicht durchquert, deutet dies darauf hin, dass kein ernsthafter Widerstand bevorsteht.

Umkehr-Setups sind oft in roten Zonen am effektivsten.

Die Kombination mit Volumenanalyse verbessert die Signale erheblich. Wenn eine blaue Zone mit einem Volumenmaximum im Volume Profile zusammenfällt, entsteht ein Bereich von maximaler Bedeutung.


Anpassung für verschiedene Märkte: Vielseitigkeit durch Anpassung

Forex mit seiner hohen Liquidität erfordert hohe Werte für AnalysisPeriod (300-500 Bars) und MaxHistory (5000-8000 Bars). Die Bewegungen sind hier gleichmäßiger, daher ist eine größere Analysetiefe erforderlich, um signifikante Bereiche zu identifizieren.

Für Aktien funktionieren moderate Einstellungen tendenziell gut: AnalysisPeriod 200-300 Bars, MaxHistory 3000-5000 Bars. Die Struktur der Handelssitzungen schafft natürliche Pausen, die sich gut in der Heatmap widerspiegeln.

Die Tick-Größe (TickSize) ist entscheidend für den ordnungsgemäßen Betrieb. Ein zu niedriger Wert führt zu übermäßigen Details ohne praktischen Nutzen. Ein zu hoher Wert führt dazu, dass wichtige Nuancen übersehen werden. Der Wert 0 (automatischer Modus) ist in der Regel optimal – das System wählt die Größe automatisch basierend auf den Eigenschaften des Instruments aus.

Transparenz beeinflusst nicht nur die visuelle Wahrnehmung, sondern auch die Leistung. Hohe Werte (70-90%) erzeugen eine halbtransparente Heatmap, die die Kerzenanalyse nicht stört, aber mehr Ressourcen für das Rendering erfordert.

Das Limit von 1000 Levels wurde aus einem bestimmten Grund eingeführt. MetaTrader 5 hat Einschränkungen bei der Anzahl grafischer Objekte, und das Überschreiten vernünftiger Grenzen führt zu einer Verlangsamung der Benutzeroberfläche, ohne die Qualität der Analyse wesentlich zu verbessern.


Integration mit anderen Tools: Synergie von Methoden

Der Heatmap-Indikator funktioniert besonders gut zusammen mit dem Volumenprofil. Das Zusammentreffen blauer Zonen mit hoher zeitlicher Präsenz mit Volumenspitzen schafft Bereiche von außergewöhnlicher Bedeutung. Solche Zonen werden oft entscheidend für die langfristige Kursbewegung.

Die Kombination mit Fibonacci-Levels liefert interessante Ergebnisse. Wenn wichtige Fibonacci-Levels in die blauen Zonen der Heatmap fallen, erhöht sich deren Bedeutung um ein Vielfaches. Dies ist natürlich: Mathematische Levels werden durch das tatsächliche Kursverhalten gestützt.

Volatilitätsindikatoren (wie Bollinger-Bänder) funktionieren gut in Verbindung mit einer Heatmap. Eine Ausweitung der Bänder in den roten Zonen geht oft starken Bewegungen voraus, während eine Verengung in den blauen Zonen auf die Ansammlung von Energie für einen zukünftigen Ausbruch hindeutet.

Die nächste Version des Indikators soll maschinelles Lernen beinhalten, um Parameter automatisch für ein bestimmtes Instrument zu optimieren. Der Algorithmus wird die Leistungsstatistik der Levels analysieren und die Einstellungen für maximale Effizienz anpassen.

Die Integration mit Alarmsystemen ermöglicht Benachrichtigungen, wenn sich der Kurs wichtigen Zonen nähert. Dies ist besonders nützlich für Swing-Trader, die Charts nicht ständig überwachen können.

Der Export von Daten in externe Systeme eröffnet Möglichkeiten zur Erstellung von Trading-Robotern auf Basis von Heatmaps. Roboter werden in der Lage sein, die Stärke von Levels als zusätzlichen Filter für den Einstieg in eine Position zu nutzen.


Methodenphilosophie: Zeit als Marktwährung

Der Indikator basiert auf einer tiefgründigen Idee: Zeit ist die Währung des Marktes. Trader setzen ihre Zeit ebenso bewusst ein, wie sie ihr Geld ausgeben. Je mehr Zeit auf einem bestimmten Niveau verbracht wird, desto mehr Emotionen, Entscheidungen und Kapital sind damit verbunden.

Diese emotionale Bindung schafft ein Marktgedächtnis. Selbst wenn sich der Kurs von einem signifikanten Niveau entfernt, bleibt die Erinnerung daran im Unterbewusstsein der Teilnehmer erhalten. Bei der Rückkehr auf dieses Niveau erwachen alte Erinnerungen: bei den einen an Gewinne, bei den anderen an Verluste. Diese Erinnerungen beeinflussen neue Handelsentscheidungen.

Die Heatmap der Zeit macht dieses unsichtbare Gedächtnis sichtbar. Sie verwandelt Marktpsychologie in Mathematik, Emotionen in Algorithmen und Intuition in Daten.


Schlussfolgerung: Ein neuer Blick auf alte Wahrheiten

Der Indikator enthüllt keine neuen Prinzipien, er macht alte deutlicher. Unterstützungs- und Widerstandsniveaus gab es schon immer, aber jetzt können sie gemessen und nach ihrer Bedeutung eingestuft werden. Die Heatmap zeigt, wo der Markt Zeit verbringt und wo daher die wahre Stärke liegt.

In Kombination mit dem Zeitlücken-Indikator ergibt dies ein umfassendes Bild des Verhaltens der großen Marktteilnehmer: wo sie schnell agieren und wo sie verweilen. Zusammen ermöglichen uns diese Werkzeuge, die Marktstruktur besser zu verstehen und Entscheidungen auf der Grundlage von Logik statt Intuition zu treffen.

Im nächsten Teil werden wir besprechen, wie man all dies zu einem einzigen Handelssystem zusammenfügt.

Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/18661

Beigefügte Dateien |
Letzte Kommentare | Zur Diskussion im Händlerforum (4)
Maxim Kuznetsov
Maxim Kuznetsov | 3 Juli 2025 in 09:42

Das hat mich daran erinnert: Vor langer, langer Zeit habe ich so etwas schon einmal gemacht,

meiner Meinung nach ist es bei Ihnen noch ganz leicht unvollständig – es bleibt noch, die regelmäßige Struktur aufzudecken (oder zu zeigen, dass es keine gibt).

Das ist natürlich keine Heatmap – es werden lediglich die Extremwerte des regelmäßigen Teils einer solchen Temperaturkarte dargestellt.

Stanislav Korotky
Stanislav Korotky | 3 Juli 2025 in 14:27
Worin unterscheidet sich das vom allseits bekannten „Marktprofil“ (abgesehen von der Farbgebung)?
Maxim Kuznetsov
Maxim Kuznetsov | 3 Juli 2025 in 16:33

Übrigens wurde die Karte mit einer Methode berechnet, die als äußerst aufwendig und fehleranfällig gilt.

Es geht auch einfacher: Für jede Kerze werden zwei Paare {price, weight} in die Sammlung aufgenommen: { price=high, weight=-1; } { price=low, weight=+1; }. Die Sammlung wird nach „price“ sortiert, und die kumulierte Summe nach „weight“ ergibt die Heatmap. Anschließend wird nach Belieben quantisiert.

Lipe Ramos
Lipe Ramos | 15 Juli 2025 in 18:44
Ich habe beschlossen, ein wenig mit ChatGPT zu experimentieren, um ein paar „Verbesserungen“ einzubauen und etwas Nützliches für mich selbst zu erstellen – das ist dabei herausgekommen. Der auskommentierte Teil ist mein Versuch, zu ermitteln, wann der Kurs in eine der farbigen Zonen eintritt, und dessen Reaktion anschaulich darzustellen. Die eigentliche Idee bestand jedoch darin, die Nuancen dieser Farben zu erfassen: Wenn die Zone beispielsweise etwas heller orange ist, bewegt sich der Kurs meinen Beobachtungen zufolge in eine Seitwärtsbewegung.
//+------------------------------------------------------------------+
//|             Heat Map Plus – modifizierte Version 3.00                     |
//|   Behält Puffer/Warnmeldungen bei und fügt ADX, Statistiken und Rangliste hinzu     |
//+------------------------------------------------------------------+
#property copyright "Chart Coloring by Time and Volume Distribution"
#property link      "https://www.mql5.com"
#property version   "3.00"
#property indicator_chart_window
#property strict

#include <Math\Stat\Math.mqh>    // MathMean, MathStdDev
#include <Indicators\Trend.mqh>     // ADX zum Erkennen von Trends

// Puffer für EA
#property indicator_buffers 3
#property indicator_plots   0
double LevelPriceBuffer[];
double LevelStrengthBuffer[];
double LevelTypeBuffer[];

//--- Eingabeparameter
sinput group "=== Heat Map Settings ==="
input int      AnalysisPeriod   = 500;
input int      MaxHistory       = 10000;
input double   TickSize         = 0;
input bool     UseTickVolume    = true;
input double   VolumeWeight     = 0.5;
input int      SessStartHour    = 8;
input int      SessEndHour      = 17;
input double   MinBarRange      = 10;
input bool     EnableAlerts     = true;

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
sinput group "=== Farben (1 % → 100 %) ==="
input color    Color1Percent    = clrRed;
input color    Color25Percent   = clrOrange;
input color    Color50Percent   = clrYellow;
input color    Color75Percent   = clrAqua;
input color    Color100Percent  = clrBlue;
input int      Transparency     = 70;

//--- Globale Variablen
struct PriceLevel {
 double price, price_high, price_low;
 long   time_spent;
 long   volume;
 double presence_percent;
 color  level_color;
 string object_name; };
PriceLevel      levels[];
datetime        startTime, endTime, lastUpdate;
int             totalPriceLevels = 0;
long            maxTimeSpent = 0, minTimeSpent = LONG_MAX;
long            maxVolume = 0,    minVolume    = LONG_MAX;
bool            calculated = false;

//--- ADX-Handle
CiADX   adx;

//+------------------------------------------------------------------+
//| Initialisierung                                                  |
//+------------------------------------------------------------------+
int OnInit() {
// Puffer für EA
 SetIndexBuffer(0, LevelPriceBuffer);
 SetIndexBuffer(1, LevelStrengthBuffer);
 SetIndexBuffer(2, LevelTypeBuffer);
 if(AnalysisPeriod < 50 || MaxHistory < AnalysisPeriod) {
  Print(„Falsche Parameter: 50 <= AnalysisPeriod <= MaxHistory“);
  return(INIT_PARAMETERS_INCORRECT); }
 if(VolumeWeight < 0 || VolumeWeight > 1) {
  Print("VolumeWeight deve estar em [0,1]");
  return(INIT_PARAMETERS_INCORRECT); }
// ADX(14)
 if(!adx.Create(_Symbol, _Period, 14)) {
  Print("Erro ao criar ADX");
  return(INIT_FAILED); }
 ArrayResize(levels, 0);
 lastUpdate = 0;
 calculated = false;
 totalPriceLevels = 0;
 return(INIT_SUCCEEDED); }

//+------------------------------------------------------------------+
//| Berechnung                                                     |
//+------------------------------------------------------------------+
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 < AnalysisPeriod) return(0);
// wird erst bei einer neuen Kerze neu berechnet
 if(lastUpdate == time[rates_total - 1]) return(rates_total);
 lastUpdate = time[rates_total - 1];
 calculated = false;
 if(!calculated) {
  // ADX aktualisieren
  adx.Refresh(1);
  // Hauptberechnung
  CalculateTimeDistribution(time, high, low, tick_volume, volume, rates_total);
  ColorChart();
  ExportBuffers(rates_total);
  calculated = true; }
 return(rates_total); }

//+------------------------------------------------------------------+
//| Gleitendes Fenster + Sitzungs- und Volatilitätsfilter               |
//+------------------------------------------------------------------+
void CalculateTimeDistribution(const datetime &time[],
                               const double &high[],
                               const double &low[],
                               const long &tick_volume[],
                               const long &volume[],
                               int rates_total) {
 ArrayResize(levels, 0);
 int historyStart = MathMax(0, rates_total - MaxHistory);
 startTime = time[historyStart];
 endTime   = time[rates_total - 1];
// Preisklasse
 double maxP = high[ArrayMaximum(high, historyStart, MaxHistory)];
 double minP = low [ArrayMinimum(low,  historyStart, MaxHistory)];
 double priceRange = maxP - minP;
 if(priceRange <= 0) return;
// Tick-Größe
 double tk = (TickSize > 0 ? TickSize : Point());
 totalPriceLevels = (int)(priceRange / tk);
 totalPriceLevels = MathMin(1000, MathMax(50, totalPriceLevels));
 double realTick = priceRange / totalPriceLevels;
// Ebenen initialisieren
 ArrayResize(levels, totalPriceLevels);
 for(int i = 0; i < totalPriceLevels; i++) {
  levels[i].price       = minP + i * realTick;
  levels[i].price_high  = levels[i].price + realTick / 2;
  levels[i].price_low   = levels[i].price - realTick / 2;
  levels[i].time_spent  = 0;
  levels[i].volume      = 0;
  levels[i].object_name = "HeatLevel_" + IntegerToString(i); }
// durchläuft die Balken
 for(int bar = historyStart; bar < rates_total; bar++) {
  // Sitzungsfilter
  int hr = TimeHour(time[bar]);
  if(hr < SessStartHour || hr > SessEndHour) continue;
  // Volatilitätsfilter
  double amp = (high[bar] - low[bar]) / Point();
  if(amp < MinBarRange) continue;
  long barVol = UseTickVolume ? tick_volume[bar] : volume[bar];
  // Betroffene Ebenen ermitteln
  int st = MathMax(0, (int)((low[bar]  - minP) / realTick));
  int en = MathMin(totalPriceLevels - 1,
                   (int)((high[bar] - minP) / realTick));
  for(int li = st; li <= en; li++) {
   if(DoesBarTouchLevel(high[bar], low[bar], levels[li])) {
    levels[li].time_spent++;
    levels[li].volume    += barVol; } } }
// acha min/max
 maxTimeSpent = 0; minTimeSpent = LONG_MAX;
 maxVolume    = 0; minVolume    = LONG_MAX;
 for(int i = 0; i < totalPriceLevels; i++) {
  long t = levels[i].time_spent;
  long v = levels[i].volume;
  if(t > maxTimeSpent) maxTimeSpent = t;
  if(t > 0 && t < minTimeSpent) minTimeSpent = t;
  if(v > maxVolume)    maxVolume    = v;
  if(v > 0 && v < minVolume)    minVolume    = v; }
 if(minTimeSpent == LONG_MAX) minTimeSpent = 0;
 if(minVolume   == LONG_MAX) minVolume   = 0;
// Prozentangaben und Farben
 CalculatePercentsAndColors(); }

//+------------------------------------------------------------------+
//| Prozentsätze & Kerne                                                |
//+------------------------------------------------------------------+
void CalculatePercentsAndColors() {
 long tr = maxTimeSpent - minTimeSpent;  if(tr <= 0) tr = 1;
 long vr = maxVolume    - minVolume;     if(vr <= 0) vr = 1;
 for(int i = 0; i < totalPriceLevels; i++) {
  double tp = levels[i].time_spent > 0 ?
              ((levels[i].time_spent - minTimeSpent) / (double)tr) * 100.0 : 0;
  double vp = levels[i].volume > 0 ?
              ((levels[i].volume - minVolume) / (double)vr) * 100.0 : 0;
  double comb = (1.0 - VolumeWeight) * tp + VolumeWeight * vp;
  levels[i].presence_percent =
   MathMax(1.0, MathMin(100.0, comb));
  levels[i].level_color =
   GetPercentageColor(levels[i].presence_percent); } }

//+------------------------------------------------------------------+
//| Diagrammfärbung + Warnmeldungen                                         |
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void ColorChart() {
 ClearPreviousObjects();
 datetime nowTime = TimeCurrent();
 double lastBid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
// Array mit Zonennamen
 string zoneNames[5] = {"", „Reversão“, „Tendência“, „Lateralisierung“, "Barreira" };
// 1) Lagerbestand nach Typ und Durchschnittspreis
 int    types[]; ArrayResize(types, totalPriceLevels);
 double mids[];  ArrayResize(mids,  totalPriceLevels);
 for(int i = 0; i < totalPriceLevels; i++) {
  types[i] = (int)LevelTypeBuffer[ArraySize(LevelTypeBuffer) - 1 - i];
  if(types[i] < 1 || types[i] > 4)
   types[i] = 4;
  mids[i] = (levels[i].price_low + levels[i].price_high) / 2.0; }
// 2) Erzeugt Zonenobjekte (farbige Rechtecke)
 for(int i = 0; i < totalPriceLevels; i++) {
  string nm = levels[i].object_name;
  // Rechteck der Zone
  if(ObjectCreate(ChartID(), nm, OBJ_RECTANGLE, 0,
                  startTime, levels[i].price_low,
                  nowTime,   levels[i].price_high)) {
   ObjectSetInteger(ChartID(), nm, OBJPROP_COLOR, levels[i].level_color);
   ObjectSetInteger(ChartID(), nm, OBJPROP_BACK, true);
   ObjectSetInteger(ChartID(), nm, OBJPROP_FILL, true);
   ENUM_LINE_STYLE style = (Transparency > 50) ? STYLE_DOT : STYLE_SOLID;
   ObjectSetInteger(ChartID(), nm, OBJPROP_STYLE, style);
   ObjectSetInteger(ChartID(), nm, OBJPROP_WIDTH, 1); }
  // Warnmeldungen
  if(EnableAlerts) {
   static double lastPrice = 0;
   bool cross = ((lastPrice < levels[i].price && lastBid >= levels[i].price) ||
                 (lastPrice > levels[i].price && lastBid <= levels[i].price));
   if(cross) {
    Alert(„HeatMap: Der Kurs hat die Marke ‚‘ überschritten“,
          DoubleToString(levels[i].price, _Digits),
          " [", zoneNames[types[i]], "]"); }
   lastPrice = lastBid; } }
//// 3) Gruppiere nach zusammenhängenden Typen und zeichne eine Linie pro Gruppe
// struct Segment {
//  int type;
//  int start;
//  int end; };
// Segment segs[];
// ArrayResize(segs, 0);
// int currentType = types[0];
// int segStart = 0;
// for(int i = 1; i < totalPriceLevels; i++) {
//  if(types[i] != currentType) {
//   Segment seg;
//   seg.type  = currentType;
//   seg.start = segStart;
//   seg.end   = i - 1;
//   ArrayResize(segs, ArraySize(segs) + 1);
//   segs[ArraySize(segs) - 1] = seg;
//   currentType = types[i];
//   segStart    = i; } }
//// letztes Segment
// Segment lastSeg;
// lastSeg.type  = currentType;
// lastSeg.start = segStart;
// lastSeg.end   = totalPriceLevels - 1;
// ArrayResize(segs, ArraySize(segs) + 1);
// segs[ArraySize(segs) - 1] = lastSeg;
//// 4) Zeichne pro Segment eine mittlere horizontale Linie
// for(int j = 0; j < ArraySize(segs); j++) {
//  int s = segs[j].start;
//  int e = segs[j].end;
//  int type = segs[j].type;
//  // Median der Durchschnittspreise
//  double avgPrice = 0;
//  for(int k = s; k <= e; k++)
//   avgPrice += mids[k];
//  avgPrice /= (e - s + 1);
//// Horizontale Linie einfügen
//  string lineName = StringFormat(zoneNames[types[s]] + "linha", s, e);
//  if(ObjectCreate(ChartID(), lineName, OBJ_HLINE, 0, 0, avgPrice)) {
//   ObjectSetInteger(ChartID(), lineName, OBJPROP_COLOR, clrWhite);
//   ObjectSetInteger(ChartID(), lineName, OBJPROP_WIDTH, 4);
//   ObjectSetInteger(ChartID(), lineName, OBJPROP_STYLE, STYLE_SOLID);  }
//// Text rechts ausrichten (letzte sichtbare Kerze)
//  double textOffset = SymbolInfoDouble(_Symbol, SYMBOL_POINT) * 100; // vertikaler Versatz oberhalb der Zeile
//  double textPrice = avgPrice + textOffset;
//  datetime timeRight = iTime(_Symbol, _Period, 0);  // Zeitpunkt der letzten Kerze
//  string textName = StringFormat(zoneNames[types[s]] + "texto", s, e);
//  if(ObjectCreate(ChartID(), textName, OBJ_TEXT, 0, timeRight, textPrice)) {
//   ObjectSetInteger(ChartID(), textName, OBJPROP_COLOR, clrBlack);
//   ObjectSetInteger(ChartID(), textName, OBJPROP_FONTSIZE, 10);
//   ObjectSetInteger(ChartID(), textName, OBJPROP_CORNER, CORNER_RIGHT_LOWER); // optional, falls Sie die Angabe in Pixel wünschen
//   ObjectSetString(ChartID(), textName, OBJPROP_TEXT, zoneNames[types[s]]);
//   ObjectSetInteger(ChartID(), textName, OBJPROP_ANCHOR, ANCHOR_RIGHT);       // rechts vom Punkt verankern
//  } }
}
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
int CountZLineObjects() {
 int total = ObjectsTotal(ChartID());
 int count = 0;
 for(int i = 0; i < total; i++) {
  string name = ObjectName(ChartID(), i);
  if(StringFind(name, "ZLine_") == 0) // Prüft, ob der Name mit „ZLine_“ beginnt
   count++; }
 return count; }
//+------------------------------------------------------------------+
//| Puffer für EA exportieren + Zoneneinteilung                 |
//+------------------------------------------------------------------+
void ExportBuffers(int rates_total) {
// Statistiken-Array zusammenstellen
 double stats[]; ArrayResize(stats, totalPriceLevels);
 for(int i = 0; i < totalPriceLevels; i++)
  stats[i] = levels[i].presence_percent;
 double mean = MathMean(stats);
 double sd   = MathStdDev(stats, mean);
 if(sd <= 0) sd = mean * 0.1;
 double adxValue = adx.Main(0);
 int cnt = MathMin(totalPriceLevels,
                   ArraySize(LevelPriceBuffer));
 for(int i = 0; i < cnt; i++) {
  // Art des Gebiets
  double p = levels[i].presence_percent;
  int ztype = 4; // BARREIRA als Standard
  if(p < mean - sd)                                 ztype = 1; // RÜCKGANG
  else if(p > mean + sd && adxValue >= 25.0)        ztype = 2; // TREND
  else if(adxValue < 20.0)                          ztype = 3; // LATERALISIERUNG
  LevelPriceBuffer   [rates_total - 1 - i] = levels[i].price;
  LevelStrengthBuffer[rates_total - 1 - i] = p;
  LevelTypeBuffer    [rates_total - 1 - i] = ztype; } }

//+------------------------------------------------------------------+
//| Hilfsfunktionen                                                         |
//+------------------------------------------------------------------+
bool DoesBarTouchLevel(double high, double low,
                       const PriceLevel &L) {
 return(high >= L.price_low && low <= L.price_high); }

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void ClearPreviousObjects() {
 int tot = ObjectsTotal(ChartID());
 for(int i = tot - 1; i >= 0; i--) {
  string nm = ObjectName(ChartID(), i);
  if(StringFind(nm, "HeatLevel_") == 0)
   ObjectDelete(ChartID(), nm); } }

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
color GetPercentageColor(double p) {
 if(p <= 1.0)      return Color1Percent;
 else if(p <= 25.) return InterpolateColor(
                            Color1Percent,
                            Color25Percent,
                            (p - 1) / 24);
 else if(p <= 50.) return InterpolateColor(
                            Color25Percent,
                            Color50Percent,
                            (p - 25) / 25);
 else if(p <= 75.) return InterpolateColor(
                            Color50Percent,
                            Color75Percent,
                            (p - 50) / 25);
 else              return InterpolateColor(
                            Color75Percent,
                            Color100Percent,
                            (p - 75) / 25); }

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
color InterpolateColor(color c1, color c2, double f) {
 int r1 = (c1 >> 16) & 0xFF, g1 = (c1 >> 8) & 0xFF,
     b1 = c1 & 0xFF;
 int r2 = (c2 >> 16) & 0xFF, g2 = (c2 >> 8) & 0xFF,
     b2 = c2 & 0xFF;
 int r = int(r1 + (r2 - r1) * f),
     g = int(g1 + (g2 - g1) * f),
     b = int(b1 + (b2 - b1) * f);
 return (r << 16) | (g << 8) | b; }

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
int TimeHour(const datetime time) {
 MqlDateTime dt;
 TimeToStruct(time, dt);
 return dt.hour; }
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| DrawAndExport                                                    |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Berechnet den Mittelwert eines Arrays vom Typ „double“                            |
//+------------------------------------------------------------------+
double MathMean(const double &a[], const int count) {
 if(count <= 0) return 0.0;
 double sum = 0.0;
 for(int i = 0; i < count; i++)
  sum += a[i];
 return sum / count; }
//+------------------------------------------------------------------+
//| Berechnet die Stichprobenstandardabweichung eines Double-Arrays           |
//+------------------------------------------------------------------+
double MathStdDev(const double &a[], const int count) {
 if(count < 2) return 0.0;
 double mean = MathMean(a, count);
 double sumSq = 0.0;
 for(int i = 0; i < count; i++)
  sumSq += MathPow(a[i] - mean, 2);
 return MathSqrt(sumSq / (count - 1));  // Stichprobenstandardabweichung
}
//+------------------------------------------------------------------+
Neuronale Netze im Trading: Anomalieerkennung im Frequenzbereich (letzter Teil) Neuronale Netze im Trading: Anomalieerkennung im Frequenzbereich (letzter Teil)
Wir arbeiten weiter an der Implementierung des Frameworks CATCH, das die Fourier-Transformation und Frequenz-Patching-Mechanismen kombiniert und so eine genaue Erkennung von Marktanomalien gewährleistet. In diesem Artikel schließen wir die Implementierung unserer eigenen Vision der vorgeschlagenen Ansätze ab und testen die neuen Modelle an realen historischen Daten.
Quantitative Strategien mit niedriger Handelsfrequenz in MetaTrader 5: (Teil 1) Einrichtung eines OLAP-geeigneten Datenspeichers Quantitative Strategien mit niedriger Handelsfrequenz in MetaTrader 5: (Teil 1) Einrichtung eines OLAP-geeigneten Datenspeichers
Der Artikel skizziert eine praktische Datenpipeline für die quantitative Analyse auf der Grundlage von Parquet-Speicherung, Hive-artigen Partitionen und DuckDB. Er beschreibt die Migration ausgewählter SQLite-Tabellen nach Parquet, die Strukturierung von Marktdaten nach Quelle, Symbol, Zeitrahmen und Datum sowie deren Abfrage mit SQL-Fensterfunktionen. Das Beispiel eines Goldenen Kreuzes veranschaulicht die symbolübergreifende Auswertung von künftigen Renditen. Begleitende Python-Skripte übernehmen den Datendownload, die Konvertierung und die Ausführung.
Untersuchung von Regressionsmodellen für kausale Inferenz und algorithmischen Handel Untersuchung von Regressionsmodellen für kausale Inferenz und algorithmischen Handel
Der Artikel untersucht die Möglichkeit, Regressionsmodelle im algorithmischen Handel einzusetzen. Regressionsmodelle ermöglichen im Gegensatz zur binären Klassifizierung die Erstellung flexiblerer Handelsstrategien durch die Quantifizierung vorhergesagter Kursänderungen.
Code, Tränen und Algo Forge Code, Tränen und Algo Forge
Dieser Artikel erörtert den Übergang zu MQL5 Algo Forge als modernes und bequemes Format für die Veröffentlichung von Programmcode und Anhängen zu Artikeln. Die Verwendung von Repositories anstelle von herkömmlichen ZIP-Archiven und Quellcode ermöglicht es Ihnen, Projekte auf dem neuesten Stand zu halten, Änderungen schnell vorzunehmen und professionell mit Ihren Lesern zu interagieren. Es werden Empfehlungen für die schnelle Migration bestehender Projekte in die Cloud über die MetaEditor-Schnittstelle gegeben.