English Русский Deutsch 日本語
preview
MQL5交易工具(第七部分):用于多品种持仓与账户监控的信息仪表盘

MQL5交易工具(第七部分):用于多品种持仓与账户监控的信息仪表盘

MetaTrader 5交易系统 |
17 0
Allan Munene Mutiiria
Allan Munene Mutiiria

引言

前一篇文章(第六部分)中,我们使用MetaQuotes Language 5(MQL5)开发了动态全息仪表盘,用于监控交易品种与时间周期,具备RSI指标、波动率警报,以及带脉冲动画的交互式控件。在第七部分,我们将打造一款信息仪表盘,用于追踪多品种持仓、总交易笔数、手数、盈亏、挂单、隔夜利息、手续费,以及余额、净值等账户指标,并支持列排序与 CSV(逗号分隔值)导出,以实现对多品种持仓和账户指标的全面总览。我们将涵盖以下主题:

  1. 了解信息仪表盘架构
  2. 在MQL5中的实现
  3. 回测
  4. 结论

到本文结束时,您将拥有一款功能强大的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"开头的账户数据标签。这样确保仪表盘实时显示最新数据,并通过动态颜色提升可读性与清晰度。编译后,我们得到以下结果:

IP动态排序

由可视化效果可见,我们现已实现动态冒泡排序效果,能够对所有表头列进行升序和降序双向排序,并实时反映数据变化。现在仅剩最后一项功能:编写一个函数,将数据导出至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,如果满足条件,则立即执行文件导出操作。这是一段简单的逻辑,因此我们用黄色高亮标注以便清晰识别。最终实现效果如下:

IP导出CSV

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


回测

我们已完成测试,以下是整合后的可视化结果,以单一的GIF动图形式呈现。

IP回测



结论

综上所述,我们使用MQL5创建了一款信息仪表盘,用于监控多品种持仓以及账户核心指标(包括“余额”、“净值”和“可用保证金”),同时支持列排序与Excel兼容的CSV导出功能,实现一站式交易监控。我们详细讲解了整体架构与实现流程,使用了"SymbolData"结构体以及"SortDashboard"等函数,提供实时、结构化的交易数据。您可以根据自身交易需求自定义这款仪表盘,从而提升高效跟踪交易表现的能力。 

本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/18986

附加的文件 |
图论:Dijkstra(迪杰斯特拉)算法在交易中的应用 图论:Dijkstra(迪杰斯特拉)算法在交易中的应用
Dijkstra(迪杰斯特拉)算法是图论中一种经典的最短路径解决方案,它可以通过对市场网络进行建模来优化交易策略。交易者可以利用它从 K 线图表数据中找到最高效的路线。
价格行为分析工具包开发(第 35 部分):预测模型训练与部署 价格行为分析工具包开发(第 35 部分):预测模型训练与部署
历史行情数据绝非 “无用糟粕”,而是所有稳健市场分析的根基。本文将带您循序渐进,从历史数据采集入手,利用数据训练预测模型,最终完成模型部署,实盘价格预测落地应用。继续往下阅读,掌握完整实现流程!
新手在交易中的10个基本错误 新手在交易中的10个基本错误
新手在交易中会犯的10个基本错误: 在市场刚开始时交易, 获利时不适当地仓促, 在损失的时候追加投资, 从最好的仓位开始平仓, 翻本心理, 最优越的仓位, 用永远买进的规则进行交易, 在第一天就平掉获利的仓位,当发出建一个相反的仓位警示时平仓, 犹豫。
MQL5 MVC 范式下表格的视图与控制器组件:容器 MQL5 MVC 范式下表格的视图与控制器组件:容器
在本文中,我们将讨论如何创建一个支持内容滚动的“容器”控件。在此过程中,将对已实现的图形库控件类进行改进。