MQL5交易工具(第七部分):用于多品种持仓与账户监控的信息仪表盘
引言
在前一篇文章(第六部分)中,我们使用MetaQuotes Language 5(MQL5)开发了动态全息仪表盘,用于监控交易品种与时间周期,具备RSI指标、波动率警报,以及带脉冲动画的交互式控件。在第七部分,我们将打造一款信息仪表盘,用于追踪多品种持仓、总交易笔数、手数、盈亏、挂单、隔夜利息、手续费,以及余额、净值等账户指标,并支持列排序与 CSV(逗号分隔值)导出,以实现对多品种持仓和账户指标的全面总览。我们将涵盖以下主题:
到本文结束时,您将拥有一款功能强大的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以监控所有持仓和订单;若指定具体数值,则可按EA的magic number进行筛选,实现针对性追踪。
我们使用定义来统一对象的命名规则:将"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";用于界面展示的对应的字符串字段(如"buys_str")。我们声明以下全局变量:"symbol_data"数组用于存储各品种数据;"totalBuys"、"totalSells"、"totalTrades"、"totalPending"为长整型全局统计值,初始化为0;"totalLots"、"totalProfit"、"totalSwap"、"totalComm"为浮点型全局统计值,初始化为0;"headers"数组为表头列名称;"column_widths"用于设置列宽;"data_default_colors"为各列专属颜色;"sort_column"默认按第3列(交易笔数)排序;"sort_ascending"默认降序排列;"glow_index"、"glow_counter" 、"glow_direction"为表头发光动画控制变量;"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”获取Z序)。此外,使用ObjectSetString函数设置“OBJPROP_TEXT”和OBJPROP_FONT,并通过"OBJPROP_TOOLTIP"为标签添加用于排序或显示数据的工具提示。使用ChartRedraw重绘图表,并返回true。
"updateLABEL"函数用于更新已存在的标签,通过ObjectFind函数查找指定名称的标签对象,如果未找到则记录日志并返回false。如果通过"ObjectGetString"获取的"current_txt"与"txt"不一致,则通过"ObjectSetString"和"ObjectSetInteger"更新"OBJPROP_TEXT"与"OBJPROP_COLOR",并返回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,通过PositionGetTicket获取每笔持仓的"ticket",并使用PositionSelectByTicket选中该持仓,如果交易品种匹配,且满足"MagicNumber"为-1(监控所有持仓)或通过"PositionGetInteger"获取的POSITION_MAGIC与设定值一致,则总持仓计数加1。再通过IntegerToString函数将统计结果转换为字符串并返回。
"countPositions"函数用于统计指定"symbol"和“pos_type”的多单或空单持仓数量,遍历全部持仓并校验POSITION_TYPE 是否与指定类型"pos_type"一致,最终以字符串形式返回统计结果。"countOrders"函数用于统计指定交易品种的挂单数量,通过遍历OrdersTotal,使用"OrderGetTicket"获取订单编号并通过OrderSelect选中"ticket",如果交易品种与"MagicNumber"匹配,则计数加1,最终以字符串形式返回统计结果。"sumPositionDouble"函数用于汇总指定"symbol"的持仓类双精度数值(如成交量、盈亏或隔夜利息),遍历全部持仓并在条件匹配时,累加PositionGetDouble获取的指定属性"prop",最终通过DoubleToString格式化为保留两位小数的字符串并返回。
"sumPositionCommission"函数用于从成交历史记录中计算指定"symbol"的总手续费:遍历所有持仓,通过"PositionGetInteger"选择"pos_id",使用HistorySelectByPosition筛选对应的成交记录,再通过 HistoryDealGetTicket获取每笔有效成交单号"deal_ticket",并利用"HistoryDealGetDouble"读取 "DEAL_COMMISSION"进行累加,最终返回总手续费数值。
"CollectActiveSymbols"函数将存在有效持仓或挂单的交易品种收集到"symbols_temp"中,通过遍历PositionsTotal和"OrdersTotal",校验"MagicNumber"筛选条件,并使用ArrayResize函数调整数组大小,添加不重复的交易品种。该函数会同步调整"symbol_data"的大小,并初始化"name"、各类计数、字符串等字段为0或默认值。这些函数能让仪表盘高效采集并展示精准的交易数据。至此,我们已具备初始化仪表盘所需的全部函数。接下来,我们将在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"数组中,并通过ArraySize函数获取数组长度,赋值给"num_rows"。再由"headers"数组计算出"num_columns",通过for循环遍历"column_widths"数组,累加所有列宽,计算得到"column_width_sum"。面板宽度"panel_width"通过MathMax函数计算确定:取"header_x_distances"最后一项 + 对应"column_widths",与"column_width_sum"对比取大值,再额外增加20像素与"settings.label_x_offset"作为内边距。
我们使用ObjectCreate函数构建一个名为“PREFIX + PANEL”的主面板,其类型为OBJ_RECTANGLE_LABEL,并设置以下属性:将“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”计算出“header_y”的值,然后使用for循环遍历“num_columns”。在循环过程中,通过“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”,并且将“selectable”属性设置为true,以便后续启用排序功能时实现交互排序。
对于交易品种和数据部分,我们将“first_row_y”设置为“header_y + settings.row_height”,“symbol_x”设置为“settings.panel_x + 10 + settings.label_x_offset”。我们通过for循环遍历“num_rows”,在循环过程中使用“createLABEL”函数创建交易品种标签,标签名称采用“PREFIX + SYMB + IntegerToString(i)”的形式,文本内容为“symbol_data[i].name”,“x”坐标为“symbol_x”,“y”坐标为“first_row_y + i * settings.row_height + settings.label_y_offset”,将颜色设置为“settings.text_color”。对于每一行,我们再通过一个for循环遍历“num_columns - 1”个数据列,使用“createLABEL”函数创建数据标签,标签名称采用“PREFIX + DATA + IntegerToString(i) + '_' + IntegerToString(j)”的形式,初始文本根据列的不同设置为“0”或“0.00”,“x”坐标起始值为“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”计算出“footer_y”的值,然后使用“ObjectCreate”函数创建一个名为“PREFIX + FOOTER_PANEL”,类型为 OBJ_RECTANGLE_LABEL的页脚面板。将“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:”,其“x”坐标为“settings.panel_x + 10 + settings.label_x_offset”(即“footer_text_x”的位置)。接下来,通过循环遍历“num_columns - 1”次,使用“createLABEL”函数创建页脚数据标签,标签名称为“PREFIX + FOOTER_DATA + IntegerToString(j)”,初始文本根据需求设置,“x”坐标每次根据“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函数中创建账户面板及其标签,用于展示账户相关指标,从而完成UI的设置。我们通过公式“account_panel_y = footer_y + settings.row_height + 10”计算出“account_panel_y”的值,然后使用ObjectCreate函数创建一个名为“PREFIX + ACCOUNT_PANEL”、类型为“OBJ_RECTANGLE_LABEL”的面板。将“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,然后根据“sort_column”的值使用switch状态来确定排序依据:0对应“name”,1对应“buys”,2对应“sells”,3对应“trades”,4对应“lots”,5对应“profit”,6对应“pending”,7对应“swaps”,8 对应“comm”。如果根据“sort_ascending”的比较结果需要进行重新排序,则将“swap”标识设置为true。
如果“swap”标识为true,则将“symbol_data[j]”存储在一个临时的“SymbolData”变量中,交换“symbol_data[j]”与“symbol_data[j + 1]”的值,完成交换操作。这种冒泡排序的实现方式确保了仪表盘能够按照任意列以升序或降序进行排序,从而提高了数据的可读性。现在,我们可以在主函数中实现“SortDashboard”函数,以处理仪表盘的更新操作。
//+------------------------------------------------------------------+ //| 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”数组,检查“ArraySize(symbol_data)”返回的“current_num”是否与“prev_num_symbols”不同。如果数量不同,则执行以下操作:使用ObjectDelete删除所有旧标签,包括以“PREFIX + SYMB”和“PREFIX + DATA”为前缀的标签;将“PREFIX + PANEL”的“OBJPROP_YSIZE”设置为“(current_num + 3) * settings.row_height”,以适应新的交易品种数量;根据新的“footer_y”和“account_panel_y”值,调整“PREFIX + FOOTER_PANEL”和“PREFIX + ACCOUNT_PANEL”的“OBJPROP_YDISTANCE”属性;通过“createLABEL”函数为每个交易品种(“symbol_data[cr_i].name”)及其初始值创建标签,将标签的“x”坐标设为“symbol_x”,“y”坐标设为“first_row_y + cr_i * settings.row_height + settings.label_y_offset”,并基于“column_widths”数组通过递增“x_offset”定位数据列。
我们将“prev_num_symbols”设置为“current_num”,并将“needs_redraw”标识设置为true。再将各类汇总数据(如“totalBuys”“totalSells”“totalTrades”“totalLots”“totalProfit”“totalPending”“totalSwap”和“totalComm”)重置为0。对于“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事件时,我们通过ArraySize遍历所有"headers",并检查"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毫秒)。如果条件成立,则调整"glow_index":如果"glow_direction"为true,则索引递增,当到达"settings.header_shades"末尾时,反转方向为false;如果方向为false,则索引递减;当到达0时,反转方向为true,将"glow_counter"重置为0。我们从"settings.header_shades[glow_index]"中获取当前"header_shade",并通过 ArraySize获取长度遍历"headers",使用ObjectSetInteger将每个"PREFIX + HEADER + IntegerToString(i)"标签的 "OBJPROP_COLOR"更新为"header_shade",并将"needs_redraw"设置为true。
如果"needs_redraw"为 true,我们调用ChartRedraw函数刷新图表。这样会在表头实现循环发光效果,并确保UI高效更新。您可以根据个人喜好自由调整循环颜色、动画频率以及透明度。显示的效果如下:

至此,我们已经实现了呼吸灯效果表头,接下来可以进阶处理更复杂的逻辑 —— 为仪表盘添加点击交互功能。
// 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"个交易品种,如果通过ObjectGetString获取的文本与新文本不一致,则通过"ObjectSetString"更新"PREFIX + SYMB + IntegerToString(i)"标签为 "symbol_data[i].name",并将"labels_updated"设为true。针对每个交易品种,我们遍历8列数据,通过switch语句选择对应"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”的条件设置颜色。如果数据发生变更,则通过"updateLABEL"函数更新所有"PREFIX + DATA"开头的数据标签,并将 "labels_updated"设为true。
我们通过以下方式更新汇总数据:使用"IntegerToString(totalBuys)"更新 "total_buys_str"、"total_sells_str"、"total_trades_str";使用 "DoubleToString(totalLots, 2)"更新"total_lots_str";使用条件"total_profit_color"更新 "total_profit_str"、"total_pending_str";使用"total_swap_color"更新"total_swap_str";使用"total_comm_color"更新"total_comm_str",通过"updateLABEL"更新所有以"PREFIX + FOOTER_DATA"开头的底部数据标签,如果发生更新则将"needs_redraw"设为true。对于账户信息,我们使用AccountInfoDouble获取"balance"、"equity"和"free_margin",通过“DoubleToString”进行格式化,使用条件“eq_color”更新“acc_bal_str”、“acc_eq_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"函数以支持数据导出功能,方便保存交易数据用于离线分析。我们通过TimeCurrent获取当前时间,并使用TimeToString以"TIME_DATE|TIME_MINUTES"(日期 + 分钟)格式生成"time_str",再通过StringReplace函数将字符串中的空格替换为下划线、冒号替换为连字符,得到整洁的文件名,最终定义文件名为"Dashboard_ + time_str + .csv"。您也可以使用其他合法的文件扩展名。我们选择CSV是因为它是最通用的格式。接下来,我们使用FileOpen并指定"FILE_WRITE|FILE_CSV"模式打开文件,如果返回的"handle"为"INVALID_HANDLE",则通过"Print"打印错误信息并退出。
我们使用FileWrite函数写入表头行,列出所有列名称;通过"ArraySize"遍历"symbol_data",写入每个品种的 "name"、"buys"、"sells"、"trades"、"lots""profit"、"pending"、"swaps"和"comm",并写入一行汇总数据,包含 “Total”字样以及对应的"totalBuys"、"totalSells"、"totalTrades", "totalLots"、"totalProfit"、"totalPending"、"totalSwap"和"totalComm"。再使用FileClose函数关闭文件,并通过"Print"打印导出成功日志。这样就为数据记录提供了便捷的CSV导出功能。接下来,我们可以在图表事件处理器中,监听按下E键的操作并调用此函数。我们选择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 } }
这里,我们检测事件ID是否为CHARTEVENT_KEYDOWN,且按下的按键是否为E,如果满足条件,则立即执行文件导出操作。这是一段简单的逻辑,因此我们用黄色高亮标注以便清晰识别。最终实现效果如下:

由可视化效果中可见,我们会根据当前时间将数据导出到不同的文件中用于分析,如果当前时间(以分钟为精度基准)相同,文件会覆盖保存。如果您不想等待1分钟才能生成新文件,可以将当前时间格式由分钟精度修改为秒精度。总体而言,我们已经完成了所有预设的目标。当前仅需完成项目可操作性的测试工作,该部分内容已在前文章节中详细阐述。
回测
我们已完成测试,以下是整合后的可视化结果,以单一的GIF动图形式呈现。

结论
综上所述,我们使用MQL5创建了一款信息仪表盘,用于监控多品种持仓以及账户核心指标(包括“余额”、“净值”和“可用保证金”),同时支持列排序与Excel兼容的CSV导出功能,实现一站式交易监控。我们详细讲解了整体架构与实现流程,使用了"SymbolData"结构体以及"SortDashboard"等函数,提供实时、结构化的交易数据。您可以根据自身交易需求自定义这款仪表盘,从而提升高效跟踪交易表现的能力。
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/18986
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
图论:Dijkstra(迪杰斯特拉)算法在交易中的应用
价格行为分析工具包开发(第 35 部分):预测模型训练与部署
新手在交易中的10个基本错误