English
preview
Verfolgung der Kontodynamik: Visualisierung von Kontosaldo, Kontoeigenkapital und laufendem Gewinn/Verlust in MQL5

Verfolgung der Kontodynamik: Visualisierung von Kontosaldo, Kontoeigenkapital und laufendem Gewinn/Verlust in MQL5

MetaTrader 5Indikatoren |
20 3
Nervada Emeule Adams
Nervada Emeule Adams

Einführung

Die meisten Händler konzentrieren sich stark auf ihre Chartanalyse, Indikatoren und Handelsstrategien, aber erstaunlich wenige achten darauf, wie sich ihr tatsächliches Konto im Laufe der Zeit verhält. Der MetaTrader 5 bietet Ihnen Zugriff auf Ihren aktuellen Kontostand und Ihre Kontoeigenkapitalzahlen, aber er bietet keine historische Übersicht darüber, wie sich diese Zahlen seit Beginn Ihres Handels zusammen oder auseinander entwickelt haben. Sie können sehen, wo Sie jetzt sind, aber nicht, wie Sie dorthin gekommen sind.

Dies wird zu einem Problem, wenn Sie Ihre Handelsleistung über einfache Gewinnquoten und Gewinnsummen hinaus analysieren wollen. Wenn Sie das Verhältnis zwischen Ihrem Kontosaldo, dem Kontoeigenkapital und dem variablen Gewinn oder Verlust im Laufe der Zeit verstehen, können Sie Muster in Bezug auf Ihr Risikomanagement, Ihr Positionshalteverhalten und die allgemeine Gesundheit Ihres Kontos erkennen. Ohne diesen visuellen Kontext fehlt Ihnen der Blick auf die statistische Entwicklung Ihres Kontos.

Die übliche Abhilfe ist der Export von Daten in Tabellenkalkulationen oder die Verwendung von Analyseplattformen von Drittanbietern. Diese Lösungen funktionieren, aber sie verlagern die Analyse aus dem Handelsterminal heraus und erfordern oft manuelle Aktualisierungen. Wie wäre es, wenn Sie all dies direkt im MetaTrader 5 sehen könnten, dargestellt als saubere Kurven in einem Unterfenster, die sich automatisch aktualisieren, während Sie handeln?

Dieser Artikel führt Sie durch die Erstellung eines benutzerdefinierten Indikators, der genau das tut. Er rekonstruiert Ihre gesamte Handelshistorie und zeichnet vier Schlüsselkurven auf: Ihren Anfangssaldo als Referenzlinie, die Entwicklung Ihres Kontosaldos und Kontoeigenkapitals und Ihren variablen Gewinn oder Verlust. Alles läuft innerhalb von MetaTrader 5, ohne externe Abhängigkeiten. Der Indikator tastet die Daten einmal pro Bar ab, um eine gleichmäßige Leistung zu gewährleisten und gleichzeitig die Positionen aller von Ihnen gehandelten Symbole zu verfolgen. Am Ende haben Sie ein funktionierendes Analysetool, das Ihnen zeigt, wie sich Ihr Konto unter realen Handelsbedingungen verhält.


Der Visualisierungsansatz

Die standardmäßige MetaTrader 5-Kontoüberwachung zeigt nur isolierte Momentaufnahmen an, was es schwer macht, zu verstehen, wie die Ergebnisse tatsächlich erzielt wurden. Durch die Behandlung von Kontosaldo, Kontoeigenkapital und dem laufenden Gewinn/Verlust als kontinuierliche Zeitreihen werden diese Kennzahlen zu einer visuellen Darstellung, die das Halteverhalten, den Drawdown und die Erholungsdynamik aufzeigen.

Das Per-Bar-Sampling bietet das richtige Gleichgewicht zwischen Genauigkeit und Leistung, indem es die Kontokurven mit den Kurscharts abgleicht und gleichzeitig über lange Zeiträume hinweg effizient bleibt. Die Verfolgung von mehreren Symbolen wird durch die Zwischenspeicherung von Preisen ermöglicht, wodurch schwerwiegende Leistungsprobleme vermieden werden.

Eine genaue Rekonstruktion erfordert die Verarbeitung der gesamten Deal-Historie in chronologischer Reihenfolge, beginnend mit der ersten Einzahlung und unter Beibehaltung des Positionsstatus im Laufe der Zeit. Bei richtiger Vorgehensweise entstehen so vier Kurven, die das historische Verhalten des Kontos vollständig beschreiben.

Visualisierungsprozess-Flussdiagramm
Abbildung 1: Arbeitsablauf zur Visualisierung der Kontodynamik, der den Prozess vom Laden der historischen Daten bis zur Ausgabe der endgültigen Kurve zeigt


Implementierung in MQL5

Indikatoreigenschaften und Puffereinstellungen

Der Indikator läuft in einem separaten Unterfenster und zeichnet vier verschiedene Kurven. Hier ist der vollständige Abschnitt der Eigenschafts- und Pufferdeklaration des Indikators:

//+------------------------------------------------------------------+
//|                                   AccountDynamicsIndicator.mq5   |
//|                                  Copyright 2025, Persist FX      |
//|                                     Educational Analysis Tool    |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, Persist FX"
#property link      "https://www.mql5.com"
#property version   "2.0"
#property description "Visual analysis of account balance, equity, and floating P/L dynamics."
#property description "Displays historical account statistics for analytical purposes."
#property indicator_separate_window
#property indicator_buffers 4
#property indicator_plots   4

//--- plot Starting Balance (Reference Line)
#property indicator_label1  "Starting Balance"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrGray
#property indicator_style1  STYLE_DASH
#property indicator_width1  1

//--- plot Balance
#property indicator_label2  "Balance"
#property indicator_type2   DRAW_LINE
#property indicator_color2  clrOrange
#property indicator_style2  STYLE_SOLID
#property indicator_width2  2

//--- plot Equity
#property indicator_label3  "Equity"
#property indicator_type3   DRAW_LINE
#property indicator_color3  clrDodgerBlue
#property indicator_style3  STYLE_SOLID
#property indicator_width3  2

//--- plot Floating P/L
#property indicator_label4  "Floating P/L"
#property indicator_type4   DRAW_LINE
#property indicator_color4  clrLime
#property indicator_style4  STYLE_SOLID
#property indicator_width4  1

//--- input parameters
input int InpHistoryBars = 0;           // Number of bars to display (0 = all available)
input int InpUpdatePeriod = 300;        // History update period in seconds (default 5 min)

//--- indicator buffers
double StartingBalanceBuffer[];
double BalanceBuffer[];
double EquityBuffer[];
double FloatingBuffer[];

Für den Anfangssaldo wird eine gestrichelte graue Linie verwendet, um sie als Referenzpunkt und nicht als dynamische Daten zu kennzeichnen. Kontosaldo und -eigenkapital erhalten dickere durchgezogene Linien, da sie die wichtigsten Kennzahlen sind, auf die sich Händler konzentrieren. Die InpUpdatePeriod ist standardmäßig auf 300 Sekunden (5 Minuten) eingestellt, um ein Kontosaldo zwischen Reaktionsfähigkeit und Rechenaufwand bei der Prüfung auf neue geschlossene Trades zu schaffen.

Datenstrukturen für die historische Verfolgung

Der Indikator muss den historischen Zustand über mehrere Berechnungszyklen hinweg beibehalten. Wir verwenden drei benutzerdefinierte Strukturen, um diese Daten effizient zu organisieren.

//--- structure to store balance changes over time
struct BalancePoint
{
   datetime time;
   double balance;
};

//--- structure for position tracking
struct PositionInfo
{
   ulong ticket;
   string symbol;
   datetime openTime;
   datetime closeTime;
   double openPrice;
   double volume;
   ENUM_POSITION_TYPE type;
   double tickSize;
   double tickValue;
};

//--- price cache for multi-symbol efficiency
struct PriceCache
{
   string symbol;
   datetime time;
   double price;
};

In der Struktur BalancePoint wird festgehalten, wann sich Ihr Kontosaldo geändert hat und wie hoch der neue Wert war. Jeder geschlossene Trade erzeugt einen neuen Punkt in der Historie der Kontosalden. In der Struktur PositionInfo werden alle Informationen gespeichert, die für die Berechnung von historischen variablen Gewinnen und Verlusten für eine Position erforderlich sind. Beachten Sie, dass wir tickSize und tickValue hier zwischenspeichern, anstatt sie während der Berechnungen wiederholt abzurufen. Dies ist eine wichtige Leistungsoptimierung bei der Arbeit mit mehreren Symbolen.

Die Struktur PriceCache behebt einen wichtigen Leistungsengpass. Ohne Zwischenspeicherung würden wir CopyRates() für dasselbe Symbol und dieselbe Zeit wiederholt aufrufen, wenn wir historische Bars verarbeiten. Durch die Speicherung der abgerufenen Preise verwandeln wir eine O(n²)-Operation in etwas, das näher an O(n) liegt.

BalancePoint balanceHistory[];
PositionInfo positionCache[];
datetime lastHistoryUpdate = 0;
datetime earliestDealTime = 0;
double initialDeposit = 0;
int totalDealsProcessed = 0;

//--- price cache for multi-symbol efficiency
PriceCache priceCache[];

Diese globalen Arrays bleiben zwischen den Aufrufen von OnCalculate() bestehen und erhalten den Status über die gesamte Lebensdauer des Indikators. Mit der Variable totalDealsProcessed können wir erkennen, wann neue Trades abgeschlossen wurden, ohne die Historie unnötig neu aufzubauen. Die earliestDealTime legt fest, wo unsere Kurven auf dem Chart beginnen sollen, um zu verhindern, dass wir Berechnungen durchführen, bevor eine Handelsaktivität stattgefunden hat.

Initialisierungsprozess (OnInit)

Die Funktion OnInit() wird einmal beim Laden des Indikators ausgeführt. An dieser Stelle verbinden wir unsere Pufferfelder mit dem Indikatorsystem und erstellen die komplette Kontohistorie.

int OnInit()
{
   //--- indicator buffers mapping
   SetIndexBuffer(0, StartingBalanceBuffer, INDICATOR_DATA);
   SetIndexBuffer(1, BalanceBuffer, INDICATOR_DATA);
   SetIndexBuffer(2, EquityBuffer, INDICATOR_DATA);
   SetIndexBuffer(3, FloatingBuffer, INDICATOR_DATA);
   
   //--- set arrays as series
   ArraySetAsSeries(StartingBalanceBuffer, true);
   ArraySetAsSeries(BalanceBuffer, true);
   ArraySetAsSeries(EquityBuffer, true);
   ArraySetAsSeries(FloatingBuffer, true);
   
   //--- set precision
   IndicatorSetInteger(INDICATOR_DIGITS, 2);
   
   //--- set indicator short name
   IndicatorSetString(INDICATOR_SHORTNAME, "Account Dynamics");
   
   //--- initialize buffers
   ArrayInitialize(StartingBalanceBuffer, EMPTY_VALUE);
   ArrayInitialize(BalanceBuffer, EMPTY_VALUE);
   ArrayInitialize(EquityBuffer, EMPTY_VALUE);
   ArrayInitialize(FloatingBuffer, EMPTY_VALUE);
   
   //--- build complete account history
   if(!BuildAccountHistory())
   {
      Print("Warning: Could not build complete account history");
   }
   
   lastHistoryUpdate = TimeCurrent();
   
   return(INIT_SUCCEEDED);
}

Die Funktion SetIndexBuffer() bindet unsere Arrays an das Plottsystem des Indikators. Der zweite Parameter, INDICATOR_DATA, teilt MQL5 mit, dass diese Puffer Werte enthalten, die gezeichnet werden sollen, im Gegensatz zu INDICATOR_CALCULATIONS, die Zwischenwerte enthalten würden, die nicht für die Anzeige bestimmt sind. Die Pufferindizes müssen mit der Reihenfolge übereinstimmen, die wir im Abschnitt Eigenschaften angegeben haben.

Die Aufrufe von ArraySetAsSeries() sind unerlässlich. Standardmäßig indexieren MQL5-Arrays vom ältesten zum neuesten, aber Chartdaten fließen natürlich vom neuesten zum ältesten. Die Einstellung von Arrays als Serie kehrt die Indizierung um, sodass Index 0 immer die jüngste Bar darstellt, was der Funktionsweise der Zeitreihen-Arrays in OnCalculate() entspricht. Ohne dies würden unsere Daten rückwärts gezeichnet.

Wir initialisieren alle Puffer mit EMPTY_VALUE, wodurch das Terminal angewiesen wird, an diesen Stellen nichts zu zeichnen. Dadurch wird verhindert, dass in Bereichen, für die keine Daten vorliegen, falsche Linien erscheinen. Die Funktion BuildAccountHistory() übernimmt die aufwendige Rekonstruktion Ihrer Handelshistorie, die wir im Folgenden untersuchen werden.

Historische Daten rekonstruieren (BuildAccountHistory)

Diese Funktion rekonstruiert Ihre gesamte Handelshistorie, indem sie jeden Deal verarbeitet, der auf Ihrem Konto ausgeführt wurde. Es ist der komplexeste Teil des Indikators, aber auch der wichtigste für eine genaue Visualisierung.

bool BuildAccountHistory()
{
   datetime toDate = TimeCurrent();
   datetime fromDate = 0; // From account inception
   
   if(!HistorySelect(fromDate, toDate))
   {
      Print("Error: Failed to load history - ", GetLastError());
      return false;
   }
   
   //--- clear previous data
   ArrayResize(balanceHistory, 0);
   ArrayResize(positionCache, 0);
   ArrayResize(priceCache, 0);
   
   int totalDeals = HistoryDealsTotal();
   totalDealsProcessed = totalDeals;
   
   if(totalDeals == 0)
   {
      Print("No trading history found");
      initialDeposit = AccountInfoDouble(ACCOUNT_BALANCE);
      AddBalancePoint(TimeCurrent(), initialDeposit);
      earliestDealTime = TimeCurrent();
      return true;
   }
   
   Print("Processing ", totalDeals, " deals from account history...");

Die Funktion HistorySelect() lädt die Historie der Deals in den Speicher. Durch die Angabe fromDate = 0 werden alle Daten ab der ersten Transaktion des Kontos abgefragt. Dies ist nicht dasselbe wie die Handelshistorie. Zu den Deals gehören Einzahlungen, Abhebungen, Kontosaldooperationen sowie Ein- und Ausstiegstrades. Die Funktion gibt false zurück, wenn der Historienserver nicht verfügbar ist oder ein Verbindungsproblem besteht.

Nach dem Laden zeigt HistoryDealsTotal() an, wie viele Datensätze von Deals vorhanden sind. Wenn dieser Wert null ist, handelt es sich vermutlich um ein neues Konto ohne Handelsaktivität. In diesem Fall erstellen wir einen einzigen Kontosaldenpunkt beim aktuellen Kontosaldo und kehren zurück. Für Konten mit Aktivität müssen wir die Ersteinzahlung und den frühesten Handelszeitpunkt ermitteln.

//--- find earliest deal and initial deposit
   earliestDealTime = TimeCurrent();
   double runningBalance = 0;
   bool foundInitialDeposit = false;
   
   for(int i = 0; i < totalDeals; i++)
   {
      ulong dealTicket = HistoryDealGetTicket(i);
      if(dealTicket == 0) continue;
      
      datetime dealTime = (datetime)HistoryDealGetInteger(dealTicket, DEAL_TIME);
      ENUM_DEAL_TYPE dealType = (ENUM_DEAL_TYPE)HistoryDealGetInteger(dealTicket, DEAL_TYPE);
      
      //--- find first balance operation (deposit/credit)
      if(!foundInitialDeposit && (dealType == DEAL_TYPE_BALANCE || dealType == DEAL_TYPE_CREDIT))
      {
         initialDeposit = HistoryDealGetDouble(dealTicket, DEAL_PROFIT);
         runningBalance = initialDeposit;
         earliestDealTime = dealTime;
         foundInitialDeposit = true;
         AddBalancePoint(dealTime, runningBalance);
      }
      
      if(dealTime < earliestDealTime)
         earliestDealTime = dealTime;
   }
   
   //--- if no initial deposit found, use first deal as reference
   if(!foundInitialDeposit)
   {
      initialDeposit = AccountInfoDouble(ACCOUNT_BALANCE);
      runningBalance = initialDeposit;
      AddBalancePoint(earliestDealTime, initialDeposit);
   }

Die Funktion HistoryDealGetTicket() ruft die eindeutige Kennung von jedem, Deal der Reihe nach ab. Anschließend verwenden wir HistoryDealGetInteger() und HistoryDealGetDouble(), um bestimmte Eigenschaften der Deals zu extrahieren. DEAL_TYPE_BALANCE steht für Einzahlungen oder Abhebungen, während DEAL_TYPE_CREDIT für Bonusgutschriften verwendet wird. Das erste Vorkommen eines dieser beiden Werte wird zur Ersteinzahlung und bildet den Ausgangspunkt für alle nachfolgenden Berechnungen.

Einige Konten haben möglicherweise keinen expliziten Einzahlungs-Deal, wenn sie mit Startkapital eingerichtet wurden. In diesem Fall greifen wir auf den aktuellen Kontosaldo als Referenzpunkt zurück. Dies ist zwar nicht perfekt, bietet aber einen vernünftigen Näherungswert für die Visualisierung.

//--- process all trading deals chronologically
   for(int i = 0; i < totalDeals; i++)
   {
      ulong dealTicket = HistoryDealGetTicket(i);
      if(dealTicket == 0) continue;
      
      datetime dealTime = (datetime)HistoryDealGetInteger(dealTicket, DEAL_TIME);
      ENUM_DEAL_ENTRY dealEntry = (ENUM_DEAL_ENTRY)HistoryDealGetInteger(dealTicket, DEAL_ENTRY);
      ENUM_DEAL_TYPE dealType = (ENUM_DEAL_TYPE)HistoryDealGetInteger(dealTicket, DEAL_TYPE);
      
      //--- skip non-trading deals in this loop
      if(dealType == DEAL_TYPE_BALANCE || dealType == DEAL_TYPE_CREDIT)
         continue;
      
      double dealProfit = HistoryDealGetDouble(dealTicket, DEAL_PROFIT);
      double dealSwap = HistoryDealGetDouble(dealTicket, DEAL_SWAP);
      double dealCommission = HistoryDealGetDouble(dealTicket, DEAL_COMMISSION);
      ulong positionId = HistoryDealGetInteger(dealTicket, DEAL_POSITION_ID);
      string symbol = HistoryDealGetString(dealTicket, DEAL_SYMBOL);

Nun gehen wir erneut alle Deals durch, wobei wir uns dieses Mal auf die Handelsaktivitäten konzentrieren. Die Eigenschaft DEAL_ENTRY gibt an, ob ein Deal eine Position eröffnet (DEAL_ENTRY_IN), geschlossen (DEAL_ENTRY_OUT) oder umgekehrt (DEAL_ENTRY_INOUT) hat. Diese Unterscheidung ist von entscheidender Bedeutung, da sich nur die schließenden Deals auf den Kontosaldo auswirken, wir aber die eröffnenden Deals verfolgen müssen, um die historischen variablen Gewinne und Verluste zu berechnen.

//--- handle position entry
      if(dealEntry == DEAL_ENTRY_IN)
      {
         int size = ArraySize(positionCache);
         ArrayResize(positionCache, size + 1);
         
         positionCache[size].ticket = positionId;
         positionCache[size].symbol = symbol;
         positionCache[size].openTime = dealTime;
         positionCache[size].closeTime = 0;
         positionCache[size].openPrice = HistoryDealGetDouble(dealTicket, DEAL_PRICE);
         positionCache[size].volume = HistoryDealGetDouble(dealTicket, DEAL_VOLUME);
         positionCache[size].type = (dealType == DEAL_TYPE_BUY) ? POSITION_TYPE_BUY : POSITION_TYPE_SELL;
         
         //--- cache symbol specifications
         positionCache[size].tickSize = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);
         positionCache[size].tickValue = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE);
         
         if(positionCache[size].tickSize == 0)
            positionCache[size].tickSize = SymbolInfoDouble(symbol, SYMBOL_POINT);
      }

Wenn eine Position geöffnet wird, speichern wir alle Details in dem Array positionCache. Der Wert von closeTime ist Null, was bedeutet, dass die Position in dieser historischen Rekonstruktion konzeptionell noch offen ist. Die Tick-Spezifikationen des Symbols werden sofort mit SymbolInfoDouble() abgerufen und zwischengespeichert. Diese Werte bestimmen, wie sich Kursbewegungen in Gewinn und Verlust niederschlagen. Durch das Zwischenspeichern dieser Daten müssen wir sie bei der Berechnung des laufenden Gewinns/Verlusts nicht erneut abrufen und sparen so Tausende von überflüssigen Suchvorgängen.

//--- handle position exit (balance change)
      else if(dealEntry == DEAL_ENTRY_OUT || dealEntry == DEAL_ENTRY_INOUT)
      {
         //--- mark position as closed
         for(int j = 0; j < ArraySize(positionCache); j++)
         {
            if(positionCache[j].ticket == positionId && positionCache[j].closeTime == 0)
            {
               positionCache[j].closeTime = dealTime;
               break;
            }
         }
         
         //--- update balance
         double balanceChange = dealProfit + dealSwap + dealCommission;
         if(balanceChange != 0)
         {
            runningBalance += balanceChange;
            AddBalancePoint(dealTime, runningBalance);
         }
      }
   }
   
   Print("History built: ", ArraySize(balanceHistory), " balance points, ", 
         ArraySize(positionCache), " positions tracked, Initial deposit: ", initialDeposit);
   
   return true;
}

Wenn eine Position geschlossen wird, suchen wir ihren Eintrag im Cache und markieren sie als geschlossen, indem wir closeTime einen Wert zuweisen. Dies ist wichtig, da unser variabler P/L-Rechner wissen muss, ob eine Position zu einem bestimmten historischen Zeitpunkt offen war. Der Kontosaldo ändert sich nur, wenn Trades geschlossen werden. Wir berechnen also den Nettoeffekt von Gewinn, Swap und Provision und fügen dann einen neuen Wert des Kontosaldos zu unserer Historie hinzu. Die Hilfsfunktion AddBalancePoint() fügt diese Daten einfach an das Array balanceHistory an.

Hauptberechnungsschleife (OnCalculate)

Die Funktion OnCalculate() wird jedes Mal ausgeführt, wenn neue Kursdaten eintreffen oder wenn der Benutzer durch das Chart scrollt. Hier werden die Indikatorpuffer mit den berechneten Werten für jede sichtbare Bar gefüllt.

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 < 1)
      return 0;
   
   ArraySetAsSeries(time, true);
   
   //--- check if we need to update history
   datetime currentTime = TimeCurrent();
   if(currentTime - lastHistoryUpdate > InpUpdatePeriod)
   {
      //--- only rebuild if new deals appeared
      HistorySelect(0, currentTime);
      int currentDeals = HistoryDealsTotal();
      
      if(currentDeals != totalDealsProcessed)
      {
         Print("New deals detected, rebuilding history...");
         BuildAccountHistory();
      }
      
      lastHistoryUpdate = currentTime;
   }

Der Parameter rates_total gibt an, wie viele Bars im Chart vorhanden sind, während prev_calculated angibt, wie viele wir bereits verarbeitet haben. Beim ersten Aufruf ist prev_calculated gleich Null, was bedeutet, dass wir alles neu berechnen müssen. Bei späteren Aufrufen muss in der Regel nur die letzte Bar aktualisiert werden. Die als Parameter übergebenen Zeitreihen-Arrays sind nicht automatisch serienindiziert, daher rufen wir ArraySetAsSeries (time, true) auf, um sie mit unserer Pufferindizierung abzugleichen.

Die regelmäßige Überprüfung der Historie ist eine intelligente Optimierung. Anstatt bei jedem Tick neu aufzubauen, prüfen wir in konfigurierbaren Abständen, ob neue Deals hinzugekommen sind. Durch den Vergleich von HistoryDealsTotal() mit unserer gespeicherten TotalDealsProcessed wissen wir sofort, ob sich etwas geändert hat. Erst dann fällt der Aufwand für einen Neuaufbau der gesamten Historie an.

//--- calculate bars to process
   int limit = rates_total - prev_calculated;
   if(prev_calculated == 0)
      limit = rates_total;
   
   if(InpHistoryBars > 0 && limit > InpHistoryBars)
      limit = InpHistoryBars;
   
   //--- get current account values
   double currentBalance = AccountInfoDouble(ACCOUNT_BALANCE);
   double currentEquity = AccountInfoDouble(ACCOUNT_EQUITY);

Die Grenzwertvariable bestimmt, wie viele Bars verarbeitet werden müssen. Wenn prev_calculated gleich Null ist, werden alle Bars verarbeitet. Andernfalls werden nur neue Bars verarbeitet, die seit dem letzten Aufruf erschienen sind. Wenn der Benutzer über InpHistoryBars eine maximale Länge des Verlaufs angegeben hat, wird diese Grenze eingehalten. Die Funktion AccountInfoDouble() ruft Live-Kontowerte unter Verwendung der vordefinierten Konstanten ACCOUNT_BALANCE und ACCOUNT_EQUITY ab.

//--- process each bar
   for(int i = 0; i < limit; i++)
   {
      datetime barTime = time[i];
      
      //--- before earliest deal, no data available
      if(barTime < earliestDealTime)
      {
         StartingBalanceBuffer[i] = EMPTY_VALUE;
         BalanceBuffer[i] = EMPTY_VALUE;
         EquityBuffer[i] = EMPTY_VALUE;
         FloatingBuffer[i] = EMPTY_VALUE;
         continue;
      }
      
      //--- starting balance is constant horizontal line
      StartingBalanceBuffer[i] = initialDeposit;
      
      //--- for the most recent bar, use live account data
      if(i == 0)
      {
         BalanceBuffer[i] = currentBalance;
         FloatingBuffer[i] = currentEquity - currentBalance;
         EquityBuffer[i] = currentEquity;
      }
      else
      {
         //--- get historical balance at this bar
         double balanceAtTime = GetBalanceAtTime(barTime, currentBalance);
         
         //--- calculate historical floating P/L
         double floatingPL = CalculateFloatingPLAtTime(barTime);
         
         BalanceBuffer[i] = balanceAtTime;
         FloatingBuffer[i] = floatingPL;
         EquityBuffer[i] = balanceAtTime + floatingPL;
      }
   }
   
   return rates_total;
}

Für die Bar vor dem ersten Trade setzen wir alle Puffer auf EMPTY_VALUE, damit nichts gezeichnet wird. Die Linie des Anfangssaldos erhält bei jeder Bar den konstanten initialDeposit-Wert, wodurch die horizontale Referenzlinie entsteht. Index 0 steht immer für die aktuelle Bar, bei dem wir direkt die aktuellen Kontodaten verwenden. Bei historischen Bars rufen wir Hilfsfunktionen auf, um den Kontosaldo zu einem bestimmten Zeitpunkt abzurufen und zu berechnen, wie hoch der variable Gewinn oder Verlust war. Das Kontoeigenkapital ist einfach der Kontosaldo plus dem laufenden Gewinn/Verlust zu einem bestimmten Zeitpunkt.

Die Rückgabe von rates_total teilt dem Terminal mit, dass wir alle verfügbaren Bars erfolgreich verarbeitet haben. Dieser Wert wird beim nächsten Aufruf zu prev_calculated, was inkrementelle Aktualisierungen ermöglicht.

Berechnung des laufenden Gewinn/Verlust

Der laufende Gewinn/Verlust ist der Ort, an dem die eigentliche Komplexität des Multi-Symbol-Ansatzes liegt. Wir benötigen Hilfsfunktionen, um historische Kontosalden abzurufen, Positionsbewertungen zu berechnen und Preise effizient zwischenzuspeichern.

double GetBalanceAtTime(datetime targetTime, double currentBalance)
{
   int historySize = ArraySize(balanceHistory);
   
   if(historySize == 0)
      return currentBalance;
   
   //--- if before first balance point, return initial deposit
   if(targetTime < balanceHistory[0].time)
      return initialDeposit;
   
   //--- find the most recent balance point before target time
   double balance = initialDeposit;
   
   for(int i = 0; i < historySize; i++)
   {
      if(balanceHistory[i].time <= targetTime)
         balance = balanceHistory[i].balance;
      else
         break;
   }
   
   return balance;
}

Diese Funktion durchläuft unser Array balanceHistory, um herauszufinden, wie hoch der Kontosaldo zu einem bestimmten Zeitpunkt war. Der Kontosaldo ändert sich nur, wenn die Trades geschlossen werden. Wir suchen also nach dem letzten Kontosaldo, der vor oder zum Zielzeitpunkt lag. Liegt der Zielzeitpunkt vor der ersten von uns erfassten Änderung des Kontostandes, erhalten Sie die ursprüngliche Einzahlung zurück. Dadurch entsteht das stufenförmige Erscheinungsbild der Kontosaldenkurve, die zwischen geschlossenen Trades waagerecht verläuft.

double CalculateFloatingPLAtTime(datetime barTime)
{
   double totalPL = 0;
   int positionsProcessed = 0;
   
   //--- iterate through all cached positions
   for(int i = 0; i < ArraySize(positionCache); i++)
   {
      //--- skip if position not yet opened at bar time
      if(positionCache[i].openTime > barTime)
         continue;
      
      //--- skip if position already closed at bar time
      if(positionCache[i].closeTime != 0 && positionCache[i].closeTime <= barTime)
         continue;
      
      //--- position was open at this bar time, calculate its P/L
      double priceAtTime = GetCachedPrice(positionCache[i].symbol, barTime);
      
      if(priceAtTime <= 0)
         continue;
      
      //--- calculate P/L using cached tick size and value
      if(positionCache[i].tickSize <= 0 || positionCache[i].tickValue <= 0)
         continue;
      
      double priceDiff = (positionCache[i].type == POSITION_TYPE_BUY) ?
                         priceAtTime - positionCache[i].openPrice :
                         positionCache[i].openPrice - priceAtTime;
      
      double positionPL = (priceDiff / positionCache[i].tickSize) * 
                          positionCache[i].tickValue * 
                          positionCache[i].volume;
      
      totalPL += positionPL;
      positionsProcessed++;
   }
   
   return totalPL;
}

Dies ist der rechnerische Kern des Indikators. Für jede historische Bar durchlaufen wir jede Position, die wir verfolgt haben, und stellen fest, ob sie zu diesem Zeitpunkt offen war. Eine Position gilt als offen, wenn ihre openTime vor oder zur Barzeit liegt und sie entweder nie geschlossen wurde oder ihre closeTime nach der Barzeit liegt. Für jede offene Position wird der Preis der jeweiligen Bar ermittelt und der Gewinn oder Verlust berechnet.

Die Gewinnberechnung erfolgt nach der Standardformel: Preisdifferenz geteilt durch Tick-Größe ergibt die Anzahl der bewegten Ticks, multipliziert mit dem Tick-Wert ergibt den Gewinn pro Lot, und multipliziert mit dem Volumen ergibt sich der Gesamtgewinn der Position. Bei Kaufpositionen entspricht der Gewinn dem aktuellen Kurs abzüglich des offenen Kurses. Bei Verkäufen ist es umgekehrt. Wir kumulieren alle offenen Positionen, um den gesamten laufenden Gewinn/Verlust zu diesem Zeitpunkt zu erhalten.

double GetCachedPrice(string symbol, datetime barTime)
{
   //--- check cache first
   for(int i = 0; i < ArraySize(priceCache); i++)
   {
      if(priceCache[i].symbol == symbol && priceCache[i].time == barTime)
         return priceCache[i].price;
   }
   
   //--- not in cache, fetch price
   double price = 0;
   
   if(symbol == _Symbol)
   {
      //--- current chart symbol (fastest)
      int shift = iBarShift(_Symbol, Period(), barTime, true);
      if(shift >= 0)
         price = iClose(_Symbol, Period(), shift);
   }
   else
   {
      //--- other symbol, use CopyRates
      MqlRates rates[];
      ArraySetAsSeries(rates, true);
      int copied = CopyRates(symbol, Period(), barTime, 1, rates);
      
      if(copied > 0)
         price = rates[0].close;
   }
   
   //--- add to cache
   if(price > 0)
   {
      int cacheSize = ArraySize(priceCache);
      
      //--- limit cache size to prevent memory issues
      if(cacheSize >= 1000)
      {
         //--- clear oldest 500 entries
         ArrayRemove(priceCache, 0, 500);
         cacheSize = ArraySize(priceCache);
      }
      
      ArrayResize(priceCache, cacheSize + 1);
      priceCache[cacheSize].symbol = symbol;
      priceCache[cacheSize].time = barTime;
      priceCache[cacheSize].price = price;
   }
   
   return price;
}

Dies ist die Leistungsoptimierung, die das Multi-Symbol-Tracking möglich macht. Bevor wir einen Preis abrufen, prüfen wir, ob wir ihn bereits abgerufen haben. Wenn er im Cache gefunden wird, geben wir ihn sofort zurück. Ist dies nicht der Fall, werden sie entweder mit iBarShift() und iClose() für das aktuelle Chart-Symbol oder mit CopyRates() für andere Symbole geholt. Die Funktion iBarShift() findet heraus, welcher Barindex einem bestimmten Zeitpunkt entspricht, während CopyRates() historische Bardaten für jedes Symbol abruft.

Nach dem Abruf fügen wir den Preis zu unserem Cache hinzu. Der Cache ist auf 1000 Einträge begrenzt. Bei Überschreitung werden die ältesten 500 mittels ArrayRemove() entfernt. Dadurch wird ein Speicherwachstum verhindert, während gleichzeitig eine ausreichende Cache-Tiefe für typische Berechnungen beibehalten wird. Ohne diese Zwischenspeicherung würde der Indikator Tausende von überflüssigen Aufrufen von CopyRates() tätigen, was zu erheblichen Verzögerungen führen würde.

void OnDeinit(const int reason)
{
   Comment("");
   ArrayFree(balanceHistory);
   ArrayFree(positionCache);
   ArrayFree(priceCache);
}

Die Deinitialisierungsfunktion bereinigt alles, wenn der Indikator entfernt oder neu kompiliert wird. Wir löschen den Kommentarbereich und geben alle dynamischen Arrays frei. Obwohl MQL5 den Speicher automatisch verwaltet, ist das explizite Freigeben großer Arrays eine gute Praxis, insbesondere für Indikatoren, die während der Entwicklung häufig neu geladen werden könnten.



Live-Beispiele

Laden und Erstanzeige

Wenn Sie den Indikator an ein beliebiges Chart anhängen, beginnt er sofort damit, Ihren Kontoverlauf zu rekonstruieren und die vier Kurven in einem separaten Unterfenster darzustellen. Der Prozess läuft automatisch ab und erfordert außer den optionalen Eingabeparametern keine weitere Konfiguration. Der Indikator zeigt Ihren gesamten Handelsverlauf ab der ersten Einzahlung an, unabhängig davon, welches Symbol oder welchen Zeitrahmen Sie gerade betrachten.

Zur Unterscheidung der Metriken wird eine Farbcodierung verwendet. Die graue gestrichelte Linie stellt Ihr Anfangssaldo dar und bleibt über alle Bars hinweg konstant und dient als Basisreferenz. Die orangefarbene Linie zeigt Ihren realisierten Kontosaldo und erhöht oder verringert ihn nur, wenn der Trade geschlossen wird. Blau zeigt das Kontoeigenkapital, das den Kontosaldo mit nicht realisierten Gewinnen oder Verlusten aus offenen Positionen kombiniert. Grün stellt die variable Gewinn- und Verlustkomponente separat dar, sodass Sie leicht erkennen können, wie viel Ihres Kontoeigenkapitals bei offenen Trades einem Risiko ausgesetzt und wie viel sicher als Kontosaldo eingeschlossen ist.

Abbildung 2: Indikatoranzeige, die alle vier Kurven im Unterfenster in unterschiedlichen Farben zeigt

Abbildung 2. Der Indikator läuft in einem separaten Unterfenster, in dem die Kurven für den Anfangssaldo (grau gestrichelt), den Kontosaldo (orange), das Kontoeigenkapital (blau) und dem laufenden Gewinn/Verlust (grün) gleichzeitig angezeigt werden.


Das Unterfenster wird automatisch skaliert, um sich allen Kurven anzupassen, obwohl in Zeiten eines erheblichen Drawdowns oder einer aggressiven Positionsgröße die variablen Gewinn- und Verlustschwankungen den visuellen Raum dominieren können. Händler können die Höhe des Unterfensters durch Ziehen des Randes anpassen, wenn sie mehr vertikale Auflösung benötigen, um zwischen eng beieinander liegenden Kurven zu unterscheiden.

Historische Verifizierung und Musteranalyse

Die Genauigkeit des Indikators kann durch einen Abgleich mit der Registerkarte „Kontohistorie“ des MetaTrader überprüft werden. Wenn Sie den Zeitstempel Ihrer ersten Einzahlung in der Historie ausfindig machen und das Chart bis zu diesem Datum zurückblättern, sollten Sie sehen, dass sich die Linie des Anfangssaldos zu diesem Zeitpunkt sowohl mit der Kontosaldo- als auch mit der Kontoeigenkapitalkurve schneidet. Diese Konvergenz bestätigt, dass der Indikator den Beginn Ihres Kontos korrekt identifiziert hat und von der richtigen Basislinie ausgeht.

Abbildung 3: Historische Überprüfung der Korrelation zwischen der Registerkarte Kontohistorie und den Indikatorkurven

Abbildung 3. Ein Zurückblättern zum Datum der ersten Einzahlung zeigt eine perfekte Korrelation zwischen der Linie des Anfangssaldos des Indikators und der tatsächlichen Kontohistorie, was eine genaue historische Rekonstruktion bestätigt.


Wenn Sie durch Ihre Handelshistorie blättern, zeigen sich Muster, die typische Verhaltensweisen erkennen lassen. Längere Zeiträume, in denen sich Kontosaldo und Kontoeigenkapital gemeinsam bewegen, deuten darauf hin, dass es entweder keine offenen Positionen gibt oder die Positionen kostendeckend sind. Wenn das Kontoeigenkapital deutlich über dem Kontosaldo liegt, halten Sie profitable offene Positionen. Liegt das Kontoeigenkapital deutlich unter dem Kontosaldo, bestehen offene Verluste. Die Differenz dieser Werte im Verhältnis zu Ihrem Kontosaldo gibt Aufschluss darüber, wie viel Risiko derzeit eingesetzt wird.

Starke vertikale Bewegungen in der Kontosaldenkurve kennzeichnen geschlossene Trades. Wenn das Kontoeigenkapital über dem Kontosaldo lag und der Kontosaldo dann nach oben springt, um ihn zu erreichen, haben Sie einen Gewinn erzielt. Wenn das Kontoeigenkapital darunterliegt und der Kontosaldo fällt, haben Sie einen Verlust realisiert. Die variable Gewinn- und Verlustkurve quantifiziert explizit diese nicht realisierte Komponente. Sie oszilliert um Null, wenn Sie keine Positionen haben, und schwankt positiv oder negativ, wenn sich offene Positionen in oder aus dem Gewinn bewegen. Wenn Sie beobachten, wie lange der variable Gewinn anhält, bevor Sie Ihre Positionen schließen, können Sie feststellen, ob Sie dazu neigen, Gewinne schnell mitzunehmen oder laufen zu lassen.

Das Verhältnis zwischen diesen Kurven gibt auch Aufschluss über die Gewohnheiten im Risikomanagement. Wenn die variablen Verluste regelmäßig Ihr Startguthaben erreichen oder übersteigen, arbeiten Sie mit einer gefährlichen Hebelwirkung. Wenn das Kontoeigenkapital nie weit vom Kontosaldo abweicht, können Sie entweder Positionen schnell schließen oder enge Stop-Losses verwenden. Händler, die sich in Verlustpositionen befinden, werden sehen, wie das Kontoeigenkapital über mehrere Eröffnungen hinweg immer weiter unter das Kontosaldo abdriftet, um dann beim endgültigen Ausstieg wieder stark zuzulegen. Jedes Konto erzählt durch diese statistischen Signaturen eine andere Geschichte.



Schlussfolgerung

Der Indikator verwandelt den MetaTrader 5 in ein vollwertiges Kontoanalyse-Tool ohne externe Dienste. Es rekonstruiert die gesamte Handelshistorie und zeigt Kontosaldo, Kontoeigenkapital und laufenden Gewinn/Verlust als kontinuierliche Kurven an, die Muster erkennen lassen, die in Standardberichten nicht sichtbar sind. Eine effiziente Zwischenspeicherung sorgt für eine stabile Leistung auch bei langen Historien mit mehreren Symbolen.

Die Vier-Kurven-Visualisierung zeigt schnell das Risikoprofil und das Handelsverhalten: Tiefe des Drawdowns, Geschwindigkeit der Gewinnmitnahme und Konsistenz des Kapitalwachstums. Dies ermöglicht Entscheidungen, die auf dem tatsächlichen Handelsverhalten und nicht auf Annahmen basieren.

Der Indikator erfordert keine Einstellungen, verarbeitet automatisch alle Deals und bleibt über alle Symbole hinweg genau. Die Kurven werden während des Handels laufend aktualisiert und bilden eine klare statistische Aufzeichnung. Sie hilft dabei, Veränderungen in der Kontodynamik zu erkennen und Strategieverbesserungen von günstigen Marktbedingungen zu unterscheiden.

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

Beigefügte Dateien |
Letzte Kommentare | Zur Diskussion im Händlerforum (3)
muhammad nasir
muhammad nasir | 30 Jan. 2026 in 10:38
Behalten Sie die Entwicklung im Auge, seien Sie nicht unvorsichtig, denn der Markt ist ständig in Bewegung. Bestimmen Sie, wann Sie kaufen, wenn der Markt seinen Tiefpunkt erreicht hat.
Ahmad Katun
Ahmad Katun | 31 Jan. 2026 in 14:06
Hallo, ich möchte Zugang zu einem Trander-Konto in mql5 erhalten.

Michael Charles Schefe
Michael Charles Schefe | 2 Feb. 2026 in 05:23

Toller Artikel und Indikator. Es ist sehr nahe an einem Balance+Equity Tracking Indikator. Es gibt jedoch ein paar ernsthafte Probleme, so dass er nicht ganz korrekt ist, siehe Bild.

Die rote vertikale Linie ist die, an der ich den Indikator angebracht habe. Die Linien sind 3k oder mehr über, wo sie sein sollten. dh sehen die Linien der offenen Kerze ist richtig auf der rechten Seite. Die lila Linie war korrekt, bis ich die Einstellungen des Indikators geändert.

Die lila Linien sind, was ich mit Snipping-Tool hinzugefügt. die hellblaue Linie war richtig, bis ich die Einstellung auf den Indikator geändert, das Diagramm aktualisiert mit dem Bild unten. die Linien wurden falsch gemacht.



Die Übertragung der Trading-Signale in einem universalen Expert Advisor. Die Übertragung der Trading-Signale in einem universalen Expert Advisor.
In diesem Artikel wurden die verschiedenen Möglichkeiten beschrieben, um die Trading-Signale von einem Signalmodul des universalen EAs zum Steuermodul der Positionen und Orders zu übertragen. Es wurden die seriellen und parallelen Interfaces betrachtet.
Vom Einsteiger zum Experten: Entwicklung einer Liquiditätsstrategie Vom Einsteiger zum Experten: Entwicklung einer Liquiditätsstrategie
Liquiditätszonen werden üblicherweise gehandelt, indem man darauf wartet, dass der Kurs zurückkehrt und die Zone von Interesse erneut testet, oft durch die Platzierung von Pending Orders innerhalb dieser Bereiche. In diesem Artikel setzen wir MQL5 ein, um dieses Konzept praktisch umzusetzen. Wir zeigen, wie solche Zonen programmatisch identifiziert werden können und wie das Risikomanagement systematisch angewendet werden kann. Nehmen Sie an der Diskussion teil, in der wir sowohl die Logik hinter dem liquiditätsbasierten Handel als auch seine praktische Umsetzung untersuchen.
Eine alternative Log-datei mit der Verwendung der HTML und CSS Eine alternative Log-datei mit der Verwendung der HTML und CSS
In diesem Artikel werden wir eine sehr einfache, aber leistungsfähige Bibliothek zur Erstellung der HTML-Dateien schreiben, dabei lernen wir auch, wie man eine ihre Darstellung einstellen kann (nach seinem Geschmack) und sehen wir, wie man es leicht in seinem Expert Advisor oder Skript hinzufügen oder verwenden kann.
Python-MetaTrader 5 Strategie-Tester (Teil 01): Handelssimulator Python-MetaTrader 5 Strategie-Tester (Teil 01): Handelssimulator
Das MetaTrader-5-Modul für Python ermöglicht es, Trades bequem über Python in der MetaTrader-5-Anwendung zu eröffnen. Es hat jedoch einen großen Nachteil: Die im MetaTrader-5-Terminal verfügbare Funktion des Strategietesters fehlt. In dieser Artikelserie werden wir ein Framework für das Backtesting Ihrer Handelsstrategien in Python-Umgebungen aufbauen.