English Deutsch 日本語
preview
Торговые инструменты на языке MQL5 (Часть 7): Информационная панель для мониторинга позиций на счете в разрезе символов

Торговые инструменты на языке MQL5 (Часть 7): Информационная панель для мониторинга позиций на счете в разрезе символов

MetaTrader 5Торговые системы |
98 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

В своей предыдущей статье (Часть 6) мы создали Динамическую голографическую панель на MetaQuotes Language 5 (MQL5) для отслеживания символов и таймфреймов с помощью RSI, оповещений о волатильности и интерактивных кнопок с анимацией импульсов. В Части 7 создаем информационную панель, отслеживающую позиции по нескольким символам, общее количество сделок, лоты, прибыль, отложенные ордера, свопы, комиссии и показатели счета, такие как баланс и эквити, с возможностью сортировки столбцов и экспорта данных в формате Comma Separated Values (CSV), для всестороннего контроля. В статье рассмотрим следующие темы:

  1. Понимание архитектуры информационной панели
  2. Реализация средствами MQL5
  3. Тестирование на истории
  4. Заключение

В итоге у вас будет мощная панель мониторинга в MQL5 для отслеживания позиций и счетов, готовая к настройке. Перейдём к реализации!


Понимание архитектуры информационной панели

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

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

ARCHITECTURE PLAN


Реализация средствами MQL5

Чтобы создать программу в MQL5, нам нужно будет определить метаданные программы, а затем определить некоторые входные параметры, которые позволят легко изменять работу программы, не вмешиваясь непосредственно в код, а также определять объекты панели.

//+------------------------------------------------------------------+
//|                                     Informational Dashboard.mq5  |
//|                           Copyright 2025, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, Allan Munene Mutiiria."
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"

// Input parameters
input int UpdateIntervalMs = 100; // Update interval (milliseconds, min 10ms)
input long MagicNumber = -1; // Magic number (-1 for all positions and orders)

// Defines for object names
#define PREFIX "DASH_"                   //--- Prefix for all dashboard objects
#define HEADER "HEADER_"                 //--- Prefix for header labels
#define SYMB "SYMB_"                     //--- Prefix for symbol labels
#define DATA "DATA_"                     //--- Prefix for data labels
#define HEADER_PANEL "HEADER_PANEL"      //--- Name for header panel
#define ACCOUNT_PANEL "ACCOUNT_PANEL"    //--- Name for account panel
#define FOOTER_PANEL "FOOTER_PANEL"      //--- Name for footer panel
#define FOOTER_TEXT "FOOTER_TEXT"        //--- Name for footer text label
#define FOOTER_DATA "FOOTER_DATA_"       //--- Prefix for footer data labels
#define PANEL "PANEL"                    //--- Name for main panel
#define ACC_TEXT "ACC_TEXT_"             //--- Prefix for account text labels
#define ACC_DATA "ACC_DATA_"             //--- Prefix for account data labels

Здесь мы настраиваем входные параметры и определяем константы для имен объектов в нашей Информационной панели в MQL5, что дает возможность выполнять пользовательские настройки и организованное именование элементов Пользовательского интерфейса (UI). Определяем "UpdateIntervalMs" как 100 миллисекунд (минимум 10 мс) для управления частотой обновления панели, обеспечивая своевременное обновление без перегрузки системы. Входной параметр "MagicNumber" устанавливается на -1 для отслеживания всех позиций и ордеров или на определенное значение для фильтрации по "магическому числу" советника для целевого отслеживания.

Для единообразного именования объектов мы используем директивы defines: "PREFIX" как "DASH_" для всех объектов панели, "HEADER" — для меток заголовков, "SYMB_" — для меток символов, "DATA_" — для меток данных, "HEADER_PANEL" — для панели заголовка, "ACCOUNT_PANEL" — для раздела счёта, "FOOTER_PANEL" — для нижнего колонтитула, "FOOTER_TEXT" — для текста нижнего колонтитула, "FOOTER_DATA_" — для префиксов данных нижнего колонтитула, "PANEL" — для основной панели, "ACC_TEXT_" — для префиксов текста счёта и "ACC_DATA_" — для префиксов данных счёта. Эти определения упрощают управление объектами и делают код более читабельным. Следующее, что нам нужно сделать, это создать некоторые структуры, которые будут содержать наши информационные данные и глобальные переменные, которые мы будем использовать на протяжении всей реализации.

// Dashboard settings
struct DashboardSettings {               //--- Structure for dashboard settings
   int panel_x;                          //--- X-coordinate of panel
   int panel_y;                          //--- Y-coordinate of panel
   int row_height;                       //--- Height of each row
   int font_size;                        //--- Font size for labels
   string font;                          //--- Font type for labels
   color bg_color;                       //--- Background color of main panel
   color border_color;                   //--- Border color of panels
   color header_color;                   //--- Default color for header text
   color text_color;                     //--- Default color for text
   color section_bg_color;               //--- Background color for header/footer panels
   int zorder_panel;                     //--- Z-order for main panel
   int zorder_subpanel;                  //--- Z-order for sub-panels
   int zorder_labels;                    //--- Z-order for labels
   int label_y_offset;                   //--- Y-offset for label positioning
   int label_x_offset;                   //--- X-offset for label positioning
   int header_x_distances[9];            //--- X-distances for header labels (9 columns)
   color header_shades[12];              //--- Array of header color shades for glow effect
} settings = {                           //--- Initialize settings with default values
   20,                                   //--- Set panel_x to 20 pixels
   20,                                   //--- Set panel_y to 20 pixels
   24,                                   //--- Set row_height to 24 pixels
   11,                                   //--- Set font_size to 11
   "Calibri Bold",                       //--- Set font to Calibri Bold
   C'240,240,240',                       //--- Set bg_color to light gray
   clrBlack,                             //--- Set border_color to black
   C'0,50,70',                           //--- Set header_color to dark teal
   clrBlack,                             //--- Set text_color to black
   C'200,220,230',                       //--- Set section_bg_color to light blue-gray
   100,                                  //--- Set zorder_panel to 100
   101,                                  //--- Set zorder_subpanel to 101
   102,                                  //--- Set zorder_labels to 102
   3,                                    //--- Set label_y_offset to 3 pixels
   25,                                   //--- Set label_x_offset to 25 pixels
   {10, 120, 170, 220, 280, 330, 400, 470, 530}, //--- X-distances for 9 columns
   {C'0,0,0', C'255,0,0', C'0,255,0', C'0,0,255', C'255,255,0', C'0,255,255', 
    C'255,0,255', C'255,255,255', C'255,0,255', C'0,255,255', C'255,255,0', C'0,0,255'}
};

// Data structure for symbol information
struct SymbolData {                      //--- Structure for symbol data
   string name;                          //--- Symbol name
   int buys;                             //--- Number of buy positions
   int sells;                            //--- Number of sell positions
   int trades;                           //--- Total number of trades
   double lots;                          //--- Total lots
   double profit;                        //--- Total profit
   int pending;                          //--- Number of pending orders
   double swaps;                         //--- Total swap
   double comm;                          //--- Total commission
   string buys_str;                      //--- String representation of buys
   string sells_str;                     //--- String representation of sells
   string trades_str;                    //--- String representation of trades
   string lots_str;                      //--- String representation of lots
   string profit_str;                    //--- String representation of profit
   string pending_str;                   //--- String representation of pending
   string swaps_str;                     //--- String representation of swap
   string comm_str;                      //--- String representation of commission
};

// Global variables
SymbolData symbol_data[];                //--- Array to store symbol data
long totalBuys = 0;                      //--- Total buy positions across symbols
long totalSells = 0;                     //--- Total sell positions across symbols
long totalTrades = 0;                    //--- Total trades across symbols
double totalLots = 0.0;                  //--- Total lots across symbols
double totalProfit = 0.0;                //--- Total profit across symbols
long totalPending = 0;                   //--- Total pending across symbols
double totalSwap = 0.0;                  //--- Total swap across symbols
double totalComm = 0.0;                  //--- Total commission across symbols
string headers[] = {"Symbol", "Buy P", "Sell P", "Trades", "Lots", "Profit", "Pending", "Swap", "Comm"}; //--- Header labels
int column_widths[] = {140, 50, 50, 50, 60, 90, 50, 60, 60}; //--- Widths for each column
color data_default_colors[] = {clrRed, clrGreen, clrDarkGray, clrOrange, clrGray, clrBlue, clrPurple, clrBrown};
int sort_column = 3;                     //--- Initial sort column (trades) 
bool sort_ascending = false;             //--- Sort direction (false for descending to show active first)
int glow_index = 0;                      //--- Current index for header glow effect
bool glow_direction = true;              //--- Glow direction (true for forward)
int glow_counter = 0;                    //--- Counter for glow timing
const int GLOW_INTERVAL_MS = 500;        //--- Glow cycle interval (500ms)
string total_buys_str = "";              //--- String for total buys display
string total_sells_str = "";             //--- String for total sells display
string total_trades_str = "";            //--- String for total trades display
string total_lots_str = "";              //--- String for total lots display
string total_profit_str = "";            //--- String for total profit display
string total_pending_str = "";           //--- String for total pending display
string total_swap_str = "";              //--- String for total swap display
string total_comm_str = "";              //--- String for total comm display
string account_items[] = {"Balance", "Equity", "Free Margin"}; //--- Account items
string acc_bal_str = "";                 //--- Strings for account data
string acc_eq_str = "";
string acc_free_str = "";
int prev_num_symbols = 0;                //--- Previous number of active symbols for dynamic resizing

Чтобы настроить пользовательский интерфейс и управление данными, мы определяем структуру "DashboardSettings" для хранения настроек структуры, инициализируя "panel_x" и "panel_y" по 20 пикселей для позиционирования, "row_height" - по 24 пикселя для межстрочного интервала, "font_size" - 11 для текста, "font" - как "Calibri Bold" для стиля, "bg_color" - светло-серый для главной панели, "border_color" - черный для контуров панели, "header_color" - темно-бирюзовый для верхних колонтитулов, "text_color" - черный для общего текста, "section_bg_color" - светло-сине-серый для верхних и нижних колонтитулов, "zorder_panel" - 100, "zorder_subpanel" - 101, и "zorder_labels" - 102 для управления слоями отображения, "label_y_offset" - 3 и "label_x_offset" - 25 для выравнивания меток, "header_x_distances" для девяти позиций столбцов и "header_shades" с 12 цветами для эффекта свечения.

Создаем структуру "SymbolData" для хранения данных по каждому символу, включая "name" для символа, "buys", "sells", "trades", "pending" для количества и "lots", "profit", "swaps", "comm" для значений, с соответствующими строковыми полями типа "buys_str" для отображения. Объявляем глобальные переменные:

  • массив "symbol_data" для данных о символах,
  • "totalBuys", "totalSells", "totalTrades", "totalPending" как параметры типа long, инициализированные нулем,
  • "totalLots", "totalProfit", "totalSwap", "totalComm" как параметры типа double, инициализированные нулем,
  • массив "headers" для меток столбцов,
  • "column_widths" - для установки размеров столбцов,
  • "data_default_colors" - для цветов по конкретным столбцам,
  • "sort_column" - значение 3 для сортировки по умолчанию по сделкам,
  • "sort_ascending" - как значение false для убывания,
  • "glow_index" и "glow_counter" - значение 0,
  • "glow_direction" - значение true, а "GLOW_INTERVAL_MS" - значение 500 мс для свечения заголовка,
  • строковые переменные, такие как "total_buys_str" для отображения итоговых значений,
  • "account_items" для меток баланса, эквити и свободной маржи, их строковые представления, такие как "acc_bal_str" и "prev_num_symbols" с значением 0 для динамического изменения размера.

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

//+------------------------------------------------------------------+
//| Create label function                                            |
//+------------------------------------------------------------------+
bool createLABEL(string objName, string txt, int xD, int yD, color clrTxt, int fontSize, string font, int anchor, bool selectable = false) {
   if(!ObjectCreate(0, objName, OBJ_LABEL, 0, 0, 0)) { //--- Create label object
      Print(__FUNCTION__, ": Failed to create label '", objName, "'. Error code = ", GetLastError()); //--- Log creation failure
      return(false);                     //--- Return failure
   }
   ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, xD); //--- Set x-coordinate
   ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, yD); //--- Set y-coordinate
   ObjectSetInteger(0, objName, OBJPROP_CORNER, CORNER_LEFT_UPPER); //--- Set corner alignment
   ObjectSetString(0, objName, OBJPROP_TEXT, txt); //--- Set text
   ObjectSetInteger(0, objName, OBJPROP_FONTSIZE, fontSize); //--- Set font size
   ObjectSetString(0, objName, OBJPROP_FONT, font); //--- Set font type
   ObjectSetInteger(0, objName, OBJPROP_COLOR, clrTxt); //--- Set text color
   ObjectSetInteger(0, objName, OBJPROP_BACK, false); //--- Set to foreground
   ObjectSetInteger(0, objName, OBJPROP_STATE, selectable); //--- Set selectable state
   ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, selectable); //--- Set selectability
   ObjectSetInteger(0, objName, OBJPROP_SELECTED, false); //--- Set not selected
   ObjectSetInteger(0, objName, OBJPROP_ANCHOR, anchor); //--- Set anchor point
   ObjectSetInteger(0, objName, OBJPROP_ZORDER, settings.zorder_labels); //--- Set z-order
   ObjectSetString(0, objName, OBJPROP_TOOLTIP, selectable ? "Click to sort" : "Position data"); //--- Set tooltip
   ChartRedraw(0);                       //--- Redraw chart
   return(true);                         //--- Return success
}

//+------------------------------------------------------------------+
//| Update label function                                            |
//+------------------------------------------------------------------+
bool updateLABEL(string objName, string txt, color clrTxt) {
   int found = ObjectFind(0, objName);   //--- Find object
   if(found < 0) {                       //--- Check if object not found
      Print(__FUNCTION__, ": Failed to find label '", objName, "'. Error code = ", GetLastError()); //--- Log error
      return(false);                     //--- Return failure
   }
   string current_txt = ObjectGetString(0, objName, OBJPROP_TEXT); //--- Get current text
   if(current_txt != txt) {              //--- Check if text changed
      ObjectSetString(0, objName, OBJPROP_TEXT, txt); //--- Update text
      ObjectSetInteger(0, objName, OBJPROP_COLOR, clrTxt); //--- Update color
      return(true);                      //--- Indicate redraw needed
   }
   return(false);                        //--- No update needed
}

Мы реализуем функцию "createLABEL" для создания текстовых меток для панели, используя параметры "objName", "txt", "xD", "yD", "clrTxt", "fontSize", "font", "anchor" и "selectable". Создаём метку с помощью функции ObjectCreate как OBJ_LABEL, выводя информацию об ошибках с помощью Print и возвращая false в случае неудачи, затем устанавливаем свойства с помощью ObjectSetInteger для свойств OBJPROP_XDISTANCE, "OBJPROP_YDISTANCE", "OBJPROP_CORNER" как "CORNER_LEFT_UPPER", "OBJPROP_FONTSIZE", "OBJPROP_COLOR", "OBJPROP_BACK" как false, "OBJPROP_STATE" и "OBJPROP_SELECTABLE" на основе "selectable", "OBJPROP_SELECTED" как false, "OBJPROP_ANCHOR" и OBJPROP_ZORDER из "settings.zorder_labels", а также ObjectSetString для "OBJPROP_TEXT" и OBJPROP_FONT, с подсказкой посредством "OBJPROP_TOOLTIP" для сортировки или данных. Перерисовываем с помощью ChartRedraw и возвращаем значение true.

Функция "updateLABEL" обновляет существующие метки, проверяя ObjectFind на наличие "objName", записывая это в лог и возвращая значение false, если метка не найдена. Если значение "current_txt" из "ObjectGetString" отличается от "txt", обновляем значения "OBJPROP_TEXT" и "OBJPROP_COLOR" значениями "ObjectSetString" и "ObjectSetInteger", возвращая значение true, чтобы указать, что требуется перерисовка, или значение false в противном случае. Эти функции обеспечат гибкое создание меток и эффективное обновление для отображения на панели. Затем можно создать другие вспомогательные функции для сбора всей необходимой информации.

//+------------------------------------------------------------------+
//| Count total positions for a symbol                               |
//+------------------------------------------------------------------+
string countPositionsTotal(string symbol) {
   int totalPositions = 0;               //--- Initialize position counter
   int count_Total_Pos = PositionsTotal(); //--- Get total positions
   for(int i = count_Total_Pos - 1; i >= 0; i--) { //--- Iterate through positions
      ulong ticket = PositionGetTicket(i); //--- Get position ticket
      if(ticket > 0 && PositionSelectByTicket(ticket)) { //--- Check if position selected
         if(PositionGetString(POSITION_SYMBOL) == symbol && (MagicNumber < 0 || PositionGetInteger(POSITION_MAGIC) == MagicNumber)) totalPositions++; //--- Check symbol and magic
      }
   }
   return IntegerToString(totalPositions); //--- Return total as string
}

//+------------------------------------------------------------------+
//| Count buy or sell positions for a symbol                         |
//+------------------------------------------------------------------+
string countPositions(string symbol, ENUM_POSITION_TYPE pos_type) {
   int totalPositions = 0;               //--- Initialize position counter
   int count_Total_Pos = PositionsTotal(); //--- Get total positions
   for(int i = count_Total_Pos - 1; i >= 0; i--) { //--- Iterate through positions
      ulong ticket = PositionGetTicket(i); //--- Get position ticket
      if(ticket > 0 && PositionSelectByTicket(ticket)) { //--- Check if position selected
         if(PositionGetString(POSITION_SYMBOL) == symbol && PositionGetInteger(POSITION_TYPE) == pos_type && (MagicNumber < 0 || PositionGetInteger(POSITION_MAGIC) == MagicNumber)) { //--- Check symbol, type, magic
            totalPositions++;            //--- Increment counter
         }
      }
   }
   return IntegerToString(totalPositions); //--- Return total as string
}

//+------------------------------------------------------------------+
//| Count pending orders for a symbol                                |
//+------------------------------------------------------------------+
string countOrders(string symbol) {
   int total = 0;                        //--- Initialize counter
   int tot = OrdersTotal();              //--- Get total orders
   for(int i = tot - 1; i >= 0; i--) {   //--- Iterate through orders
      ulong ticket = OrderGetTicket(i);  //--- Get order ticket
      if(ticket > 0 && OrderSelect(ticket)) { //--- Check if order selected
         if(OrderGetString(ORDER_SYMBOL) == symbol && (MagicNumber < 0 || OrderGetInteger(ORDER_MAGIC) == MagicNumber)) total++; //--- Check symbol and magic
      }
   }
   return IntegerToString(total);        //--- Return total as string
}

//+------------------------------------------------------------------+
//| Sum double property for positions of a symbol                    |
//+------------------------------------------------------------------+
string sumPositionDouble(string symbol, ENUM_POSITION_PROPERTY_DOUBLE prop) {
   double total = 0.0;                   //--- Initialize total
   int count_Total_Pos = PositionsTotal(); //--- Get total positions
   for(int i = count_Total_Pos - 1; i >= 0; i--) { //--- Iterate through positions
      ulong ticket = PositionGetTicket(i); //--- Get position ticket
      if(ticket > 0 && PositionSelectByTicket(ticket)) { //--- Check if position selected
         if(PositionGetString(POSITION_SYMBOL) == symbol && (MagicNumber < 0 || PositionGetInteger(POSITION_MAGIC) == MagicNumber)) { //--- Check symbol and magic
            total += PositionGetDouble(prop); //--- Add property value
         }
      }
   }
   return DoubleToString(total, 2);      //--- Return total as string
}

//+------------------------------------------------------------------+
//| Sum commission for positions of a symbol from history            |
//+------------------------------------------------------------------+
double sumPositionCommission(string symbol) {
   double total_comm = 0.0;              //--- Initialize total commission
   int pos_total = PositionsTotal();     //--- Get total positions
   for(int p = 0; p < pos_total; p++) {  //--- Iterate through positions
      ulong ticket = PositionGetTicket(p); //--- Get position ticket
      if(ticket > 0 && PositionSelectByTicket(ticket)) { //--- Check if selected
         if(PositionGetString(POSITION_SYMBOL) == symbol && (MagicNumber < 0 || PositionGetInteger(POSITION_MAGIC) == MagicNumber)) { //--- Check symbol and magic
            long pos_id = PositionGetInteger(POSITION_IDENTIFIER); //--- Get position ID
            if(HistorySelectByPosition(pos_id)) { //--- Select history by position
               int deals_total = HistoryDealsTotal(); //--- Get total deals
               for(int d = 0; d < deals_total; d++) { //--- Iterate through deals
                  ulong deal_ticket = HistoryDealGetTicket(d); //--- Get deal ticket
                  if(deal_ticket > 0) {    //--- Check valid
                     total_comm += HistoryDealGetDouble(deal_ticket, DEAL_COMMISSION); //--- Add commission
                  }
               }
            }
         }
      }
   }
   return total_comm;                    //--- Return total commission
}

//+------------------------------------------------------------------+
//| Collect active symbols with positions or orders                  |
//+------------------------------------------------------------------+
void CollectActiveSymbols() {
   string symbols_temp[];
   int added = 0;
   // Collect from positions
   int pos_total = PositionsTotal();
   for(int i = 0; i < pos_total; i++) {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0) continue;
      PositionSelectByTicket(ticket);
      if(MagicNumber < 0 || PositionGetInteger(POSITION_MAGIC) == MagicNumber) {
         string sym = PositionGetString(POSITION_SYMBOL);
         bool found = false;
         for(int k = 0; k < added; k++) {
            if(symbols_temp[k] == sym) {
               found = true;
               break;
            }
         }
         if(!found) {
            ArrayResize(symbols_temp, added + 1);
            symbols_temp[added] = sym;
            added++;
         }
      }
   }
   // Collect from orders
   int ord_total = OrdersTotal();
   for(int i = 0; i < ord_total; i++) {
      ulong ticket = OrderGetTicket(i);
      if(ticket == 0) continue;
      bool isSelected = OrderSelect(ticket);
      if(MagicNumber < 0 || OrderGetInteger(ORDER_MAGIC) == MagicNumber) {
         string sym = OrderGetString(ORDER_SYMBOL);
         bool found = false;
         for(int k = 0; k < added; k++) {
            if(symbols_temp[k] == sym) {
               found = true;
               break;
            }
         }
         if(!found) {
            ArrayResize(symbols_temp, added + 1);
            symbols_temp[added] = sym;
            added++;
         }
      }
   }
   // Set symbol_data
   ArrayResize(symbol_data, added);
   for(int i = 0; i < added; i++) {
      symbol_data[i].name = symbols_temp[i];
      symbol_data[i].buys = 0;
      symbol_data[i].sells = 0;
      symbol_data[i].trades = 0;
      symbol_data[i].lots = 0.0;
      symbol_data[i].profit = 0.0;
      symbol_data[i].pending = 0;
      symbol_data[i].swaps = 0.0;
      symbol_data[i].comm = 0.0;
      symbol_data[i].buys_str = "0";
      symbol_data[i].sells_str = "0";
      symbol_data[i].trades_str = "0";
      symbol_data[i].lots_str = "0.00";
      symbol_data[i].profit_str = "0.00";
      symbol_data[i].pending_str = "0";
      symbol_data[i].swaps_str = "0.00";
      symbol_data[i].comm_str = "0.00";
   }
}

Здесь мы используем вспомогательные функции для сбора и обобщения торговых данных, обеспечивая точное отслеживание позиций и ордеров по различным символам. Функция "countPositionsTotal" подсчитывает все позиции для заданного "symbol", проходя циклом по PositionsTotal, выбирая каждый "ticket" с помощью PositionGetTicket и PositionSelectByTicket и увеличивая "totalPositions", если символ совпадает, а "MagicNumber" равен -1 или совпадает с POSITION_MAGIC с помощью "PositionGetInteger". Возвращает значение count в виде строки с помощью IntegerToString. 

Функция "countPositions" подсчитывает позиции на покупку или продажу для "symbol" и "pos_type", аналогично перебирая позиции, проверяя POSITION_TYPE на соответствие "pos_type" и возвращая количество в виде строки. Функция "countOrders" подсчитывает отложенные ордера для "symbol", перебирая OrdersTotal, выбирая "ticket" с помощью "OrderGetTicket" и OrderSelect, увеличивая "total", если символ и "MagicNumber" совпадают, и возвращает количество в виде строки. Функция "sumPositionDouble" суммирует свойство double, такое как объем, прибыль или своп, для "symbol", перебирая позиции, добавляя значения PositionGetDouble для указанного параметра "prop", если условия совпадают, и возвращает итоговое значение, отформатированное с помощью DoubleToString с точностью до двух знаков после запятой.

Функция "sumPositionCommission" вычисляет общую комиссию для "symbol" из истории сделок, перебирая позиции, выбирая "pos_id" с помощью "PositionGetInteger", используя HistorySelectByPosition для получения сделок, суммируя "DEAL_COMMISSION" с помощью "HistoryDealGetDouble" для каждого действительного "deal_ticket" из HistoryDealGetTicket и возвращая общую сумму.

Функция "CollectActiveSymbols" собирает символы с активными позициями или ордерами в "symbols_temp", перебирая PositionsTotal и "OrdersTotal", проверяя условия "MagicNumber" и добавляя уникальные символы с помощью ArrayResize. Она изменяет размер "symbol_data" в соответствии с заданными параметрами и инициализирует такие поля, как "name", counts и strings, нулями или значениями по умолчанию. Эти функции позволят панели эффективно собирать и отображать точные торговые данные. На данный момент у нас есть все необходимые функции для инициализации нашей панели. Перейдём к созданию панели в обработчике OnInit, чтобы можно было продолжать отслеживать наши обновления.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   // Collect active symbols first
   CollectActiveSymbols();
   int num_rows = ArraySize(symbol_data);
   // Calculate dimensions
   int num_columns = ArraySize(headers);    //--- Get number of columns
   int column_width_sum = 0;                //--- Initialize sum of column widths
   for(int i = 0; i < num_columns; i++)     //--- Iterate through columns
      column_width_sum += column_widths[i]; //--- Add column width to sum
   int panel_width = MathMax(settings.header_x_distances[num_columns - 1] + column_widths[num_columns - 1], column_width_sum) + 20 + settings.label_x_offset; //--- Calculate panel width

   // Create main panel in foreground
   string panel_name = PREFIX + PANEL;                                                   //--- Define main panel name
   ObjectCreate(0, panel_name, OBJ_RECTANGLE_LABEL, 0, 0, 0);                            //--- Create main panel
   ObjectSetInteger(0, panel_name, OBJPROP_CORNER, CORNER_LEFT_UPPER);                   //--- Set panel corner
   ObjectSetInteger(0, panel_name, OBJPROP_XDISTANCE, settings.panel_x);                 //--- Set panel x-coordinate
   ObjectSetInteger(0, panel_name, OBJPROP_YDISTANCE, settings.panel_y);                 //--- Set panel y-coordinate
   ObjectSetInteger(0, panel_name, OBJPROP_XSIZE, panel_width);                          //--- Set panel width
   ObjectSetInteger(0, panel_name, OBJPROP_YSIZE, (num_rows + 3) * settings.row_height); //--- Set panel height
   ObjectSetInteger(0, panel_name, OBJPROP_BGCOLOR, settings.bg_color);                  //--- Set background color
   ObjectSetInteger(0, panel_name, OBJPROP_BORDER_TYPE, BORDER_FLAT);                    //--- Set border type
   ObjectSetInteger(0, panel_name, OBJPROP_BORDER_COLOR, settings.border_color);         //--- Set border color
   ObjectSetInteger(0, panel_name, OBJPROP_BACK, false);                                 //--- Set panel to foreground
   ObjectSetInteger(0, panel_name, OBJPROP_ZORDER, settings.zorder_panel);               //--- Set z-order

   // Create header panel
   string header_panel = PREFIX + HEADER_PANEL;                                          //--- Define header panel name
   ObjectCreate(0, header_panel, OBJ_RECTANGLE_LABEL, 0, 0, 0);                          //--- Create header panel
   ObjectSetInteger(0, header_panel, OBJPROP_CORNER, CORNER_LEFT_UPPER);                 //--- Set header panel corner
   ObjectSetInteger(0, header_panel, OBJPROP_XDISTANCE, settings.panel_x);               //--- Set header panel x-coordinate
   ObjectSetInteger(0, header_panel, OBJPROP_YDISTANCE, settings.panel_y);               //--- Set header panel y-coordinate
   ObjectSetInteger(0, header_panel, OBJPROP_XSIZE, panel_width);                        //--- Set header panel width
   ObjectSetInteger(0, header_panel, OBJPROP_YSIZE, settings.row_height);                //--- Set header panel height
   ObjectSetInteger(0, header_panel, OBJPROP_BGCOLOR, settings.section_bg_color);        //--- Set header panel background color
   ObjectSetInteger(0, header_panel, OBJPROP_BORDER_TYPE, BORDER_FLAT);                  //--- Set header panel border type
   ObjectSetInteger(0, header_panel, OBJPROP_BORDER_COLOR, settings.border_color);       //--- Set border color
   ObjectSetInteger(0, header_panel, OBJPROP_ZORDER, settings.zorder_subpanel);          //--- Set header panel z-order

   return(INIT_SUCCEEDED);               //--- Return initialization success
}

В обработчике OnInit инициализируем логику для настройки основы пользовательского интерфейса для мониторинга позиций и торговых счетов. Для начала вызовем функцию "CollectActiveSymbols", чтобы заполнить массив "symbol_data" активными символами, и установим значение параметра "num_rows" равным его размеру с помощью ArraySize. Рассчитываем "num_columns" из массива "headers" и вычисляем "column_width_sum", перебирая значения "column_widths" в цикле for и суммируя каждое значение ширины. Значение "panel_width" определяется с помощью MathMax, используя последнее значение "header_x_distances" плюс соответствующие значения "column_widths" и "column_width_sum", добавляя 20 и "settings.label_x_offset" для заполнения.

Создаём главную панель с помощью ObjectCreate как OBJ_RECTANGLE_LABEL с именем "PREFIX + PANEL", устанавливая "OBJPROP_CORNER" в "CORNER_LEFT_UPPER", "OBJPROP_XDISTANCE" и "OBJPROP_YDISTANCE" из "settings.panel_x" и "settings.panel_y", "OBJPROP_XSIZE" в "panel_width", "OBJPROP_YSIZE" в "(num_rows + 3) * settings.row_height", "OBJPROP_BGCOLOR" в "settings.bg_color", "OBJPROP_BORDER_TYPE" в "BORDER_FLAT", "OBJPROP_BORDER_COLOR" в "settings.border_color", "OBJPROP_BACK" в значение false и OBJPROP_ZORDER в "settings.zorder_panel". Для панели заголовка мы используем аналогичный подход и возвращаем значение "INIT_SUCCEEDED", указывающее на успешную инициализацию. Это формирует основные элементы панели для отображения данных, и после компиляции получаем следующий результат.

MAIN AND HEADER PANEL

Заложив основу, теперь можем создать остальные подпанели и метки. Для этого мы используем следующую логику.

// Create headers with manual X-distances
int header_y = settings.panel_y + 8 + settings.label_y_offset; //--- Calculate header y-coordinate
for(int i = 0; i < num_columns; i++) {                         //--- Iterate through headers
   string header_name = PREFIX + HEADER + IntegerToString(i);  //--- Define header label name
   int header_x = settings.panel_x + settings.header_x_distances[i] + settings.label_x_offset; //--- Calculate header x-coordinate
   createLABEL(header_name, headers[i], header_x, header_y, settings.header_color, 12, settings.font, ANCHOR_LEFT, true); //--- Create header label
}

// Create symbol labels and data labels
int first_row_y = header_y + settings.row_height;                                       //--- Calculate y-coordinate for first row
int symbol_x = settings.panel_x + 10 + settings.label_x_offset;                         //--- Set x-coordinate for symbol labels
for(int i = 0; i < num_rows; i++) {                                                     //--- Iterate through symbols
   string symbol_name = PREFIX + SYMB + IntegerToString(i);                             //--- Define symbol label name
   createLABEL(symbol_name, symbol_data[i].name, symbol_x, first_row_y + i * settings.row_height + settings.label_y_offset, settings.text_color, settings.font_size, settings.font, ANCHOR_LEFT); //--- Create symbol label
   int x_offset = settings.panel_x + 10 + column_widths[0] + settings.label_x_offset;   //--- Set initial x-offset for data labels
   for(int j = 0; j < num_columns - 1; j++) {                                           //--- Iterate through data columns
      string data_name = PREFIX + DATA + IntegerToString(i) + "_" + IntegerToString(j); //--- Define data label name
      color initial_color = data_default_colors[j];                                     //--- Set initial color
      string initial_txt = (j <= 2 || j == 5) ? "0" : "0.00";                           //--- Set initial text
      createLABEL(data_name, initial_txt, x_offset, first_row_y + i * settings.row_height + settings.label_y_offset, initial_color, settings.font_size, settings.font, ANCHOR_RIGHT); //--- Create data label
      x_offset += column_widths[j + 1];                                                 //--- Update x-offset
   }
}

// Create footer panel at the bottom
int footer_y = settings.panel_y + (num_rows + 3) * settings.row_height - settings.row_height - 5; //--- Calculate footer y-coordinate
string footer_panel = PREFIX + FOOTER_PANEL;                                    //--- Define footer panel name
ObjectCreate(0, footer_panel, OBJ_RECTANGLE_LABEL, 0, 0, 0);                    //--- Create footer panel
ObjectSetInteger(0, footer_panel, OBJPROP_CORNER, CORNER_LEFT_UPPER);           //--- Set footer panel corner
ObjectSetInteger(0, footer_panel, OBJPROP_XDISTANCE, settings.panel_x);         //--- Set footer panel x-coordinate
ObjectSetInteger(0, footer_panel, OBJPROP_YDISTANCE, footer_y);                 //--- Set footer panel y-coordinate
ObjectSetInteger(0, footer_panel, OBJPROP_XSIZE, panel_width);                  //--- Set footer panel width
ObjectSetInteger(0, footer_panel, OBJPROP_YSIZE, settings.row_height + 5);      //--- Set footer panel height
ObjectSetInteger(0, footer_panel, OBJPROP_BGCOLOR, settings.section_bg_color);  //--- Set footer panel background color
ObjectSetInteger(0, footer_panel, OBJPROP_BORDER_TYPE, BORDER_FLAT);            //--- Set footer panel border type
ObjectSetInteger(0, footer_panel, OBJPROP_BORDER_COLOR, settings.border_color); //--- Set border color
ObjectSetInteger(0, footer_panel, OBJPROP_ZORDER, settings.zorder_subpanel);    //--- Set footer panel z-order

// Create footer text and data
int footer_text_x = settings.panel_x + 10 + settings.label_x_offset;               //--- Set x-coordinate for footer text
createLABEL(PREFIX + FOOTER_TEXT, "Total:", footer_text_x, footer_y + 8 + settings.label_y_offset, settings.text_color, settings.font_size, settings.font, ANCHOR_LEFT); //--- Create footer text label
int x_offset = settings.panel_x + 10 + column_widths[0] + settings.label_x_offset; //--- Set initial x-offset for footer data
for(int j = 0; j < num_columns - 1; j++) {                                         //--- Iterate through footer data columns
   string footer_data_name = PREFIX + FOOTER_DATA + IntegerToString(j);            //--- Define footer data label name
   color footer_color = data_default_colors[j];                                    //--- Set footer data color
   string initial_txt = (j <= 2 || j == 5) ? "0" : "0.00";                         //--- Set initial text
   createLABEL(footer_data_name, initial_txt, x_offset, footer_y + 8 + settings.label_y_offset, footer_color, settings.font_size, settings.font, ANCHOR_RIGHT); //--- Create footer data label
   x_offset += column_widths[j + 1];                                               //--- Update x-offset
}

Продолжаем создавать информационную панель, разрабатывая элементы пользовательского интерфейса для заголовка, символов, данных и нижнего колонтитула в функции OnInit, настраивая визуальную структуру для отображения торговых данных. Для заголовка рассчитываем "header_y" как "settings.panel_y + 8 + settings.label_y_offset" и перебираем "num_columns" с помощью цикла for, создавая каждую метку заголовка с помощью "createLABEL", используя "PREFIX + HEADER + IntegerToString(i)" в качестве имени заголовка для уникальности, "headers[i]" в качестве текста, "header_x", вычисленный из "settings.panel_x + settings.header_x_distances[i] + settings.label_x_offset", "settings.header_color", размер шрифта 12, "settings.font", "ANCHOR_LEFT", и выбираемое значение true для взаимодействия с сортировкой, поскольку позже нам потребуется включить функцию сортировки.

Для символов и данных задаем "first_row_y" как "header_y + settings.row_height", а "symbol_x" как "settings.panel_x + 10 + settings.label_x_offset". Перебираем "num_rows" с помощью цикла for, создавая метки символов с помощью "createLABEL", используя "PREFIX + SYMB + IntegerToString(i)", "symbol_data[i].name", "symbol_x" и "first_row_y + i * settings.row_height + settings.label_y_offset" в "settings.text_color". Для каждого ряда проходим циклом по столбцам данных "num_columns - 1", создавая метки с помощью "createLABEL", используя "PREFIX + DATA + IntegerToString(i) + '_' + IntegerToString(j)", начальный текст "0" или "0.00" в зависимости от столбца, "x_offset", начинающийся с "settings.panel_x + 10 + column_widths[0] + settings.label_x_offset" и увеличивающийся на "column_widths[j + 1]", "data_default_colors[j]" и "ANCHOR_RIGHT".

Для нижнего колонтитула рассчитываем "footer_y" как "settings.panel_y + (num_rows + 3) * settings.row_height - settings.row_height - 5" и создаем панель нижнего колонтитула с помощью "ObjectCreate" как OBJ_RECTANGLE_LABEL с именем "PREFIX + FOOTER_PANEL", устанавливая "OBJPROP_CORNER" в "CORNER_LEFT_UPPER", OBJPROP_XDISTANCE в "settings.panel_x", "OBJPROP_YDISTANCE" в "footer_y", "OBJPROP_XSIZE" в "panel_width", "OBJPROP_YSIZE" в "settings.row_height + 5", "OBJPROP_BGCOLOR" в "settings.section_bg_color", "OBJPROP_BORDER_TYPE" в "BORDER_FLAT". "OBJPROP_BORDER_COLOR" в "settings.border_color", а "OBJPROP_ZORDER" — в "settings.zorder_subpanel".

Создаём текст нижнего колонтитула с помощью "createLABEL", используя "PREFIX + FOOTER_TEXT", "Total:", "footer_text_x" в "settings.panel_x + 10 + settings.label_x_offset", и проходим в цикле по "num_columns - 1" для создания меток данных нижнего колонтитула с помощью "createLABEL", используя "PREFIX + FOOTER_DATA + IntegerToString(j)", начальный текст, "x_offset", обновляемое значением "column_widths[j + 1]", и "data_default_colors[j]", взятые из массива. При компилировании получаем следующий результат.

FILLED DATA METRICS PANEL

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

// Create account panel below footer
int account_panel_y = footer_y + settings.row_height + 10;                            //--- Calculate account panel y-coordinate
string account_panel_name = PREFIX + ACCOUNT_PANEL;                                   //--- Define account panel name
ObjectCreate(0, account_panel_name, OBJ_RECTANGLE_LABEL, 0, 0, 0);                    //--- Create account panel
ObjectSetInteger(0, account_panel_name, OBJPROP_CORNER, CORNER_LEFT_UPPER);           //--- Set corner
ObjectSetInteger(0, account_panel_name, OBJPROP_XDISTANCE, settings.panel_x);         //--- Set x-coordinate
ObjectSetInteger(0, account_panel_name, OBJPROP_YDISTANCE, account_panel_y);          //--- Set y-coordinate
ObjectSetInteger(0, account_panel_name, OBJPROP_XSIZE, panel_width);                  //--- Set width
ObjectSetInteger(0, account_panel_name, OBJPROP_YSIZE, settings.row_height);          //--- Set height
ObjectSetInteger(0, account_panel_name, OBJPROP_BGCOLOR, settings.section_bg_color);  //--- Set background color
ObjectSetInteger(0, account_panel_name, OBJPROP_BORDER_TYPE, BORDER_FLAT);            //--- Set border type
ObjectSetInteger(0, account_panel_name, OBJPROP_BORDER_COLOR, settings.border_color); //--- Set border color
ObjectSetInteger(0, account_panel_name, OBJPROP_ZORDER, settings.zorder_subpanel);    //--- Set z-order

// Create account text and data labels
int acc_x = settings.panel_x + 10 + settings.label_x_offset;                          //--- Set base x for account labels
int acc_data_offset = 160;                                                            //--- Increased offset for data labels to avoid overlap
int acc_spacing = (panel_width - 45) / ArraySize(account_items);                      //--- Adjusted spacing to fit
for(int k = 0; k < ArraySize(account_items); k++) {                                   //--- Iterate through account items
   string acc_text_name = PREFIX + ACC_TEXT + IntegerToString(k);                     //--- Define text label name
   int text_x = acc_x + k * acc_spacing;                                              //--- Calculate text x
   createLABEL(acc_text_name, account_items[k] + ":", text_x, account_panel_y + 8 + settings.label_y_offset, settings.text_color, settings.font_size, settings.font, ANCHOR_LEFT); //--- Create text label
   string acc_data_name = PREFIX + ACC_DATA + IntegerToString(k);                     //--- Define data label name
   int data_x = text_x + acc_data_offset;                                             //--- Calculate data x
   createLABEL(acc_data_name, "0.00", data_x, account_panel_y + 8 + settings.label_y_offset, settings.text_color, settings.font_size, settings.font, ANCHOR_RIGHT); //--- Create data label
}

Здесь мы создаём панель счёта и её метки для отображения показателей счёта, завершая настройку пользовательского интерфейса в функции OnInit. Мы рассчитываем "account_panel_y" как "footer_y + settings.row_height + 10" и создаем панель с помощью ObjectCreate как "OBJ_RECTANGLE_LABEL" с именем "PREFIX + ACCOUNT_PANEL", устанавливая "OBJPROP_CORNER" в "CORNER_LEFT_UPPER", "OBJPROP_XDISTANCE" в "settings.panel_x", "OBJPROP_YDISTANCE" в "account_panel_y", "OBJPROP_XSIZE" в "panel_width", "OBJPROP_YSIZE" в "settings.row_height", "OBJPROP_BGCOLOR" в "settings.section_bg_color", "OBJPROP_BORDER_TYPE" в "BORDER_FLAT", OBJPROP_BORDER_COLOR в "settings.border_color" и "OBJPROP_ZORDER" в "settings.zorder_subpanel".

Для меток счёта устанавливаем "acc_x" равным "settings.panel_x + 10 + settings.label_x_offset", "acc_data_offset" равным 160, а "acc_spacing" равным "(panel_width - 45) / ArraySize(account_items)" для равномерного расстояния и следуем аналогичному формату, как и в логике создания основной панели. В этой конфигурации баланс, эквити и свободная маржа будут отображаться в аккуратной, выровненной панели под нижним колонтитулом. Смотрите ниже.

ACCOUNT METRICS PANEL

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

//+------------------------------------------------------------------+
//| Sort dashboard by selected column                                |
//+------------------------------------------------------------------+
void SortDashboard() {
   int n = ArraySize(symbol_data);       //--- Get number of symbols
   for(int i = 0; i < n - 1; i++) {     //--- Iterate through symbols
      for(int j = 0; j < n - i - 1; j++) { //--- Compare adjacent symbols
         bool swap = false;              //--- Initialize swap flag
         switch(sort_column) {           //--- Check sort column
            case 0:                      //--- Sort by symbol name
               swap = sort_ascending ? symbol_data[j].name > symbol_data[j + 1].name : symbol_data[j].name < symbol_data[j + 1].name;
               break;
            case 1:                      //--- Sort by buys
               swap = sort_ascending ? symbol_data[j].buys > symbol_data[j + 1].buys : symbol_data[j].buys < symbol_data[j + 1].buys;
               break;
            case 2:                      //--- Sort by sells
               swap = sort_ascending ? symbol_data[j].sells > symbol_data[j + 1].sells : symbol_data[j].sells < symbol_data[j + 1].sells;
               break;
            case 3:                      //--- Sort by trades
               swap = sort_ascending ? symbol_data[j].trades > symbol_data[j + 1].trades : symbol_data[j].trades < symbol_data[j + 1].trades;
               break;
            case 4:                      //--- Sort by lots
               swap = sort_ascending ? symbol_data[j].lots > symbol_data[j + 1].lots : symbol_data[j].lots < symbol_data[j + 1].lots;
               break;
            case 5:                      //--- Sort by profit
               swap = sort_ascending ? symbol_data[j].profit > symbol_data[j + 1].profit : symbol_data[j].profit < symbol_data[j + 1].profit;
               break;
            case 6:                      //--- Sort by pending
               swap = sort_ascending ? symbol_data[j].pending > symbol_data[j + 1].pending : symbol_data[j].pending < symbol_data[j + 1].pending;
               break;
            case 7:                      //--- Sort by swaps
               swap = sort_ascending ? symbol_data[j].swaps > symbol_data[j + 1].swaps : symbol_data[j].swaps < symbol_data[j + 1].swaps;
               break;
            case 8:                      //--- Sort by comm
               swap = sort_ascending ? symbol_data[j].comm > symbol_data[j + 1].comm : symbol_data[j].comm < symbol_data[j + 1].comm;
               break;
         }
         if(swap) {                      //--- Check if swap needed
            SymbolData temp = symbol_data[j]; //--- Store temporary data
            symbol_data[j] = symbol_data[j + 1]; //--- Swap data
            symbol_data[j + 1] = temp;   //--- Complete swap
         }
      }
   }
}

Мы реализуем функцию "SortDashboard" для обеспечения динамической сортировки, позволяющей нам упорядочивать данные символов по выбранным столбцам. Получаем количество символов с помощью параметра ArraySize в переменной "symbol_data" и сохраняем в переменной "n". Используя вложенные циклы for, перебираем "n - 1" символов и сравниваем смежные пары вплоть до "n - i - 1". Инициализируем флаг «swap» значением false и используем оператор switch для столбца «sort_column», чтобы определить критерии сортировки: 0 для "name", 1 для "buys", 2 для "sells", 3 для "trades", 4 для "lots", 5 для "profit", 6 для "pending", 7 для "swaps" или 8 для "comm", устанавливая "swap" в значение true, если сравнение (на основе "sort_ascending") указывает на необходимость переупорядочивания.

Если "swap" истинно, сохраняем "symbol_data[j]" во временной переменной "SymbolData", меняем местами "symbol_data[j]" и "symbol_data[j + 1]" и завершаем обмен. Данная реализация пузырьковой сортировки позволяет сортировать панель по любому столбцу в порядке возрастания или убывания, что повышает наглядность данных. Теперь можно реализовать эту функцию в главной функции, чтобы она обрабатывала обновления.

//+------------------------------------------------------------------+
//| Update dashboard function                                        |
//+------------------------------------------------------------------+
void UpdateDashboard() {
   bool needs_redraw = false;             //--- Initialize redraw flag
   CollectActiveSymbols();
   int current_num = ArraySize(symbol_data);
   if(current_num != prev_num_symbols) {
      // Delete old symbol and data labels
      for(int del_i = 0; del_i < prev_num_symbols; del_i++) {
         ObjectDelete(0, PREFIX + SYMB + IntegerToString(del_i));
         for(int del_j = 0; del_j < 8; del_j++) {
            ObjectDelete(0, PREFIX + DATA + IntegerToString(del_i) + "_" + IntegerToString(del_j));
         }
      }
      // Adjust panel sizes and positions
      int panel_height = (current_num + 3) * settings.row_height;
      ObjectSetInteger(0, PREFIX + PANEL, OBJPROP_YSIZE, panel_height);
      int footer_y = settings.panel_y + panel_height - settings.row_height - 5;
      ObjectSetInteger(0, PREFIX + FOOTER_PANEL, OBJPROP_YDISTANCE, footer_y);
      int account_panel_y = footer_y + settings.row_height + 10;
      ObjectSetInteger(0, PREFIX + ACCOUNT_PANEL, OBJPROP_YDISTANCE, account_panel_y);
      // Create new symbol and data labels
      int header_y = settings.panel_y + 8 + settings.label_y_offset;
      int first_row_y = header_y + settings.row_height;
      int symbol_x = settings.panel_x + 10 + settings.label_x_offset;
      for(int cr_i = 0; cr_i < current_num; cr_i++) {
         string symb_name = PREFIX + SYMB + IntegerToString(cr_i);
         createLABEL(symb_name, symbol_data[cr_i].name, symbol_x, first_row_y + cr_i * settings.row_height + settings.label_y_offset, settings.text_color, settings.font_size, settings.font, ANCHOR_LEFT);
         int x_offset = settings.panel_x + 10 + column_widths[0] + settings.label_x_offset;
         for(int cr_j = 0; cr_j < 8; cr_j++) {
            string data_name = PREFIX + DATA + IntegerToString(cr_i) + "_" + IntegerToString(cr_j);
            color init_color = data_default_colors[cr_j];
            string init_txt = (cr_j <= 2 || cr_j == 5) ? "0" : "0.00";
            createLABEL(data_name, init_txt, x_offset, first_row_y + cr_i * settings.row_height + settings.label_y_offset, init_color, settings.font_size, settings.font, ANCHOR_RIGHT);
            x_offset += column_widths[cr_j + 1];
         }
      }
      prev_num_symbols = current_num;
      needs_redraw = true;
   }
   // Reset totals
   totalBuys = 0;
   totalSells = 0;
   totalTrades = 0;
   totalLots = 0.0;
   totalProfit = 0.0;
   totalPending = 0;
   totalSwap = 0.0;
   totalComm = 0.0;
   // Calculate symbol data and totals (without updating labels yet)
   for(int i = 0; i < current_num; i++) {
      string symbol = symbol_data[i].name;
      for(int j = 0; j < 8; j++) {
         string value = "";
         color data_color = data_default_colors[j];
         double dval = 0.0;
         int ival = 0;
         switch(j) {
            case 0: // Buy positions
               value = countPositions(symbol, POSITION_TYPE_BUY);
               ival = (int)StringToInteger(value);
               if(value != symbol_data[i].buys_str) {
                  symbol_data[i].buys_str = value;
                  symbol_data[i].buys = ival;
               }
               totalBuys += ival;
               break;
            case 1: // Sell positions
               value = countPositions(symbol, POSITION_TYPE_SELL);
               ival = (int)StringToInteger(value);
               if(value != symbol_data[i].sells_str) {
                  symbol_data[i].sells_str = value;
                  symbol_data[i].sells = ival;
               }
               totalSells += ival;
               break;
            case 2: // Total trades
               value = countPositionsTotal(symbol);
               ival = (int)StringToInteger(value);
               if(value != symbol_data[i].trades_str) {
                  symbol_data[i].trades_str = value;
                  symbol_data[i].trades = ival;
               }
               totalTrades += ival;
               break;
            case 3: // Lots
               value = sumPositionDouble(symbol, POSITION_VOLUME);
               dval = StringToDouble(value);
               if(value != symbol_data[i].lots_str) {
                  symbol_data[i].lots_str = value;
                  symbol_data[i].lots = dval;
               }
               totalLots += dval;
               break;
            case 4: // Profit
               value = sumPositionDouble(symbol, POSITION_PROFIT);
               dval = StringToDouble(value);
               data_color = (dval > 0) ? clrGreen : (dval < 0) ? clrRed : clrGray;
               if(value != symbol_data[i].profit_str) {
                  symbol_data[i].profit_str = value;
                  symbol_data[i].profit = dval;
               }
               totalProfit += dval;
               break;
            case 5: // Pending
               value = countOrders(symbol);
               ival = (int)StringToInteger(value);
               if(value != symbol_data[i].pending_str) {
                  symbol_data[i].pending_str = value;
                  symbol_data[i].pending = ival;
               }
               totalPending += ival;
               break;
            case 6: // Swap
               value = sumPositionDouble(symbol, POSITION_SWAP);
               dval = StringToDouble(value);
               data_color = (dval > 0) ? clrGreen : (dval < 0) ? clrRed : data_color;
               if(value != symbol_data[i].swaps_str) {
                  symbol_data[i].swaps_str = value;
                  symbol_data[i].swaps = dval;
               }
               totalSwap += dval;
               break;
            case 7: // Comm
               dval = sumPositionCommission(symbol);
               value = DoubleToString(dval, 2);
               data_color = (dval > 0) ? clrGreen : (dval < 0) ? clrRed : data_color;
               if(value != symbol_data[i].comm_str) {
                  symbol_data[i].comm_str = value;
                  symbol_data[i].comm = dval;
               }
               totalComm += dval;
               break;
         }
      }
   }
   // Sort after calculating values
   SortDashboard();
}

Здесь мы реализуем функцию "UpdateDashboard" для обновления панели, обеспечивая обновление данных о позиции и счёте в режиме реального времени и динамическую корректировку изменений символов. Инициализируем переменную "needs_redraw" значением false и вызываем функцию "CollectActiveSymbols" для обновления переменной "symbol_data", проверяя, отличается ли значение "current_num" из "ArraySize(symbol_data)" от "prev_num_symbols". Если значения отличаются, удаляем старые метки с помощью ObjectDelete для меток "PREFIX + SYMB" и "PREFIX + DATA", корректируем размеры панелей, устанавливая "OBJPROP_YSIZE" для "PREFIX + PANEL" равным "(current_num + 3) * settings.row_height", обновляем "OBJPROP_YDISTANCE" для "PREFIX + FOOTER_PANEL" и "PREFIX + ACCOUNT_PANEL" на основе новых значений "footer_y" и "account_panel_y", и создаем заново метки символов и данных с помощью "createLABEL" для "symbol_data[cr_i].name" и начальных значений, используя "symbol_x", "first_row_y + cr_i * settings.row_height + settings.label_y_offset" и "x_offset", увеличенное на "column_widths".

Устанавливаем значение параметра "prev_num_symbols" равным "current_num" и помечаем параметр "needs_redraw" как true. Мы обнулили итоговые значения таких показателей, как "totalBuys", "totalSells", "totalTrades", "totalLots", "totalProfit", "totalPending", "totalSwap" и "totalComm". Для каждого символа в "symbol_data" перебираем восемь столбцов данных, вычисляя значения с помощью "countPositions", "countPositionsTotal", "sumPositionDouble" или "sumPositionCommission", обновляя поля "symbol_data[i]", такие как "buys_str", "sells", "profit_str", и устанавливая цвета для "profit", "swaps" и "comm" в зависимости от положительных (зеленый), отрицательных (красный) или нейтральных значений, а также добавляя их к итоговым значениям. Вызываем функцию "SortDashboard", чтобы изменить порядок отображения в зависимости от текущих значений "sort_column" и "sort_ascending". Для использования этой функции нам потребуется вызвать её в функциях инициализации и таймера. Просто добавим её в конец обработчика OnInit. 

// Set millisecond timer for updates
EventSetMillisecondTimer(MathMax(UpdateIntervalMs, 10)); //--- Set timer with minimum 10ms

// Initial update
prev_num_symbols = num_rows;
UpdateDashboard();                                       //--- Update dashboard

Первое, что мы делаем, это устанавливаем предыдущее количество рядов на количество вычисленных рядов для инициализации и вызываем функцию "UpdateDashboard" для обновления панели. Поскольку нам нужно будет вызвать ту же функцию в функции OnTimer, устанавливаем время таймера с помощью функции EventSetMillisecondTimer с интервалом обновления не менее 10 миллисекунд, чтобы не перегружать системные ресурсы. Поскольку мы создаем таймер, для высвобождения ресурсов не забудьте уничтожить его, когда он больше не понадобится.

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   ObjectsDeleteAll(0, PREFIX, -1, -1);  //--- Delete all objects with PREFIX
   EventKillTimer();                     //--- Stop timer
}

В обработчике OnDeinit используем ObjectsDeleteAll для удаления всех объектов с "PREFIX" и остановки таймера с помощью функции EventKillTimer. Теперь можно вызвать функцию обновления в обработчике "OnTimer", чтобы выполнить обновления следующим образом.

//+------------------------------------------------------------------+
//| Timer function for millisecond-based updates                     |
//+------------------------------------------------------------------+
void OnTimer() {
   UpdateDashboard();                    //--- Update dashboard on timer event
}

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

//+------------------------------------------------------------------+
//| Chart event handler for sorting and export                       |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) {
   if(id == CHARTEVENT_OBJECT_CLICK) {              //--- Handle object click event
      for(int i = 0; i < ArraySize(headers); i++) { //--- Iterate through headers
         if(sparam == PREFIX + HEADER + IntegerToString(i)) { //--- Check if header clicked
            if(sort_column == i)                    //--- Check if same column clicked
               sort_ascending = !sort_ascending;    //--- Toggle sort direction
            else {
               sort_column = i;                     //--- Set new sort column
               sort_ascending = true;               //--- Set to ascending
            }
            UpdateDashboard();                      //--- Update dashboard display
            break;                                  //--- Exit loop
         }
      }
   }
}

Реализуем обработчик OnChartEvent для обработки пользовательских взаимодействий при сортировке панели, повышая ее интерактивность. Для CHARTEVENT_OBJECT_CLICK перебираем "headers" с помощью ArraySize и проверяем, совпадает ли "sparam" с "PREFIX + HEADER + IntegerToString(i)". Если индекс выбранного заголовка равен "sort_column", переключаем "sort_ascending"; в противном случае присваиваем "sort_column" значение выбранного индекса, а "sort_ascending" - значение true. Вызываем "UpdateDashboard", чтобы обновить отображение с помощью новой сортировки и разорвать цикл. Это позволит выполнять динамическую сортировку нажатием кнопки мыши по заголовкам столбцов, что сделает анализ данных более гибким. После компиляции получаем следующий результат.

STATIC DASHBOARD

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

// Update header breathing effect every 500ms
glow_counter += MathMax(UpdateIntervalMs, 10); //--- Increment glow counter
if(glow_counter >= GLOW_INTERVAL_MS) { //--- Check if glow interval reached
   if(glow_direction) {                //--- Check if glowing forward
      glow_index++;                    //--- Increment glow index
      if(glow_index >= ArraySize(settings.header_shades) - 1) //--- Check if at end
         glow_direction = false;       //--- Reverse glow direction
   } else {                            //--- Glow backward
      glow_index--;                    //--- Decrement glow index
      if(glow_index <= 0)              //--- Check if at start
         glow_direction = true;        //--- Reverse glow direction
   }
   glow_counter = 0;                   //--- Reset glow counter
}
color header_shade = settings.header_shades[glow_index];          //--- Get current header shade
for(int i = 0; i < ArraySize(headers); i++) {                     //--- Iterate through headers
   string header_name = PREFIX + HEADER + IntegerToString(i);     //--- Define header name
   ObjectSetInteger(0, header_name, OBJPROP_COLOR, header_shade); //--- Update header color
   needs_redraw = true;                //--- Set redraw flag
}


// Batch redraw if needed
if(needs_redraw) {                     //--- Check if redraw needed
   ChartRedraw(0);                     //--- Redraw chart
}

Здесь мы реализуем эффект свечения заголовка и финальную перерисовку в функции "UpdateDashboard" для улучшения визуальной обратной связи. Увеличиваем значение "glow_counter" максимум на "UpdateIntervalMs" и 10, проверяя, достигает ли оно значения "GLOW_INTERVAL_MS" (500 мс). Если true, корректируем "glow_index": увеличиваем, если "glow_direction" имеет значение true, меняем значение на false при достижении конца "settings.header_shades", или уменьшаем, если значение равно false, меняем значение на true при нулевом значении, затем сбрасываем значение "glow_counter" на 0. Устанавливаем "header_shade" из "settings.header_shades[glow_index]" и перебираем "headers" с помощью ArraySize, обновляя для каждой метки "PREFIX + HEADER + IntegerToString(i)" значение "OBJPROP_COLOR" в "header_shade" с помощью ObjectSetInteger, устанавливая для "needs_redraw" значение true.

Если значение "needs_redraw" равно true, вызываем ChartRedraw для обновления графика. Это создает эффект циклического свечения заголовков и обеспечивает эффективное обновление пользовательского интерфейса. Можно свободно менять цвета в соответствии с вашими предпочтениями, их цикличность, частоту, а также степень прозрачности. Получим такие результаты.

BREATHING HEADER

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

// Update symbol and data labels after sorting
bool labels_updated = false;
for(int i = 0; i < current_num; i++) {
   string symbol = symbol_data[i].name;
   string symb_name = PREFIX + SYMB + IntegerToString(i);
   string current_symb_txt = ObjectGetString(0, symb_name, OBJPROP_TEXT);
   if(current_symb_txt != symbol) {
      ObjectSetString(0, symb_name, OBJPROP_TEXT, symbol);
      labels_updated = true;
   }
   for(int j = 0; j < 8; j++) {
      string data_name = PREFIX + DATA + IntegerToString(i) + "_" + IntegerToString(j);
      string value;
      color data_color = data_default_colors[j];
      switch(j) {
         case 0:
            value = symbol_data[i].buys_str;
            data_color = clrRed;
            break;
         case 1:
            value = symbol_data[i].sells_str;
            data_color = clrGreen;
            break;
         case 2:
            value = symbol_data[i].trades_str;
            data_color = clrDarkGray;
            break;
         case 3:
            value = symbol_data[i].lots_str;
            data_color = clrOrange;
            break;
         case 4:
            value = symbol_data[i].profit_str;
            data_color = (symbol_data[i].profit > 0) ? clrGreen : (symbol_data[i].profit < 0) ? clrRed : clrGray;
            break;
         case 5:
            value = symbol_data[i].pending_str;
            data_color = clrBlue;
            break;
         case 6:
            value = symbol_data[i].swaps_str;
            data_color = (symbol_data[i].swaps > 0) ? clrGreen : (symbol_data[i].swaps < 0) ? clrRed : clrPurple;
            break;
         case 7:
            value = symbol_data[i].comm_str;
            data_color = (symbol_data[i].comm > 0) ? clrGreen : (symbol_data[i].comm < 0) ? clrRed : clrBrown;
            break;
      }
      if(updateLABEL(data_name, value, data_color)) labels_updated = true;
   }
}
if(labels_updated) needs_redraw = true;
// Update totals
string new_total_buys = IntegerToString(totalBuys); //--- Format total buys
if(new_total_buys != total_buys_str) { //--- Check if changed
   total_buys_str = new_total_buys;    //--- Update string
   if(updateLABEL(PREFIX + FOOTER_DATA + "0", new_total_buys, clrRed)) needs_redraw = true; //--- Update label
}
string new_total_sells = IntegerToString(totalSells); //--- Format total sells
if(new_total_sells != total_sells_str) { //--- Check if changed
   total_sells_str = new_total_sells;  //--- Update string
   if(updateLABEL(PREFIX + FOOTER_DATA + "1", new_total_sells, clrGreen)) needs_redraw = true; //--- Update label
}
string new_total_trades = IntegerToString(totalTrades); //--- Format total trades
if(new_total_trades != total_trades_str) { //--- Check if changed
   total_trades_str = new_total_trades; //--- Update string
   if(updateLABEL(PREFIX + FOOTER_DATA + "2", new_total_trades, clrDarkGray)) needs_redraw = true; //--- Update label
}
string new_total_lots = DoubleToString(totalLots, 2); //--- Format total lots
if(new_total_lots != total_lots_str) { //--- Check if changed
   total_lots_str = new_total_lots;    //--- Update string
   if(updateLABEL(PREFIX + FOOTER_DATA + "3", new_total_lots, clrOrange)) needs_redraw = true; //--- Update label
}
string new_total_profit = DoubleToString(totalProfit, 2); //--- Format total profit
color total_profit_color = (totalProfit > 0) ? clrGreen : (totalProfit < 0) ? clrRed : clrGray; //--- Set color
if(new_total_profit != total_profit_str) { //--- Check if changed
   total_profit_str = new_total_profit; //--- Update string
   if(updateLABEL(PREFIX + FOOTER_DATA + "4", new_total_profit, total_profit_color)) needs_redraw = true; //--- Update label
}
string new_total_pending = IntegerToString(totalPending); //--- Format total pending
if(new_total_pending != total_pending_str) { //--- Check if changed
   total_pending_str = new_total_pending; //--- Update string
   if(updateLABEL(PREFIX + FOOTER_DATA + "5", new_total_pending, clrBlue)) needs_redraw = true; //--- Update label
}
string new_total_swap = DoubleToString(totalSwap, 2); //--- Format total swap
color total_swap_color = (totalSwap > 0) ? clrGreen : (totalSwap < 0) ? clrRed : clrPurple; //--- Set color
if(new_total_swap != total_swap_str) { //--- Check if changed
   total_swap_str = new_total_swap;    //--- Update string
   if(updateLABEL(PREFIX + FOOTER_DATA + "6", new_total_swap, total_swap_color)) needs_redraw = true; //--- Update label
}
string new_total_comm = DoubleToString(totalComm, 2); //--- Format total comm
color total_comm_color = (totalComm > 0) ? clrGreen : (totalComm < 0) ? clrRed : clrBrown; //--- Set color
if(new_total_comm != total_comm_str) { //--- Check if changed
   total_comm_str = new_total_comm;    //--- Update string
   if(updateLABEL(PREFIX + FOOTER_DATA + "7", new_total_comm, total_comm_color)) needs_redraw = true; //--- Update label
}

// Update account info
double balance = AccountInfoDouble(ACCOUNT_BALANCE); //--- Get balance
double equity = AccountInfoDouble(ACCOUNT_EQUITY); //--- Get equity
double free_margin = AccountInfoDouble(ACCOUNT_MARGIN_FREE); //--- Get free margin
string new_bal = DoubleToString(balance, 2); //--- Format balance
if(new_bal != acc_bal_str) {          //--- Check if changed
   acc_bal_str = new_bal;             //--- Update string
   if(updateLABEL(PREFIX + ACC_DATA + "0", new_bal, clrBlack)) needs_redraw = true; //--- Update label
}
string new_eq = DoubleToString(equity, 2); //--- Format equity
color eq_color = (equity > balance) ? clrGreen : (equity < balance) ? clrRed : clrBlack; //--- Set color
if(new_eq != acc_eq_str) {            //--- Check if changed
   acc_eq_str = new_eq;               //--- Update string
   if(updateLABEL(PREFIX + ACC_DATA + "1", new_eq, eq_color)) needs_redraw = true; //--- Update label
}
string new_free = DoubleToString(free_margin, 2); //--- Format free margin
if(new_free != acc_free_str) {        //--- Check if changed
   acc_free_str = new_free;           //--- Update string
   if(updateLABEL(PREFIX + ACC_DATA + "2", new_free, clrBlack)) needs_redraw = true; //--- Update label
}

В функции "UpdateDashboard" обновляем метки символов, данных, итогов и счетов, чтобы отразить отсортированные и текущие торговые данные, обеспечивая адаптивное отображение. Устанавливаем для "labels_updated" значение false и перебираем символы "current_num", обновляя "PREFIX + SYMB + IntegerToString(i)" с "symbol_data[i].name" через "ObjectSetString", если ObjectGetString отличается, устанавливая для "labels_updated" значение true. Для каждого символа перебираем восемь столбцов, выбирая "value" и "data_color" с помощью переключателя: "buys_str" с "clrRed", "sells_str" с "clrGreen", "trades_str" с "clrDarkGray", "lots_str" с clrOrange, "profit_str" с условным цветом на основе "symbol_data[i].profit", "pending_str" с "clrBlue", "swaps_str" с условным цветом на основе "symbol_data[i].swaps" и "comm_str" с условным цветом на основе "symbol_data[i].comm", обновляя метки "PREFIX + DATA" с помощью "updateLABEL" и устанавливая "labels_updated", если они изменились.

Обновляем итоговые значения, такие как "total_buys_str" с помощью "IntegerToString(totalBuys)", "total_sells_str", "total_trades_str", "total_lots_str" с помощью "DoubleToString(totalLots, 2)", "total_profit_str" с помощью условного значения "total_profit_color", "total_pending_str", "total_swap_str" с помощью "total_swap_color" и "total_comm_str" с помощью "total_comm_color", используя функцию "updateLABEL" для меток "PREFIX + FOOTER_DATA" и устанавливая значение "needs_redraw" в случае обновления. Для получения информации о счете получаем значения "balance", "equity" и "free_margin" с помощью AccountInfoDouble, форматируем их с помощью "DoubleToString", обновляем "acc_bal_str", "acc_eq_str" с помощью условного "eq_color" и "acc_free_str", используя "updateLABEL" для меток "PREFIX + ACC_DATA". Это гарантирует, что на панели текущие данные будут отображаться с использованием динамических цветов для большей наглядности. После компиляции получаем следующий результат.

IP DYNAMIC SORTING

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

//+------------------------------------------------------------------+
//| Export dashboard data to CSV                                     |
//+------------------------------------------------------------------+
void ExportToCSV() {
   string time_str = TimeToString(TimeCurrent(), TIME_DATE|TIME_MINUTES); //--- Get current time string
   StringReplace(time_str, " ", "_"); //--- Replace spaces
   StringReplace(time_str, ":", "-"); //--- Replace colons
   string filename = "Dashboard_" + time_str + ".csv"; //--- Define filename
   int handle = FileOpen(filename, FILE_WRITE|FILE_CSV); //--- Open CSV file in terminal's Files folder
   if(handle == INVALID_HANDLE) {        //--- Check for invalid handle
      Print("Failed to open CSV file '", filename, "'. Error code = ", GetLastError()); //--- Log error
      return;                            //--- Exit function
   }
   FileWrite(handle, "Symbol,Buy Positions,Sell Positions,Total Trades,Lots,Profit,Pending Orders,Swap,Comm"); //--- Write header
   for(int i = 0; i < ArraySize(symbol_data); i++) { //--- Iterate through symbols
      FileWrite(handle, symbol_data[i].name, symbol_data[i].buys, symbol_data[i].sells, symbol_data[i].trades, symbol_data[i].lots, symbol_data[i].profit, symbol_data[i].pending, symbol_data[i].swaps, symbol_data[i].comm); //--- Write symbol data
   }
   FileWrite(handle, "Total", totalBuys, totalSells, totalTrades, totalLots, totalProfit, totalPending, totalSwap, totalComm); //--- Write totals
   FileClose(handle);                    //--- Close file
   Print("Dashboard data exported to CSV: ", filename); //--- Log export success
}

Мы реализуем функцию "ExportToCSV" для экспорта данных, что позволяет сохранять торговые данные для анализа в автономном режиме. Создаем "time_str" с помощью TimeToString, используя TimeCurrent и "TIME_DATE|TIME_MINUTES", заменяя пробелы символами подчеркивания, а двоеточия дефисами с помощью StringReplace для получения четкого имени файла, затем определяем "filename" как "Dashboard_" плюс "time_str" плюс ".csv". Для этого можно использовать любое другое разрешенное расширение. Мы выбрали CSV, поскольку это наиболее распространенное расширение. Затем открываем файл с помощью FileOpen, используя параметры "FILE_WRITE|FILE_CSV", выводим сообщения об ошибках с помощью "Print" и завершаем работу, если "handle" равно "INVALID_HANDLE".

Записываем строку заголовков с помощью FileWrite с отображением названий столбцов, затем проходим циклом по "symbol_data" с параметром "ArraySize", чтобы записать "name", "buys", "sells", "trades", "lots", "profit", "pending", "swaps" и "comm" для каждого символа, и записываем строку итоговых значений с помощью "Total" и соответствующими значениями "totalBuys", "totalSells", "totalTrades", "totalLots", "totalProfit", "totalPending", "totalSwap" и "totalComm". Закрываем файл с помощью FileClose и выводим сообщение об успешном выполнении с помощью "Print". Это обеспечивает удобный экспорт в формате CSV для ведения учета. Затем мы можем использовать эту функцию в обработчике событий графика при нажатии клавиши 'E'. Мы выбрали клавишу таким образом, чтобы её было легко вспомнить как 'Export', но вы можете использовать любую клавишу по вашему выбору. У вас может быть кнопка для работы с экспортом, которую мы ранее в достаточной мере не применяли. Для этого мы применили следующую логику.

//+------------------------------------------------------------------+
//| Chart event handler for sorting and export                       |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) {
   if(id == CHARTEVENT_OBJECT_CLICK) {   //--- Handle object click event
      for(int i = 0; i < ArraySize(headers); i++) { //--- Iterate through headers
         if(sparam == PREFIX + HEADER + IntegerToString(i)) { //--- Check if header clicked
            if(sort_column == i)         //--- Check if same column clicked
               sort_ascending = !sort_ascending; //--- Toggle sort direction
            else {
               sort_column = i;          //--- Set new sort column
               sort_ascending = true;    //--- Set to ascending
            }
            UpdateDashboard();           //--- Update dashboard display
            break;                       //--- Exit loop
         }
      }
   }
   else if(id == CHARTEVENT_KEYDOWN && lparam == 'E') { //--- Handle 'E' key press
      ExportToCSV();                    //--- Export data to CSV
   }
}

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

IP EXPORT CSV

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


Тестирование на истории

Мы провели тестирование, а ниже представлена скомпилированная визуализация в едином формате растрового изображения Graphics Interchange Format (GIF).

IP BACKTEST



Заключение

В заключение, мы создали информационную панель на MetaQuotes Language 5, которая отслеживает позиции по нескольким символам и показатели счета, такие как "Balance", "Equity" и "Free Margin", с возможностью сортировки столбцов и экспорта в Excel в формате CSV для удобного мониторинга торговли. Мы подробно описали архитектуру и реализацию, используя такие структуры, как "SymbolData", и такие функции, как "SortDashboard", для получения организованной информации в режиме реального времени. Вы можете настроить эту панель в соответствии со своими торговыми потребностями, повысив возможность эффективно отслеживать результаты торговли. 

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

Прикрепленные файлы |
Возможности Мастера MQL5, которые вам нужно знать (Часть 68): Использование паттернов TRIX и процентного диапазона Уильямса с сетью косинусного ядра Возможности Мастера MQL5, которые вам нужно знать (Часть 68): Использование паттернов TRIX и процентного диапазона Уильямса с сетью косинусного ядра
В продолжение нашей предыдущей статьи, где мы представили пару индикаторов TRIX и процентного диапазона Уильямса, мы рассмотрим, как эту пару индикаторов можно расширить с помощью машинного обучения. TRIX и процентный диапазон Уильямса представляют собой взаимодополняющую пару, отражающую тренд и уровни поддержки/сопротивления. Наш подход на основе машинного обучения использует сверточную нейронную сеть (convolution neural network), в архитектуре которой задействуется косинусное ядро (cosine kernel) при точной настройке прогнозов этой пары индикаторов. Как обычно, это делается в пользовательском файле класса сигналов (signal class), который взаимодействует с Мастером MQL5 для создания советника.
Нейросети в трейдинге: Единая архитектура взаимодействия рыночных признаков и торгового контекста (OneTrans) Нейросети в трейдинге: Единая архитектура взаимодействия рыночных признаков и торгового контекста (OneTrans)
В статье рассматривается архитектура фреймворка OneTrans, предложенного для эффективной работы с длинными последовательностями событий, и анализируются ключевые инженерные решения, лежащие в его основе. Особое внимание уделяется механизмам оптимизации вычислений внимания — пирамидальной схеме обработки токенов, использованию кэширования Key/Value и современных алгоритмов ускорения внимания, таких как FlashAttention-2.
Как подключить LLM к советнику MQL5 через Python-сервер Как подключить LLM к советнику MQL5 через Python-сервер
В статье разобраны три ключевые преграды интеграции LLM с MetaTrader 5: отсутствие прямого доступа, жёсткие rate limits и безопасность API‑ключей при архитектурных ограничениях MQL5. Предложена схема с локальным Python‑сервером как мостом между советником и OpenRouter. Рассматриваются WebSocket и fallback на TCP, хранение ключа на сервере, пакетная обработка нескольких символов и формирование технического промпта. Читатель получит готовую архитектуру, снижающую задержки и издержки.
Автоматизация торговых стратегий на MQL5 (Часть 18): Envelopes Trend Bounce Scalping - Базовая инфраструктура и генерация сигналов (Часть I) Автоматизация торговых стратегий на MQL5 (Часть 18): Envelopes Trend Bounce Scalping - Базовая инфраструктура и генерация сигналов (Часть I)
В этой статье мы создадим основную инфраструктуру для советника Envelopes Trend Bounce Scalping (скальпинг на коррекции на основе конвертов) на MQL5. Мы инициализируем конверты и другие индикаторы для генерации сигналов. Также мы настроим тестирование стратегии на истории, чтобы подготовиться к исполнению сделок в следующей части.