English Deutsch 日本語
preview
Отслеживание динамики счета: Визуализация баланса, средств счета и плавающей прибыли/убытка в MQL5

Отслеживание динамики счета: Визуализация баланса, средств счета и плавающей прибыли/убытка в MQL5

MetaTrader 5Индикаторы |
51 3
Nervada Emeule Adams
Nervada Emeule Adams

Введение

Большинство трейдеров уделяют огромное внимание анализу графиков, индикаторам и торговым стратегиям, но, как ни удивительно, лишь немногие внимательно следят за тем, как их реальный счет ведет себя с течением времени. MetaTrader 5 предоставляет вам доступ к данным о текущем балансе и средствах счета, но не позволяет проследить историю того, как эти показатели двигались вместе или расходились с момента, когда вы начали торговать. Вы можете видеть, где находитесь сейчас, но не путь, которым вы к этому пришли.

Это становится проблемой, когда вы хотите проанализировать эффективность своей торговли, выходя за рамки простых показателей доли прибыльных сделок и общей суммы прибыли. Анализ взаимосвязи между балансом, средствами счета и плавающей прибылью/убытком в динамике позволяет выявить закономерности в управлении рисками, поведении при удержании позиций и общем состоянии счета. Без этого визуального контекста вы, по сути, торгуете, не имея представления о статистической истории своего счета.

Обычный обходной путь — экспортировать данные в электронные таблицы или использовать сторонние аналитические платформы. Эти решения работают, но отрывают вас от торгового терминала и зачастую требуют обновлений вручную. А что, если бы вы могли видеть все это прямо в MetaTrader 5 — в виде аккуратных кривых, отрисованных в подокне и автоматически обновляющихся по мере вашей торговли?

В этой статье подробно описан процесс создания пользовательского индикатора, который как раз и выполняет эту задачу. Он восстанавливает всю вашу историю торговли и отрисовывает четыре ключевые кривые: начальный баланс как опорную линию, динамику баланса, динамику средств счета и плавающую прибыль/убыток. Все работает внутри MetaTrader 5 без каких-либо внешних зависимостей. Индикатор выполняет выборку данных один раз на бар, чтобы обеспечить плавную работу при отслеживании позиций по всем символам, которыми вы торгуете. В итоге у вас будет готовый аналитический инструмент, который покажет, как на самом деле ведет себя ваш счет в реальных торговых условиях.


Подход к визуализации

Стандартный мониторинг счета в MetaTrader 5 показывает лишь отдельные срезы, из-за чего трудно понять, как на самом деле были достигнуты результаты. Если рассматривать баланс, средства счета и плавающую прибыль/убыток как непрерывные временные ряды, эти показатели превращаются в наглядные визуальные истории, раскрывающие поведение при удержании позиций, просадки и динамику восстановления.

Выборка по барам обеспечивает оптимальный баланс между точностью и производительностью, согласуя кривые счета с графиками цен и сохраняя эффективность при работе с длинной историей. Отслеживание нескольких символов становится практичным благодаря кэшированию цен, что позволяет избежать серьезных проблем с производительностью.

Для точной реконструкции необходимо обработать полную историю сделок в хронологическом порядке, начиная с начального депозита и сохраняя состояние позиций во времени. Если все сделано правильно, в результате получаются четыре кривые, полностью описывающие историческое поведение счета.

Блок-схема процесса визуализации
Рисунок 1. Рабочий процесс визуализации динамики счета — от загрузки исторических данных до вывода итоговых кривых


Реализация на языке MQL5

Свойства индикатора и настройка буферов

Индикатор работает в отдельном подокне и строит четыре разные кривые. Ниже приведен полный раздел объявлений свойств и буферов из индикатора:

//+------------------------------------------------------------------+
//|                                   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[];

Начальный баланс отображается серой пунктирной линией, чтобы подчеркнуть, что это ориентир, а не динамические данные. Баланс и средства счета отображаются более толстыми сплошными линиями, поскольку это основные показатели, на которых сосредоточены трейдеры. По умолчанию значение параметра InpUpdatePeriod составляет 300 секунд (5 минут), что обеспечивает компромисс между оперативностью и вычислительной нагрузкой при проверке появления новых закрытых сделок.

Структуры данных для отслеживания истории

Индикатор должен сохранять историческое состояние между несколькими циклами расчета. Для эффективной организации этих данных мы используем три пользовательские структуры.

//--- 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;
};

Структура BalancePoint отслеживает, когда изменился баланс и каким стало его новое значение. Каждая закрытая сделка создает новую точку баланса. Структура PositionInfo хранит все данные, необходимые для расчета исторической плавающей прибыли/убытка по позиции. Обратите внимание, что здесь мы кэшируем значения tickSize и tickValue, а не запрашиваем их повторно во время вычислений. Это критически важная оптимизация производительности при работе с несколькими символами.

Структура PriceCache устраняет серьезное узкое место производительности. Без кэширования при обработке исторических баров нам пришлось бы снова и снова вызывать функцию CopyRates() для одного и того же символа и одного и того же времени. Благодаря сохранению полученных цен мы преобразуем операцию сложности O(n²) в операцию, сложность которой ближе к O(n).

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

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

Эти глобальные массивы сохраняются между вызовами OnCalculate(), поддерживая состояние на протяжении всего времени жизни индикатора. Переменная totalDealsProcessed позволяет определять, когда появились новые закрытые сделки, без лишнего перестроения истории. Переменная earliestDealTime определяет, где на графике должны начинаться наши кривые, не позволяя выполнять расчеты до появления какой-либо торговой активности.

Процесс инициализации (OnInit)

Функция OnInit() выполняется один раз при загрузке индикатора. Именно здесь мы подключаем наши массивы буферов к системе индикатора и формируем полную историю счета.

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);
}

Функция SetIndexBuffer() связывает наши массивы с системой отрисовки индикатора. Второй параметр, INDICATOR_DATA, сообщает MQL5, что эти буферы содержат значения, которые нужно отрисовывать, в отличие от INDICATOR_CALCULATIONS, где хранились бы промежуточные значения, не предназначенные для отображения. Индексы буферов должны соответствовать порядку, в котором мы объявили их в разделе свойств.

Вызовы функции ArraySetAsSeries() крайне важны. По умолчанию массивы MQL5 индексируются от самых старых к самым новым, однако данные графика естественным образом располагаются от самых новых к самым старым. При задании массивов в виде временных рядов порядок индексации меняется на обратный, так что индекс 0 всегда соответствует самому последнему бару, что соответствует принципу работы массивов временных рядов в функции OnCalculate(). Без этого наши данные отрисовывались бы в обратном порядке.

Мы инициализируем все буферы значением EMPTY_VALUE, что дает терминалу указание не отрисовывать ничего в этих точках. Это позволяет избежать появления лишних линий в тех областях, где у нас нет данных. Функция BuildAccountHistory() выполняет основную работу по воссозданию вашей торговой истории; далее мы рассмотрим ее подробнее.

Восстановление исторических данных (BuildAccountHistory)

Эта функция восстанавливает полную торговую историю, обрабатывая каждую сделку, выполненную на вашем счете. Это самая сложная часть индикатора, но в то же время и самая важная для точной визуализации.

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...");

Функция HistorySelect() загружает историю сделок в память. Передавая fromDate = 0, мы запрашиваем всю историю, начиная с первой транзакции по счету. Это не то же самое, что история торговых операций. Сделки включают пополнения, выводы средств, балансовые операции, а также сделки входа и выхода. Функция возвращает значение false, если сервер истории недоступен или возникла проблема с подключением.

После загрузки функция HistoryDealsTotal() сообщает, сколько существует записей о сделках. Если это значение равно нулю, то, скорее всего, речь идет о новом счете, на котором отсутствует торговая активность. В этом случае мы создаем единственную точку баланса на уровне текущего баланса и выходим из функции. Для счетов, на которых уже была активность, нам необходимо определить начальный депозит и самую раннюю временную метку торговой операции.

//--- 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);
   }

Функция HistoryDealGetTicket() последовательно извлекает уникальный идентификатор каждой сделки. Затем мы используем функции HistoryDealGetInteger() и HistoryDealGetDouble() для извлечения определенных свойств сделки. DEAL_TYPE_BALANCE обозначает пополнения или выводы средств, а DEAL_TYPE_CREDIT — бонусные начисления. Первое появление любого из этих типов считается нашим начальным депозитом и задает отправную точку для всех последующих расчетов.

Некоторые счета могут не иметь явных сделок пополнения, если они были созданы со стартовым капиталом. В таком случае мы используем текущий баланс в качестве опорной точки. Это не идеальный вариант, но он дает приемлемое приближение для визуализации.

//--- 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);

Теперь мы снова просматриваем все сделки, на этот раз уделяя особое внимание торговой активности. Свойство DEAL_ENTRY указывает, открыла ли сделка позицию (DEAL_ENTRY_IN), закрыла ее (DEAL_ENTRY_OUT) или развернула (DEAL_ENTRY_INOUT). Это различие имеет решающее значение, поскольку на баланс влияют только закрывающие сделки, однако для расчета исторической плавающей прибыли/убытка нам необходимо отслеживать открывающие сделки.

//--- 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);
      }

Когда открывается позиция, мы сохраняем все ее данные в массиве positionCache. Значение closeTime устанавливается равным нулю, что указывает на то, что в рамках исторической реконструкции позиция считается открытой. Мы сразу же извлекаем и кэшируем параметры тика символа с помощью функции SymbolInfoDouble(). Эти значения определяют, как движения цены преобразуются в прибыль и убыток. Кэширование этих данных здесь означает, что нам больше не придется запрашивать их повторно при расчете плавающей прибыли/убытка, что позволяет избежать тысяч лишних обращений.

//--- 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;
}

Когда позиция закрывается, мы находим ее запись в кэше и помечаем ее как закрытую, устанавливая значение closeTime. Это важно, поскольку нашему калькулятору плавающей прибыли/убытка необходимо знать, была ли позиция открыта в каждый конкретный исторический момент. Баланс изменяется только при закрытии сделок, поэтому мы рассчитываем чистый результат с учетом прибыли, свопа и комиссии, а затем добавляем новую точку баланса в нашу историю. Вспомогательная функция AddBalancePoint() просто добавляет эти данные в массив balanceHistory.

Основной цикл вычислений (OnCalculate)

Функция OnCalculate() выполняется каждый раз при поступлении новых ценовых данных или при прокрутке графика пользователем. Именно здесь мы заполняем буферы индикатора вычисленными значениями для каждого видимого бара.

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;
   }

Параметр rates_total указывает, сколько баров имеется на графике, а prev_calculated — сколько из них уже обработано. При первом вызове значение prev_calculated равно нулю, что означает, что нам нужно вычислить все. При последующих вызовах нам, как правило, нужно обновлять только последний бар. Массивы временных рядов, передаваемые в качестве параметров, автоматически не индексируются как серии, поэтому мы вызываем ArraySetAsSeries(time, true), чтобы согласовать их с индексацией нашего буфера.

Периодическая проверка истории — это продуманная оптимизация. Вместо того чтобы перестраивать данные на каждом тике, мы проверяем с настраиваемой периодичностью, появились ли новые сделки. Сравнивая значение функции HistoryDealsTotal() с нашим сохраненным значением totalDealsProcessed, мы сразу же узнаем, произошли ли какие-либо изменения. И только тогда мы выполняем затратное перестроение полной истории.

//--- 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);

Переменная limit определяет, сколько баров необходимо обработать. Когда prev_calculated равен нулю, мы обрабатываем все бары. В противном случае мы обрабатываем только новые бары, появившиеся с момента последнего вызова. Если пользователь указал максимальную длину истории через параметр InpHistoryBars, мы соблюдаем это ограничение. Функция AccountInfoDouble() получает текущие значения счета с помощью предопределенных констант ACCOUNT_BALANCE и ACCOUNT_EQUITY.

//--- 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;
}

Для баров до вашей первой сделки мы задаем всем буферам значение EMPTY_VALUE, чтобы ничего не отрисовывалось. Линия начального баланса на каждом баре получает постоянное значение initialDeposit, создавая горизонтальную опорную линию. Индекс 0 всегда обозначает текущий бар, на котором мы напрямую используем актуальные данные счета. Для исторических баров мы вызываем вспомогательные функции, чтобы получить баланс на конкретный момент времени и рассчитать размер плавающей прибыли/убытка. Средства счета — это просто баланс плюс плавающая прибыль/убыток в любой момент времени.

Возврат rates_total сообщает терминалу, что мы успешно обработали все доступные бары. При следующем вызове это значение становится prev_calculated, что позволяет выполнять инкрементные обновления.

Расчет плавающей прибыли/убытка

Именно в расчете плавающей прибыли/убытка сосредоточена сложность работы с несколькими символами. Нам нужны вспомогательные функции для получения исторического баланса, расчета стоимости позиций и эффективного кэширования цен.

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;
}

Эта функция проходит по нашему массиву balanceHistory, чтобы определить, каким был баланс в любой заданный момент времени. Баланс изменяется только при закрытии сделок, поэтому мы ищем последнюю точку баланса, которая была до целевого момента времени или совпадала с ним. Если целевое время предшествует первому зарегистрированному изменению баланса, мы возвращаем начальный депозит. В результате кривая баланса приобретает ступенчатый вид, при этом между закрытыми сделками она остается ровной.

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;
}

Это вычислительное ядро индикатора. Для каждого исторического бара мы перебираем все отслеживаемые позиции и определяем, была ли каждая из них открыта в тот момент. Позиция считается открытой, если ее openTime находится до времени бара или совпадает с ним и при этом она либо никогда не закрывалась, либо ее closeTime позже времени бара. Для каждой открытой позиции мы получаем цену на этом баре и рассчитываем прибыль или убыток.

При расчете прибыли используется стандартная формула: разница цен, деленная на размер тика, дает количество пройденных тиков; умножение на стоимость тика дает прибыль на один лот, а умножение на объем — итоговую прибыль или убыток по позиции. Для позиций на покупку прибыль равна разнице между текущей ценой и ценой открытия. Для позиций на продажу формула обратная. Мы суммируем результаты по всем открытым позициям, чтобы получить совокупную плавающую прибыль/убыток на этот момент.

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;
}

Именно эта оптимизация производительности делает возможным отслеживание нескольких символов. Прежде чем запрашивать какую-либо цену, мы проверяем, не получали ли мы ее уже ранее. Если цена найдена в кэше, мы сразу же ее возвращаем. Если цена не найдена, мы получаем ее с помощью функций iBarShift() и iClose() для символа текущего графика либо с помощью функции CopyRates() для других символов. Функция iBarShift() определяет, какой индекс бара соответствует заданному моменту времени, а функция CopyRates() извлекает исторические данные по барам для любого символа.

После получения мы добавляем цену в кэш. Максимальный размер кэша составляет 1000 записей. При превышении этого числа мы удаляем 500 самых старых записей с помощью ArrayRemove(). Это предотвращает рост потребления памяти, сохраняя при этом достаточную глубину кэша для типичных вычислительных задач. Без этого кэширования индикатор выполнял бы тысячи лишних вызовов CopyRates(), что привело бы к сильным задержкам.

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

Функция деинициализации выполняет очистку при удалении или перекомпиляции индикатора. Мы очищаем область комментариев и освобождаем все динамические массивы. Несмотря на то что MQL5 автоматически управляет памятью, явное освобождение больших массивов является рекомендуемой практикой, особенно для индикаторов, которые во время разработки могут часто перезагружаться.



Примеры в работе

Загрузка и начальное отображение

Когда вы добавляете индикатор на любой график, он сразу начинает восстанавливать историю вашего счета и отрисовывать четыре кривые в отдельном подокне. Процесс выполняется автоматически и не требует дополнительной настройки, за исключением необязательных входных параметров. Индикатор отображает всю историю вашей торговли с момента начального депозита, независимо от того, какой символ или таймфрейм вы просматриваете.

Для визуального разграничения каждой метрики используется цветовое кодирование. Серая пунктирная линия обозначает ваш начальный баланс и остается неизменной на всех барах, служа базовым ориентиром. Оранжевая линия отслеживает ваш реализованный баланс, ступенчато повышаясь или снижаясь только при закрытии сделок. Синяя линия показывает средства счета, то есть баланс с учетом нереализованной прибыли или убытка по открытым позициям. Зеленая линия отдельно отрисовывает компонент плавающей прибыли/убытка, позволяя легко увидеть, какая часть ваших средств счета подвержена риску в открытых сделках, а какая надежно зафиксирована в балансе.

Рисунок 2. Отображение индикатора со всеми четырьмя кривыми в подокне и четким цветовым кодированием

Рисунок 2. Индикатор работает в отдельном подокне и одновременно отображает кривые начального баланса (серая пунктирная), баланса (оранжевая), средств счета (синяя) и плавающей прибыли/убытка (зеленая)


Подокно автоматически масштабируется так, чтобы вместить все кривые, однако в периоды значительной просадки или выбора слишком крупного размера позиций колебания плавающей прибыли/убытка могут занимать большую часть визуальной области. Если трейдерам требуется большее разрешение по вертикали, чтобы различать близко расположенные кривые, они могут изменить высоту подокна, перетащив его границу.

Проверка по истории и анализ закономерностей

Точность индикатора можно проверить, сопоставив его данные с данными на вкладке «История счета» в MetaTrader. Определив метку времени вашего первого депозита в истории счета и прокрутив график назад до этой даты, вы должны увидеть, что линия начального баланса в этой точке пересекается как с кривой баланса, так и с кривой средств счета. Такое схождение подтверждает, что индикатор правильно определил момент создания вашего счета и начал отслеживание от корректного базового уровня.

Рисунок 3: Историческая проверка, демонстрирующая корреляцию между данными на вкладке «История счета» и кривыми индикатора

Рисунок 3. При прокрутке назад до даты начального депозита видно идеальное соответствие между линией начального баланса индикатора и фактической историей счета, что подтверждает точное восстановление истории


По мере прокрутки вашей торговой истории вперед проявляются закономерности, отражающие особенности вашего поведения в торговле. Длительные периоды, когда баланс и средства счета движутся синхронно, указывают либо на отсутствие открытых позиций, либо на позиции, находящиеся в безубытке. Когда средства счета значительно превышают баланс, это означает, что у вас есть прибыльные открытые позиции. Расхождение ниже баланса означает наличие убытков по открытым позициям. Размер этого расхождения относительно вашего баланса показывает, какой объем риска сейчас задействован.

Резкие вертикальные скачки на кривой баланса отмечают закрытие сделок. Если средства счета были выше баланса, а затем баланс резко подскочил к их уровню, значит, вы зафиксировали прибыль. Если средства счета были ниже баланса, а затем баланс снизился до их уровня, вы реализовали убыток. Кривая плавающей прибыли/убытка наглядно количественно показывает этот нереализованный компонент: она колеблется вокруг нуля, когда у вас нет открытых позиций, и уходит в положительную или отрицательную область по мере того, как открытые сделки становятся прибыльными или убыточными. Отслеживание того, как долго сохраняется плавающая прибыль до закрытия позиций, может показать, склонны ли вы быстро фиксировать прибыль или предпочитаете давать прибыльным позициям расти дальше.

Взаимосвязь между этими кривыми также раскрывает особенности подхода к управлению рисками. Если плавающие убытки регулярно приближаются к вашему начальному балансу или превышают его, это означает, что вы работаете с опасным уровнем кредитного плеча. Если средства счета никогда далеко не отклоняются от баланса, значит, вы либо быстро закрываете позиции, либо используете жесткие стоп-лоссы. Трейдеры, которые усредняют убыточные позиции, видят, как средства счета с каждым новым входом все дальше опускаются ниже баланса, а затем резко возвращаются к нему, когда они наконец выходят из позиции. Каждый счет рассказывает свою историю через эти статистические характеристики.



Заключение

Этот индикатор превращает MetaTrader 5 в полноценный инструмент для анализа счета без использования сторонних сервисов. Он восстанавливает всю торговую историю и отображает баланс, средства счета и плавающую прибыль/убыток в виде непрерывных кривых, позволяя выявить закономерности, которые не видны в стандартных отчетах. Эффективное кэширование обеспечивает стабильную производительность даже при обширной истории по нескольким символам.

Визуализация с четырьмя кривыми позволяет быстро оценить профиль риска и торговое поведение: глубину просадки, скорость фиксации прибыли и стабильность роста средств счета. Это позволяет принимать решения на основе фактического торгового поведения, а не предположений.

Индикатор не требует настройки, автоматически обрабатывает все сделки и остается точным при работе с разными символами. Кривые обновляются по мере совершения вами сделок, формируя наглядную статистическую картину. Это помогает выявлять изменения в динамике счета и отличать улучшение стратегии от благоприятных рыночных условий.

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/20669

Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (3)
muhammad nasir
muhammad nasir | 30 янв. 2026 в 10:38
Продолжайте следить за ситуацией, не теряйте бдительности — рынок постоянно меняется. Определите, когда следует покупать, когда рынок достигнет своего минимума.
Ahmad Katun
Ahmad Katun | 31 янв. 2026 в 14:06
Здравствуйте, я хочу получить доступ к торговому счету в MQL5

Michael Charles Schefe
Michael Charles Schefe | 2 февр. 2026 в 05:23

Отличная статья и индикатор. Он очень близок к индикатору отслеживания баланса и собственного капитала. Однако есть несколько серьезных проблем, поэтому он не совсем точен. См. изображение.

Красная вертикальная линия обозначает момент, когда я подключил индикатор. Линии находятся на 3 тыс. или более выше того уровня, на котором они должны быть. То есть, как видно, линии открытия свечи справа отображаются правильно. Фиолетовая линия была правильной, пока я не изменил настройки индикатора.

Фиолетовые линии — это то, что я добавил с помощью инструмента «Ножницы». Голубая линия была правильной, пока я не изменил настройки индикатора; после этого график обновился, как показано на изображении ниже, и линии стали отображаться некорректно.



Особенности написания Пользовательских Индикаторов Особенности написания Пользовательских Индикаторов
Написание пользовательских индикаторов в торговой системе MetaTrader 4
Создание и использование Скиллов для создания артефактов MQL. Часть 2 Создание и использование Скиллов для создания артефактов MQL. Часть 2
Развиваем скилл MQL-TUTOR для генерации артефактов MQL5: добавляем логирование, файл проекта, единый стиль и аккуратный режим Edit. Подключаем опциональный поиск документации через MCP, вводим проверку «здоровья» скилла и поддержку служебных скриптов. В итоге получаем воспроизводимый процесс создания и редактирования скриптов с контролем ошибок, прозрачной историей изменений и меньшими трудозатратами.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Нейросети в трейдинге: Адаптация прогноза при смене рыночного режима (Окончание) Нейросети в трейдинге: Адаптация прогноза при смене рыночного режима (Окончание)
Завершаем адаптацию фреймворка OMPB к торговой модели средствами MQL5. Новый слой встраивается в энкодер состояния рынка между RankTCM и ScenarioForecast. Мы рассматриваем двухэтапное обучение Forecast, разделение истории на Reference и Calibration, фиксацию базовой модели и последующую калибровку OMPB. В практической части показано переключение режимов слоя, контроль неизменности замороженных компонентов и итоговое тестирование системы на данных 2026 года.