Панель мониторинга счёта на графике в MQL4

Панель мониторинга счёта на графике в MQL4

10 июня 2026, 10:49
Антон Влупидол
1
8

Введение

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

В этой статье мы построим небольшую информационную панель (только для чтения), которая рисует состояние счёта прямо на графике и обновляется раз в секунду. К концу у тебя будет готовый настраиваемый инструмент и — что важнее — ясное понимание, как читать состояние счёта в MQL4 и как рисовать аккуратный интерфейс графическими объектами.

Вот что показывает панель:

  • Баланс, Средства, Свободная маржа
  • Уровень маржи (%)
  • Плавающий P/L всех открытых позиций
  • Реализованный P/L за сегодня, неделю и месяц — в деньгах и процентах
  • Внутридневная просадка от пика средств за день
  • Количество открытых позиций и суммарный объём

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

Предполагается, что ты знаком с базовым синтаксисом MQL4 (функции, циклы, обработчики OnInit / OnTick ). Торговой логики здесь нет.

Что будем показывать

Прежде чем писать код, полезно назвать метрики и понять, откуда каждая берётся.

Метрика Источник
Баланс AccountBalance()
Средства AccountEquity()
Свободная маржа AccountFreeMargin()
Уровень маржи расчёт: средства / задейств. маржа × 100
Плавающий P/L сумма по открытым ордерам (profit + swap + commission)
Реализованный P/L (день/нед./мес.) сумма по закрытым ордерам периода
Внутридневная просадка (пик средств − средства) / пик средств × 100
Позиции / объём счётчик и сумма лотов по пулу открытых ордеров

Выделяются две группы: мгновенные значения, которые терминал отдаёт напрямую (баланс, средства, маржа), и производные, требующие перебора ордеров или хранения состояния во времени (реализованный P/L, просадка). Самый интересный код — во второй группе.

1. Чтение состояния счёта

В MQL4 базовые показатели доступны через семейство функций Account*() . Они всегда описывают счёт, к которому подключён терминал, поэтому параметры не нужны:

double bal = AccountBalance(); // баланс без открытых позиций double eq = AccountEquity(); // средства = баланс + плавающий P/L double fm = AccountFreeMargin(); // свободные = средства - задейств. маржа double mrg = AccountMargin(); // маржа, заблокированная позициями

Стоит быть точным в значениях — трейдеры часто их путают:

  • Баланс — это счёт без открытых сделок, итог всего уже закрытого.
  • Средства — баланс плюс плавающий результат того, что открыто сейчас. Когда позиций нет, средства равны балансу.
  • Свободная маржа — часть средств, не заблокированная под залог, грубо говоря, то, чем ещё можно открывать новые сделки.

Готовой функции для уровня маржи в MT4 нет, но это просто отношение средств к задействованной марже, в процентах:

double ml = (mrg > 0.0) ? eq / mrg * 100.0 : 0.0; 

Защита важна: при отсутствии позиций задействованная маржа равна 0 , и деление на неё было бы ошибкой. В этом случае мы покажем прочерк вместо числа.

Для отображения нам также пригодится валюта депозита, которую AccountCurrency() возвращает строкой (“USD”, “EUR”, …). Дописав её, мы превращаем голое 1234.56 в гораздо более читаемое 1234.56 USD .

2. Открытые позиции: плавающий P/L и объём

Чтобы свести то, что открыто сейчас, проходим по пулу ордеров. OrdersTotal() даёт его размер, а OrderSelect(index, SELECT_BY_POS, MODE_TRADES) делает каждый ордер «текущим», чтобы аксессоры Order*() возвращали его данные.

Нам нужны только рыночные ордера — OP_BUY (0) и OP_SELL (1), — и надо пропустить отложенные, чьи константы типа ( OP_BUYLIMIT , OP_SELLLIMIT , OP_BUYSTOP , OP_SELLSTOP ) все больше OP_SELL :

double floatPL = 0.0, lots = 0.0; int cnt = 0; for(int i = 0; i < OrdersTotal(); i++) { if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue; if(OrderType() > OP_SELL) continue; // пропускаем отложенные floatPL += OrderProfit() + OrderSwap() + OrderCommission(); lots += OrderLots(); cnt++; }

Частая ошибка — читать только OrderProfit() . Реальный плавающий результат позиции включает ещё своп (ночное финансирование) и комиссию. Сумма всех трёх даёт честную цифру, которую ты действительно получишь, если закроешь всё в этот момент, — а именно она и интересует трейдера.

3. Реализованный P/L по периодам

На вопрос «как прошёл сегодня / эта неделя / месяц» отвечают закрытые сделки из истории. Читаем их через OrdersHistoryTotal() и MODE_HISTORY .

Тонкость — в фильтрации. В истории лежат не только закрытые сделки, но и записи удалённых отложенных ордеров, и балансовые операции: у пополнений и выводов тип ордера равен 6 , что выше OP_SELL . Если наивно сложить всё, пополнение засчитается как прибыль. Поэтому оставляем только закрытые рыночные сделки и суммируем profit, swap и commission для тех, что закрыты после заданной отметки времени:

double RealizedPnL(datetime from)
  {
   double sum = 0.0;
   for(int i = OrdersHistoryTotal() - 1; i >= 0; i--)
     {
      if(!OrderSelect(i, SELECT_BY_POS, MODE_HISTORY)) continue;
      if(OrderType() > OP_SELL) continue;        // пропускаем отложки и балансовые
      if(OrderCloseTime() >= from)
         sum += OrderProfit() + OrderSwap() + OrderCommission();
     }
   return(sum);
  } 

Границы периодов строим от серверного времени, которое возвращает TimeCurrent() . Начало «сегодня» — текущее время, округлённое вниз до полуночи; неделя и месяц используют скользящие окна 7 и 30 дней:

datetime DayStart(datetime t) { return( t - (t % 86400) ); } datetime now = TimeCurrent(); double pDay = RealizedPnL(DayStart(now)); double pWk = RealizedPnL(now - 7 * 86400); double pMo = RealizedPnL(now - 30 * 86400);

Два практических замечания:

  • Глубина истории. Эти цифры настолько полны, насколько полна история, загруженная в терминал. Если во вкладке История счёта стоит «Последние 3 дня», старые сделки отсутствуют в памяти, и месячная цифра будет неверной. Пользователю нужно переключить вкладку на «Всю историю». Это стоит упомянуть в описании продукта.
  • Серверное против локального времени. TimeCurrent() — это время сервера. Поэтому границы дня следуют дню брокера, а не твоим настенным часам. Для панели это обычно как раз то, что нужно (совпадает с дневным роллбэком брокера), но это осознанный выбор, о котором полезно знать.

Чтобы перевести деньги в проценты, считаем результат периода относительно баланса на начало периода — текущий баланс минус то, что заработано за период:

string PctStr(double pnl, double balNow)
  {
   double startBal = balNow - pnl;
   double pct = (startBal > 0.0) ? pnl / startBal * 100.0 : 0.0;
   return( (pnl>0?"+":"") + DoubleToString(pnl,2)
         + " (" + (pct>0?"+":"") + DoubleToString(pct,2) + "%)" );
  } 

Это приближение (оно игнорирует внутрипериодные пополнения), но для панели «бросил взгляд» оно и интуитивно, и достаточно точно.

4. Внутридневная просадка

Просадка имеет смысл только если помнить максимум. Храним наибольшие средства, достигнутые с начала торгового дня, в глобальных переменных, обновляем при новом максимуме и сбрасываем при смене дня:

double gPeakEq = 0.0; datetime gPeakDay = 0; datetime dStart = DayStart(TimeCurrent()); if(dStart != gPeakDay) { gPeakDay = dStart; gPeakEq = AccountEquity(); } if(AccountEquity() > gPeakEq) gPeakEq = AccountEquity(); double ddPct = (gPeakEq > 0.0) ? (gPeakEq - AccountEquity()) / gPeakEq * 100.0 : 0.0;

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

Если нужна не дневная, а абсолютная или скользящая просадка, схема та же; меняется лишь то, когда (и сбрасывается ли вообще) gPeakEq .

5. Построение разметки панели

Интерфейс состоит из объектов двух типов: один OBJ_RECTANGLE_LABEL для фоновой карточки и набор OBJ_LABEL для строк текста. Оба привязаны к углу графика, чтобы оставаться на месте при прокрутке цены.

Хелпер создаёт метку при первом вызове и далее лишь переставляет её, что делает код разметки компактным:

#define PFX "fmAP_"

void MakeLabel(string name, int x, int y, int fs, color clr)
  {
   string n = PFX + name;
   if(ObjectFind(0, n) < 0)
     {
      ObjectCreate(0, n, OBJ_LABEL, 0, 0, 0);
      ObjectSetInteger(0, n, OBJPROP_CORNER, InpCorner);
      ObjectSetInteger(0, n, OBJPROP_ANCHOR, ANCHOR_LEFT_UPPER);
      ObjectSetInteger(0, n, OBJPROP_SELECTABLE, false);
      ObjectSetInteger(0, n, OBJPROP_HIDDEN, true);
     }
   ObjectSetString (0, n, OBJPROP_FONT, InpFont);
   ObjectSetInteger(0, n, OBJPROP_FONTSIZE, fs);
   ObjectSetInteger(0, n, OBJPROP_COLOR, clr);
   ObjectSetInteger(0, n, OBJPROP_XDISTANCE, x);
   ObjectSetInteger(0, n, OBJPROP_YDISTANCE, y);
  } 

Два свойства объекта стоит выделить. OBJPROP_SELECTABLE = false не даёт пользователю случайно перетащить метку, а OBJPROP_HIDDEN = true убирает эти служебные объекты из окна Список объектов, чтобы не засорять его.

Фоновая карточка — это OBJ_RECTANGLE_LABEL . Её полезные свойства:

Свойство Назначение
OBJPROP_XSIZE / OBJPROP_YSIZE ширина и высота карточки в пикселях
OBJPROP_BGCOLOR цвет заливки
OBJPROP_BORDER_TYPE BORDER_FLAT для чистого прямоугольника
OBJPROP_COLOR цвет рамки (при BORDER_FLAT )
OBJPROP_CORNER от какого угла отсчитываются X/Y

Вот процедура разметки. Она рисует карточку, затем расставляет пары «подпись/значение» построчно. Подписи идут в левую колонку, значения — в правую (больший отступ X), а у пары строк добавлен интервал, чтобы визуально сгруппировать баланс, P/L и риск:

void BuildPanel() { int w = 232; int rows = InpShowBranding ? 15 : 14; int h = 14 + rows * gRowStep; string bg = PFX + "bg"; if(ObjectFind(0, bg) < 0) ObjectCreate(0, bg, OBJ_RECTANGLE_LABEL, 0, 0, 0); ObjectSetInteger(0, bg, OBJPROP_CORNER, InpCorner); ObjectSetInteger(0, bg, OBJPROP_XDISTANCE, InpX - 8); ObjectSetInteger(0, bg, OBJPROP_YDISTANCE, InpY - 8); ObjectSetInteger(0, bg, OBJPROP_XSIZE, w); ObjectSetInteger(0, bg, OBJPROP_YSIZE, h); ObjectSetInteger(0, bg, OBJPROP_BGCOLOR, InpColBg); ObjectSetInteger(0, bg, OBJPROP_BORDER_TYPE, BORDER_FLAT); ObjectSetInteger(0, bg, OBJPROP_COLOR, InpColBorder); ObjectSetInteger(0, bg, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, bg, OBJPROP_HIDDEN, true); int lx = InpX; // колонка подписей int vx = InpX + 96; // колонка значений int y = InpY; int fs = InpFontSize; MakeLabel("title", lx, y, fs + 1, InpColTitle); y += gRowStep + 4; MakeLabel("lBal", lx, y, fs, InpColText); MakeLabel("vBal", vx, y, fs, InpColText); y += gRowStep; MakeLabel("lEq", lx, y, fs, InpColText); MakeLabel("vEq", vx, y, fs, InpColText); y += gRowStep; // ... остальные строки по той же схеме (см. полный исходник) SetText("title", "Account Panel", InpColTitle); SetText("lBal", "Balance", InpColText); SetText("lEq", "Equity", InpColText); // ... остальные подписи }

Замечание про углы. Чтобы расчёт координат был читаемым, эта версия рассчитана на верхний-левый угол. Поддержка всех четырёх углов означает смену якоря ( ANCHOR_RIGHT_UPPER для правых углов) и разворот направления, в котором смещается колонка значений, потому что для правых и нижних углов X и Y отсчитываются от противоположного края. Это полезное упражнение и естественная фича для версии 2.

6. Обновление значений

BuildPanel() выполняется один раз и создаёт всё. Второй хелпер переписывает текст и цвет метки, не пересоздавая её:

void SetText(string name, string text, color clr)
  {
   string n = PFX + name;
   ObjectSetString (0, n, OBJPROP_TEXT,  text);
   ObjectSetInteger(0, n, OBJPROP_COLOR, clr);
  } 

UpdatePanel() собирает всё из предыдущих разделов и выводит, окрашивая денежные результаты зелёным для плюса и красным для минуса:

void UpdatePanel() { datetime now = TimeCurrent(); datetime dStart = DayStart(now); if(dStart != gPeakDay) { gPeakDay = dStart; gPeakEq = AccountEquity(); } if(AccountEquity() > gPeakEq) gPeakEq = AccountEquity(); double bal = AccountBalance(); double eq = AccountEquity(); double fm = AccountFreeMargin(); double mrg = AccountMargin(); double ml = (mrg > 0.0) ? eq / mrg * 100.0 : 0.0; double floatPL = 0.0, lots = 0.0; int cnt = 0; for(int i = 0; i < OrdersTotal(); i++) { if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue; if(OrderType() > OP_SELL) continue; floatPL += OrderProfit() + OrderSwap() + OrderCommission(); lots += OrderLots(); cnt++; } double pDay = RealizedPnL(dStart); double pWk = RealizedPnL(now - 7 * 86400); double pMo = RealizedPnL(now - 30 * 86400); double ddPct = (gPeakEq > 0.0) ? (gPeakEq - eq) / gPeakEq * 100.0 : 0.0; SetText("vBal", Money(bal), InpColText); SetText("vEq", Money(eq), InpColText); SetText("vFm", Money(fm), InpColText); SetText("vMl", (mrg > 0.0) ? DoubleToString(ml, 0) + "%" : "—", InpColText); SetText("vFl", Signed(floatPL), ValColor(floatPL)); SetText("vDay", PctStr(pDay, bal), ValColor(pDay)); SetText("vWk", PctStr(pWk, bal), ValColor(pWk)); SetText("vMo", PctStr(pMo, bal), ValColor(pMo)); SetText("vDd", (ddPct > 0.005 ? "-" : "") + DoubleToString(ddPct, 2) + "%", ddPct > 0.005 ? InpColNeg : InpColText); SetText("vPos", IntegerToString(cnt) + " (" + DoubleToString(lots, 2) + ")", InpColText); ChartRedraw(); }

Небольшие хелперы форматирования держат код чистым:

string Money(double v)  { return( DoubleToString(v, 2) + " " + AccountCurrency() ); }
string Signed(double v) { return( (v>0?"+":"") + DoubleToString(v, 2) ); }
color  ValColor(double v){ return( v>0 ? InpColPos : (v<0 ? InpColNeg : InpColText) ); } 

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

7. Жизненный цикл: держим панель «живой»

Значения счёта меняются на каждом тике, но график может молчать минутами — а нам всё равно нужно обновлять панель, потому что метрики по времени продолжают идти (наступает новый день, скользящая неделя сдвигается). Поэтому запускаем обновление и по секундному таймеру, и в обработчике тиков:

int OnInit() { gPeakEq = AccountEquity(); gPeakDay = DayStart(TimeCurrent()); BuildPanel(); // создаём статичную разметку один раз UpdatePanel(); // первое заполнение EventSetTimer(1); // обновление раз в секунду return(INIT_SUCCEEDED); } void OnTimer() { UpdatePanel(); } void OnTick() { UpdatePanel(); } void OnDeinit(const int reason) { EventKillTimer(); ObjectsDeleteAll(0, PFX); // удаляем все объекты с нашим префиксом ChartRedraw(); }

Префикс в имени каждого объекта ( fmAP_ ) окупается здесь: ObjectsDeleteAll(0, PFX) удаляет всю панель одним вызовом при снятии советника или закрытии графика, не оставляя объектов-сирот.

8. Настройки

Всё, что касается внешнего вида, вынесено во входные параметры, чтобы панель подходила под любую тему графика:

Параметр Значение
InpCorner 0 = верх-лево, 1 = верх-право, 2 = низ-лево, 3 = низ-право
InpX , InpY отступ от угла, в пикселях
InpFontSize размер шрифта
InpFont имя шрифта (моноширинный ровно выравнивает цифры)
InpColText , InpColTitle цвета текста и заголовка
InpColPos , InpColNeg цвета положительных / отрицательных значений
InpColBg , InpColBorder фон и рамка панели
InpShowBranding показывать строку-футер

Рекомендовать моноширинный шрифт (Consolas) — не косметика: при пропорциональных шрифтах цифры смещаются влево-вправо при смене разрядов, и панель «дёргается». Моноширинные цифры стоят идеально ровно.

9. Как расширить панель

Когда скелет работает, полезные дополнения делаются легко. Несколько идей:

Окраска уровня маржи по риску. Вместо обычного текста — красить уровень маржи в красный, когда он падает ниже порога: ранний сигнал перегруженного счёта:

color mlColor = InpColText;
if(mrg > 0.0)
  {
   if(ml < 200.0)      mlColor = InpColNeg;     // опасно
   else if(ml < 500.0) mlColor = clrOrange;     // внимание
  }
SetText("vMl", (mrg > 0.0) ? DoubleToString(ml,0)+"%" : "—", mlColor); 

Разбивка по инструментам. Накапливай плавающий P/L по символам в небольшой массив во время прохода по пулу ордеров, затем выводи дополнительные строки — удобно, когда несколько советников торгуют разные инструменты на одном счёте.

Алерт по дневному убытку. Сравнивай pDay с порогом пользователя и вызывай Alert() один раз при пробитии. (Держи флаг, чтобы срабатывало только раз в день.)

Имя/номер счёта. AccountName() , AccountNumber() и AccountServer() дают хороший заголовок, когда панель работает на нескольких терминалах.

10. Портирование на MQL5

Та же панель работает в MQL5; код отрисовки идентичен (графические объекты ведут себя так же). Меняется только слой данных, потому что MT5 использует неттинговую/позиционную модель и историю на основе сделок (deals).

Значения счёта переезжают в AccountInfoDouble() , и MT5 даёт уровень маржи напрямую:

double bal = AccountInfoDouble(ACCOUNT_BALANCE); double eq = AccountInfoDouble(ACCOUNT_EQUITY); double fm = AccountInfoDouble(ACCOUNT_MARGIN_FREE); double ml = AccountInfoDouble(ACCOUNT_MARGIN_LEVEL); // уже в процентах

Открытая экспозиция читается через API позиций вместо пула ордеров:

double floatPL = 0.0, lots = 0.0; int cnt = 0;
for(int i = 0; i < PositionsTotal(); i++)
  {
   ulong ticket = PositionGetTicket(i);
   if(ticket == 0) continue;
   floatPL += PositionGetDouble(POSITION_PROFIT) + PositionGetDouble(POSITION_SWAP);
   lots    += PositionGetDouble(POSITION_VOLUME);
   cnt++;
  } 

Реализованный P/L берётся из сделок (deals), после выбора диапазона времени через HistorySelect() :

double RealizedPnL(datetime from) { if(!HistorySelect(from, TimeCurrent())) return(0.0); double sum = 0.0; int deals = HistoryDealsTotal(); for(int i = 0; i < deals; i++) { ulong t = HistoryDealGetTicket(i); if(HistoryDealGetInteger(t, DEAL_ENTRY) != DEAL_ENTRY_OUT) continue; // только закрывающие sum += HistoryDealGetDouble(t, DEAL_PROFIT) + HistoryDealGetDouble(t, DEAL_SWAP) + HistoryDealGetDouble(t, DEAL_COMMISSION); } return(sum); }

С этими тремя заменами остальная часть статьи переносится без изменений — хорошая иллюстрация разделения слоя данных и слоя представления.

Полный код

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

Заключение

Несколькими десятками строк мы получили действительно полезный, всегда видимый срез состояния счёта: средства и маржа на глаз, плавающий и реализованный P/L и живая просадка — всё это только для чтения и автономно, из терминала ничего не уходит. По пути мы разобрали, как читать состояние счёта, как правильно проходить по открытому и историческому пулу ордеров (включая ловушку с балансовыми операциями), как хранить состояние во времени для просадки и как рисовать и поддерживать чистый интерфейс на графике.

Это покрывает один счёт в одном терминале. На практике многие трейдеры ведут несколько счетов сразу — разные стратегии, советники, проп-челленджи, управляемые счета — и хотят видеть их вместе, с телефона, и получать оповещение, когда что-то идёт не так. Это уже другая архитектура: советник ужимается до лёгкого отправителя данных, а дашборд переезжает в веб, где может агрегировать все счета и слать уведомления. Я сделал именно такое (FilMonitor); панель из этой статьи — её одноаккаунтный собрат на стороне терминала и удобный способ разобраться в метриках до того, как масштабировать их.

Полный AccountPanel.mq4 приведён выше и приложен. Скомпилируй, поставь на график — и жизненные показатели счёта всегда перед глазами.