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

Реализация средствами 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", указывающее на успешную инициализацию. Это формирует основные элементы панели для отображения данных, и после компиляции получаем следующий результат.

Заложив основу, теперь можем создать остальные подпанели и метки. Для этого мы используем следующую логику.
// 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]", взятые из массива. При компилировании получаем следующий результат.

Теперь, когда основная панель заполнена данными, перейдем к панели показателей счёта, которая должна располагаться под основной панелью для динамического отображения данных счёта.
// 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)" для равномерного расстояния и следуем аналогичному формату, как и в логике создания основной панели. В этой конфигурации баланс, эквити и свободная маржа будут отображаться в аккуратной, выровненной панели под нижним колонтитулом. Смотрите ниже.

На изображении видно, что раздел показателей счёта создан. Теперь осталось лишь обновить панель и сделать её адаптивной. Давайте создадим функцию для обновления панели.
//+------------------------------------------------------------------+ //| 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", чтобы обновить отображение с помощью новой сортировки и разорвать цикл. Это позволит выполнять динамическую сортировку нажатием кнопки мыши по заголовкам столбцов, что сделает анализ данных более гибким. После компиляции получаем следующий результат.

На изображении видно, что при наведении курсора мыши появляются подсказки, которые подсказывают нам, что делать, например "Нажмите для сортировки", но при нажатии ничего не происходит. Эта информация даже не отображается. Причина этого заключается в том, что при сборе информации она не обновляется на панели визуально, но доступна внутренне. Поэтому давайте обновим нашу функцию "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 для обновления графика. Это создает эффект циклического свечения заголовков и обеспечивает эффективное обновление пользовательского интерфейса. Можно свободно менять цвета в соответствии с вашими предпочтениями, их цикличность, частоту, а также степень прозрачности. Получим такие результаты.

Теперь, когда у нас есть «дышащий» заголовок, можно перейти к более сложной логике, которая заключается в обновлении нашей панели мониторинга для обработки даже кликов.
// 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". Это гарантирует, что на панели текущие данные будут отображаться с использованием динамических цветов для большей наглядности. После компиляции получаем следующий результат.

На изображении видно, что теперь мы достигли эффекта динамической пузырьковой сортировки, отражающего все в наших столбцах с заголовками как в порядке возрастания, так и в порядке убывания. Теперь остается только добавить функцию экспорта данных в 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', и немедленно экспортируем файл. Это простая логика, поэтому мы выделили ее желтым цветом для наглядности. Вот что мы получаем в результате.

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

Заключение
В заключение, мы создали информационную панель на MetaQuotes Language 5, которая отслеживает позиции по нескольким символам и показатели счета, такие как "Balance", "Equity" и "Free Margin", с возможностью сортировки столбцов и экспорта в Excel в формате CSV для удобного мониторинга торговли. Мы подробно описали архитектуру и реализацию, используя такие структуры, как "SymbolData", и такие функции, как "SortDashboard", для получения организованной информации в режиме реального времени. Вы можете настроить эту панель в соответствии со своими торговыми потребностями, повысив возможность эффективно отслеживать результаты торговли.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/18986
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Возможности Мастера MQL5, которые вам нужно знать (Часть 68): Использование паттернов TRIX и процентного диапазона Уильямса с сетью косинусного ядра
Нейросети в трейдинге: Единая архитектура взаимодействия рыночных признаков и торгового контекста (OneTrans)
Как подключить LLM к советнику MQL5 через Python-сервер
Автоматизация торговых стратегий на MQL5 (Часть 18): Envelopes Trend Bounce Scalping - Базовая инфраструктура и генерация сигналов (Часть I)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования