English Deutsch 日本語
preview
MQL5交易工具(第三部分):构建用于策略交易的多时间周期扫描仪表盘

MQL5交易工具(第三部分):构建用于策略交易的多时间周期扫描仪表盘

MetaTrader 5交易 |
29 0
Allan Munene Mutiiria
Allan Munene Mutiiria

概述

先前第二部分中,我们为MetaQuotes Language 5(MQL5)中的交易助手工具增添了动态视觉反馈功能,以提升其交互性。现在,我们将聚焦于构建一个多时间周期扫描仪表盘,为策略决策提供实时交易信号。我们引入一个基于网格的界面,该界面包含由指标驱动的信号以及一个关闭按钮,并通过以下子主题阐述这些改进:

  1. 扫描仪表盘方案
  2. 在MQL5中的实现
  3. 回测
  4. 结论

这些章节将带领我们打造一个直观且功能强大的交易仪表盘。


扫描仪表盘方案

我们旨在构建一个多时间周期扫描仪表盘,提供清晰、实时的交易信号,从而提升策略决策能力。该仪表盘将采用网格布局,展示多个时间框架的买卖信号,使我们无需切换图表即可快速评估市场状况。此外,还将设置一个关闭按钮,以便轻松关闭面板,确保用户获得简洁、灵活的体验,满足我们的交易需求。

我们将整合相对强弱指数(RSI)、随机振荡指标(STOCH)、商品通道指数(CCI)、平均趋向指数(ADX)以及动量振荡指标(AO)等关键指标信号,这些指标旨在通过可自定义的强度阈值,帮助识别潜在的交易机会。然而,具体使用哪些指标或价格行为数据,由您自行决定。这种设置将帮助我们跨时间周期发现趋势和反转,同时支持短期和长期交易策略。我们的目标是打造一款精简、直观的工具,既能提供可操作的见解,又易于使用,为未来实现自动提醒或添加更多指标等功能升级铺平道路。以下是我们期望达成的效果图示。

实施计划


在MQL5中的实现

要在MQL5中创建该程序,我们需要先定义程序元数据,然后定义一些对象名称常量,这些常量将有助于我们引用仪表盘对象并轻松管理它们。

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

// Define identifiers and properties for UI elements
#define MAIN_PANEL              "PANEL_MAIN"                     //--- Main panel rectangle identifier
#define HEADER_PANEL            "PANEL_HEADER"                   //--- Header panel rectangle identifier
#define HEADER_PANEL_ICON       "PANEL_HEADER_ICON"              //--- Header icon label identifier
#define HEADER_PANEL_TEXT       "PANEL_HEADER_TEXT"              //--- Header title label identifier
#define CLOSE_BUTTON            "BUTTON_CLOSE"                   //--- Close button identifier
#define SYMBOL_RECTANGLE        "SYMBOL_HEADER"                  //--- Symbol rectangle identifier
#define SYMBOL_TEXT             "SYMBOL_TEXT"                    //--- Symbol text label identifier
#define TIMEFRAME_RECTANGLE     "TIMEFRAME_"                     //--- Timeframe rectangle prefix
#define TIMEFRAME_TEXT          "TIMEFRAME_TEXT_"                //--- Timeframe text label prefix
#define HEADER_RECTANGLE        "HEADER_"                        //--- Header rectangle prefix
#define HEADER_TEXT             "HEADER_TEXT_"                   //--- Header text label prefix
#define RSI_RECTANGLE           "RSI_"                           //--- RSI rectangle prefix
#define RSI_TEXT                "RSI_TEXT_"                      //--- RSI text label prefix
#define STOCH_RECTANGLE         "STOCH_"                         //--- Stochastic rectangle prefix
#define STOCH_TEXT              "STOCH_TEXT_"                    //--- Stochastic text label prefix
#define CCI_RECTANGLE           "CCI_"                           //--- CCI rectangle prefix
#define CCI_TEXT                "CCI_TEXT_"                      //--- CCI text label prefix
#define ADX_RECTANGLE           "ADX_"                           //--- ADX rectangle prefix
#define ADX_TEXT                "ADX_TEXT_"                      //--- ADX text label prefix
#define AO_RECTANGLE            "AO_"                            //--- AO rectangle prefix
#define AO_TEXT                 "AO_TEXT_"                       //--- AO text label prefix
#define BUY_RECTANGLE           "BUY_"                           //--- Buy rectangle prefix
#define BUY_TEXT                "BUY_TEXT_"                      //--- Buy text label prefix
#define SELL_RECTANGLE          "SELL_"                          //--- Sell rectangle prefix
#define SELL_TEXT               "SELL_TEXT_"                     //--- Sell text label prefix
#define WIDTH_TIMEFRAME         90                               //--- Width of timeframe and symbol rectangles
#define WIDTH_INDICATOR         70                               //--- Width of indicator rectangles
#define WIDTH_SIGNAL            90                               //--- Width of BUY/SELL signal rectangles
#define HEIGHT_RECTANGLE        25                               //--- Height of all rectangles
#define COLOR_WHITE             clrWhite                         //--- White color for text and backgrounds
#define COLOR_BLACK             clrBlack                         //--- Black color for borders and text
#define COLOR_LIGHT_GRAY        C'230,230,230'                   //--- Light gray color for signal backgrounds
#define COLOR_DARK_GRAY         C'105,105,105'                   //--- Dark gray color for indicator backgrounds

我们首先使用#define指令为多时间周期扫描仪表盘搭建用户界面框架,创建诸如“MAIN_PANEL”和“HEADER_PANEL”等常量,用于表示主面板和标题面板的矩形区域,同时用“HEADER_PANEL_ICON”、“HEADER_PANEL_TEXT”和“CLOSE_BUTTON”等常量来标识标题面板的图标、标题和关闭按钮元素。

我们为仪表盘的网格结构定义标识符。对于交易品种,我们设置“SYMBOL_RECTANGLE”和“SYMBOL_TEXT”;"TIMEFRAME_RECTANGLE"和"TIMEFRAME_TEXT"前缀则用于标识时间周期行。我们使用“HEADER_RECTANGLE”和“HEADER_TEXT”前缀来标识列标题,并使用“RSI_RECTANGLE”、“STOCH_RECTANGLE”、“BUY_RECTANGLE”等前缀,以及对应的“RSI_TEXT”、“STOCH_TEXT”和“BUY_TEXT”来标识指标和信号单元格。

我们配置尺寸参数,包括“WIDTH_TIMEFRAME”(90像素)、“WIDTH_INDICATOR”(70像素)、“WIDTH_SIGNAL”(90像素)以及“HEIGHT_RECTANGLE”(25像素)。我们通过"COLOR_WHITE"和"COLOR_BLACK"定义文本和边框颜色,将信号背景设置为"COLOR_LIGHT_GRAY"(“C'230,230,230'”),指标背景设为"COLOR_DARK_GRAY"(“C'105,105,105'”),以确保布局统一清晰。接下来,我们还需要在程序中定义一些全局变量。

bool panel_is_visible = true;                                    //--- Flag to control panel visibility

// Define the timeframes to be used
ENUM_TIMEFRAMES timeframes_array[] = {PERIOD_M1, PERIOD_M5, PERIOD_M15, PERIOD_M20, PERIOD_M30, 
                                      PERIOD_H1, PERIOD_H2, PERIOD_H3, PERIOD_H4, PERIOD_H8, 
                                      PERIOD_H12, PERIOD_D1, PERIOD_W1}; //--- Array of timeframes for scanning

// Global variables for indicator values
double rsi_values[];                                             //--- Array to store RSI values
double stochastic_values[];                                      //--- Array to store Stochastic signal line values
double cci_values[];                                             //--- Array to store CCI values
double adx_values[];                                             //--- Array to store ADX values
double ao_values[];                                              //--- Array to store AO values

在此阶段,我们声明一个布尔型变量“panel_is_visible”,并将其初始值设置为“true”,用于确定仪表盘是否显示在图表上。这个标识位使我们能够根据需要切换仪表盘的可见性,特别是在我们不需要数据更新时。接下来,我们使用ENUM_TIMEFRAMES 类型定义数组“timeframes_array”,该数组列出了从“PERIOD_M1”(1分钟)到“PERIOD_W1”(周线)的时间周期。该数组指定了仪表盘将要分析的时间周期,使我们能够以结构化的方式跨多个时间范围扫描市场信号。如果您不需要某些时间周期,或者需要调整它们,只需修改枚举值即可。

为了存储指标数据,我们创建了双精度数组“rsi_values”、“stochastic_values”、“cci_values”、“adx_values”和“ao_values”。这些数组分别用于存储RSI、STOCH、CCI、ADI和AO的计算值,使我们能够高效地处理并显示每个时间周期的交易信号。现在,我们可以定义一些辅助函数,用于确定所获取图表交易品种的方向和截断方式,具体如下:

//+------------------------------------------------------------------+
//| Truncate timeframe enum to display string                        |
//+------------------------------------------------------------------+
string truncate_timeframe_name(int timeframe_index)              //--- Function to format timeframe name
{
   string timeframe_string = StringSubstr(EnumToString(timeframes_array[timeframe_index]), 7); //--- Extract timeframe name
   return timeframe_string;                                      //--- Return formatted name
}

//+------------------------------------------------------------------+
//| Calculate signal strength for buy/sell                           |
//+------------------------------------------------------------------+
string calculate_signal_strength(double rsi, double stochastic, double cci, double adx, double ao, bool is_buy) //--- Function to compute signal strength
{
   int signal_strength = 0;                                      //--- Initialize signal strength counter
   
   if(is_buy && rsi < 40) signal_strength++;                     //--- Increment for buy if RSI is oversold
   else if(!is_buy && rsi > 60) signal_strength++;               //--- Increment for sell if RSI is overbought
   
   if(is_buy && stochastic < 40) signal_strength++;              //--- Increment for buy if Stochastic is oversold
   else if(!is_buy && stochastic > 60) signal_strength++;        //--- Increment for sell if Stochastic is overbought
   
   if(is_buy && cci < -70) signal_strength++;                    //--- Increment for buy if CCI is oversold
   else if(!is_buy && cci > 70) signal_strength++;               //--- Increment for sell if CCI is overbought
   
   if(adx > 40) signal_strength++;                               //--- Increment if ADX indicates strong trend
   
   if(is_buy && ao > 0) signal_strength++;                       //--- Increment for buy if AO is positive
   else if(!is_buy && ao < 0) signal_strength++;                 //--- Increment for sell if AO is negative
   
   if(signal_strength >= 3) return is_buy ? "Strong Buy" : "Strong Sell"; //--- Return strong signal if 3+ conditions met
   if(signal_strength >= 2) return is_buy ? "Buy" : "Sell";      //--- Return regular signal if 2 conditions met
   return "Neutral";                                             //--- Return neutral if insufficient conditions
}

在此阶段,我们定义了“truncate_timeframe_name”函数,该函数接受一个整型参数“timeframe_index”,用于格式化时间周期名称以供显示。在内部,我们使用StringSubstr函数从“EnumToString”函数对“timeframes_array[timeframe_index]”转换后的结果中提取子字符串,从第7个位置开始提取,并将其存储在“timeframe_string”中。接下来,我们返回“timeframe_string”,提供一个简洁、用户可读的时间周期名称。

我们创建了“calculate_signal_strength”函数,用于根据指标值判断买入或卖出信号。将整型变量“signal_strength”初始化为0,用于统计符合条件的信号数量。对于相对强弱指数,如果“is_buy”为true且“rsi”值低于40(超卖状态),或“is_buy”为false且“rsi”值超过60(超买状态),则“signal_strength”加1。同理,我们检查“stochastic”指标(低于40或高于60)、“CCI”指标(低于-70或高于70)以及“ao”指标(正值对应买入,负值对应卖出),每当满足一个条件时,“signal_strength”就增加1。

我们还对ADX进行评估,如果“adx”的值超过40,表明趋势强劲,无论买入还是卖出场景,“signal_strength”均加1。如果“signal_strength”达到3或以上,当“is_buy”为true时返回“强烈买入”,否则返回“强烈卖出”。如果“signal_strength”为2,则返回“买入”或“卖出”;反之,如果小于2,则返回“中性”,从而为仪表盘提供清晰的信号分类。最后,我们现在可以定义创建对象所需的函数了。

//+------------------------------------------------------------------+
//| Create a rectangle for the UI                                    |
//+------------------------------------------------------------------+
bool create_rectangle(string object_name, int x_distance, int y_distance, int x_size, int y_size, 
                      color background_color, color border_color = COLOR_BLACK) //--- Function to create a rectangle
{
   ResetLastError();                                                            //--- Reset error code
   if(!ObjectCreate(0, object_name, OBJ_RECTANGLE_LABEL, 0, 0, 0)) {            //--- Create rectangle object
      Print(__FUNCTION__, ": failed to create Rectangle: ERR Code: ", GetLastError()); //--- Log creation failure
      return(false);                                                            //--- Return failure
   }
   ObjectSetInteger(0, object_name, OBJPROP_XDISTANCE, x_distance);             //--- Set x position
   ObjectSetInteger(0, object_name, OBJPROP_YDISTANCE, y_distance);             //--- Set y position
   ObjectSetInteger(0, object_name, OBJPROP_XSIZE, x_size);                     //--- Set width
   ObjectSetInteger(0, object_name, OBJPROP_YSIZE, y_size);                     //--- Set height
   ObjectSetInteger(0, object_name, OBJPROP_CORNER, CORNER_RIGHT_UPPER);        //--- Set corner to top-right
   ObjectSetInteger(0, object_name, OBJPROP_BGCOLOR, background_color);         //--- Set background color
   ObjectSetInteger(0, object_name, OBJPROP_BORDER_COLOR, border_color);        //--- Set border color
   ObjectSetInteger(0, object_name, OBJPROP_BORDER_TYPE, BORDER_FLAT);          //--- Set flat border style
   ObjectSetInteger(0, object_name, OBJPROP_BACK, false);                       //--- Set to foreground
   
   ChartRedraw(0);                                                              //--- Redraw chart
   return(true);                                                                //--- Return success
}

//+------------------------------------------------------------------+
//| Create a text label for the UI                                   |
//+------------------------------------------------------------------+
bool create_label(string object_name, string text, int x_distance, int y_distance, int font_size = 12, 
                  color text_color = COLOR_BLACK, string font = "Arial Rounded MT Bold") //--- Function to create a label
{
   ResetLastError();                                                               //--- Reset error code
   if(!ObjectCreate(0, object_name, OBJ_LABEL, 0, 0, 0)) {                         //--- Create label object
      Print(__FUNCTION__, ": failed to create Label: ERR Code: ", GetLastError()); //--- Log creation failure
      return(false);                                                               //--- Return failure
   }
   ObjectSetInteger(0, object_name, OBJPROP_XDISTANCE, x_distance);                //--- Set x position
   ObjectSetInteger(0, object_name, OBJPROP_YDISTANCE, y_distance);                //--- Set y position
   ObjectSetInteger(0, object_name, OBJPROP_CORNER, CORNER_RIGHT_UPPER);           //--- Set corner to top-right
   ObjectSetString(0, object_name, OBJPROP_TEXT, text);                            //--- Set label text
   ObjectSetString(0, object_name, OBJPROP_FONT, font);                            //--- Set font
   ObjectSetInteger(0, object_name, OBJPROP_FONTSIZE, font_size);                  //--- Set font size
   ObjectSetInteger(0, object_name, OBJPROP_COLOR, text_color);                    //--- Set text color
   ObjectSetInteger(0, object_name, OBJPROP_ANCHOR, ANCHOR_CENTER);                //--- Center text
   
   ChartRedraw(0);                                                                 //--- Redraw chart
   return(true);                                                                   //--- Return success
}

为支持对象的创建,我们定义了“create_rectangle”函数,该函数接受以下参数:“object_name”、“x_distance”、“y_distance”、“x_size”、“y_size”、“background_color”和“border_color”。我们使用ResetLastError函数重置错误状态,通过ObjectCreate函数创建一个OBJ_RECTANGLE_LABEL(矩形标签对象),如果创建失败则使用“Print”函数记录错误信息,并返回false。

我们使用ObjectSetInteger函数设置矩形对象的属性,包括位置、尺寸、"CORNER_RIGHT_UPPER"(右上角锚点)、"background_color"(背景颜色)、"border_color"(边框颜色)和"BORDER_FLAT"(平直边框样式),确保其在前景层显示。随后调用ChartRedraw函数刷新图表,并返回true表示操作成功。对于文本显示,我们定义了"create_label"函数,其参数包括:"object_name"、"text"、"x_distance"、"y_distance"、"font_size"、"text_color"和"font"。

我们调用"ResetLastError"函数重置错误状态,使用"ObjectCreate"函数创建一个"OBJ_LABEL",如果创建失败则记录错误日志。接下来,通过"ObjectSetInteger"函数设置标签的位置、尺寸、颜色以及"ANCHOR_CENTER"(居中锚点),并使用ObjectSetString函数配置文本内容和字体。随后调用"ChartRedraw"函数刷新图表,并返回true表示操作成功。借助这些函数,我们便可以在"OnInit"事件处理器中创建初始面板对象,为仪表盘搭建基础框架。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()                                                                             //--- Initialize EA
{
   create_rectangle(MAIN_PANEL, 632, 40, 617, 374, C'30,30,30', BORDER_FLAT);            //--- Create main panel background
   create_rectangle(HEADER_PANEL, 632, 40, 617, 27, C'60,60,60', BORDER_FLAT);           //--- Create header panel background
   create_label(HEADER_PANEL_ICON, CharToString(91), 620, 54, 18, clrAqua, "Wingdings"); //--- Create header icon
   create_label(HEADER_PANEL_TEXT, "TimeframeScanner", 527, 52, 13, COLOR_WHITE);        //--- Create header title
   create_label(CLOSE_BUTTON, CharToString('r'), 32, 54, 18, clrYellow, "Webdings");     //--- Create close button

   // Create header rectangle and label
   create_rectangle(SYMBOL_RECTANGLE, 630, 75, WIDTH_TIMEFRAME, HEIGHT_RECTANGLE, clrGray); //--- Create symbol rectangle
   create_label(SYMBOL_TEXT, _Symbol, 585, 85, 11, COLOR_WHITE); //--- Create symbol label
   
   // Create summary and indicator headers (rectangles and labels)
   string header_names[] = {"BUY", "SELL", "RSI", "STOCH", "CCI", "ADX", "AO"};            //--- Define header titles
   for(int header_index = 0; header_index < ArraySize(header_names); header_index++) {     //--- Loop through headers
      int x_offset = (630 - WIDTH_TIMEFRAME) - (header_index < 2 ? header_index * WIDTH_SIGNAL : 2 * WIDTH_SIGNAL + (header_index - 2) * WIDTH_INDICATOR) + (1 + header_index); //--- Calculate x position
      int width = (header_index < 2 ? WIDTH_SIGNAL : WIDTH_INDICATOR);                     //--- Set width based on header type
      create_rectangle(HEADER_RECTANGLE + IntegerToString(header_index), x_offset, 75, width, HEIGHT_RECTANGLE, clrGray);             //--- Create header rectangle
      create_label(HEADER_TEXT + IntegerToString(header_index), header_names[header_index], x_offset - width/2, 85, 11, COLOR_WHITE); //--- Create header label
   }
   
   // Create timeframe rectangles and labels, and summary/indicator cells
   for(int timeframe_index = 0; timeframe_index < ArraySize(timeframes_array); timeframe_index++) {            //--- Loop through timeframes
      // Highlight current timeframe
      color timeframe_background = (timeframes_array[timeframe_index] == _Period) ? clrLimeGreen : clrGray;    //--- Set background color for current timeframe
      color timeframe_text_color = (timeframes_array[timeframe_index] == _Period) ? COLOR_BLACK : COLOR_WHITE; //--- Set text color for current timeframe
      
      create_rectangle(TIMEFRAME_RECTANGLE + IntegerToString(timeframe_index), 630, (75 + HEIGHT_RECTANGLE) + timeframe_index * HEIGHT_RECTANGLE - (1 + timeframe_index), WIDTH_TIMEFRAME, HEIGHT_RECTANGLE, timeframe_background);   //--- Create timeframe rectangle
      create_label(TIMEFRAME_TEXT + IntegerToString(timeframe_index), truncate_timeframe_name(timeframe_index), 585, (85 + HEIGHT_RECTANGLE) + timeframe_index * HEIGHT_RECTANGLE - (1 + timeframe_index), 11, timeframe_text_color); //--- Create timeframe label
                  
      // Create summary and indicator cells
      for(int header_index = 0; header_index < ArraySize(header_names); header_index++) { //--- Loop through headers for cells
         string cell_rectangle_name, cell_text_name;                                      //--- Declare cell name and label variables
         color cell_background = (header_index < 2) ? COLOR_LIGHT_GRAY : COLOR_BLACK;     //--- Set cell background color
         switch(header_index) {                                   //--- Select cell type
            case 0: cell_rectangle_name = BUY_RECTANGLE + IntegerToString(timeframe_index); cell_text_name = BUY_TEXT + IntegerToString(timeframe_index); break;     //--- Buy cell
            case 1: cell_rectangle_name = SELL_RECTANGLE + IntegerToString(timeframe_index); cell_text_name = SELL_TEXT + IntegerToString(timeframe_index); break;   //--- Sell cell
            case 2: cell_rectangle_name = RSI_RECTANGLE + IntegerToString(timeframe_index); cell_text_name = RSI_TEXT + IntegerToString(timeframe_index); break;     //--- RSI cell
            case 3: cell_rectangle_name = STOCH_RECTANGLE + IntegerToString(timeframe_index); cell_text_name = STOCH_TEXT + IntegerToString(timeframe_index); break; //--- Stochastic cell
            case 4: cell_rectangle_name = CCI_RECTANGLE + IntegerToString(timeframe_index); cell_text_name = CCI_TEXT + IntegerToString(timeframe_index); break;     //--- CCI cell
            case 5: cell_rectangle_name = ADX_RECTANGLE + IntegerToString(timeframe_index); cell_text_name = ADX_TEXT + IntegerToString(timeframe_index); break;     //--- ADX cell
            case 6: cell_rectangle_name = AO_RECTANGLE + IntegerToString(timeframe_index); cell_text_name = AO_TEXT + IntegerToString(timeframe_index); break;       //--- AO cell
         }
         int x_offset = (630 - WIDTH_TIMEFRAME) - (header_index < 2 ? header_index * WIDTH_SIGNAL : 2 * WIDTH_SIGNAL + (header_index - 2) * WIDTH_INDICATOR) + (1 + header_index);        //--- Calculate x position
         int width = (header_index < 2 ? WIDTH_SIGNAL : WIDTH_INDICATOR); //--- Set width based on cell type
         create_rectangle(cell_rectangle_name, x_offset, (75 + HEIGHT_RECTANGLE) + timeframe_index * HEIGHT_RECTANGLE - (1 + timeframe_index), width, HEIGHT_RECTANGLE, cell_background); //--- Create cell rectangle
         create_label(cell_text_name, "-/-", x_offset - width/2, (85 + HEIGHT_RECTANGLE) + timeframe_index * HEIGHT_RECTANGLE - (1 + timeframe_index), 10, COLOR_WHITE);                  //--- Create cell label
      }
   }
   
   // Initialize indicator arrays
   ArraySetAsSeries(rsi_values, true);                       //--- Set RSI array as timeseries
   ArraySetAsSeries(stochastic_values, true);                //--- Set Stochastic array as timeseries
   ArraySetAsSeries(cci_values, true);                       //--- Set CCI array as timeseries
   ArraySetAsSeries(adx_values, true);                       //--- Set ADX array as timeseries
   ArraySetAsSeries(ao_values, true);                        //--- Set AO array as timeseries
    
   return(INIT_SUCCEEDED);                                   //--- Return initialization success
}

OnInit事件处理器中,我们初始化多时间周期扫描仪表盘的用户界面。我们使用 create_rectangle 函数绘制"MAIN_PANEL"(主面板),位置为 (632, 40),尺寸为 617×374 像素,背景色设置为“C’30,30,30’”(深灰色);同时绘制"HEADER_PANEL"(标题栏),位置与主面板相同,但高度为27像素,背景色为“C’60,60,60’”(稍浅的灰色)。接着,我们使用"create_label"函数在标题栏上添加"HEADER_PANEL_ICON",位置为 (620, 54),并使用Wingdings字体中的字符。MQL5中可直接使用默认字符,我们通过CharToString函数将字符代码转换为字符串。此处使用的字符代码为91,您可根据需要替换为其他代码。

MQL5 Wingdings字符说明

随后,我们创建"HEADER_PANEL_TEXT",内容为"TimeframeScanner",位置为 (527, 52);同时创建"CLOSE_BUTTON",位置为 (32, 54)。这次我们改用另一种字体,并通过字符映射将字母 "r" 转换为特定符号字符串。以下是可选用的符号字体及其对应示例:

符号字体对照表

我们使用"create_rectangle"函数在坐标 (630, 75) 处创建名为"SYMBOL_RECTANGLE"的矩形对象,其宽度为"WIDTH_TIMEFRAME" ,高度为"HEIGHT_RECTANGLE",背景色设置为灰色。接着,通过"create_label"函数在坐标 (585, 85) 处创建文本标签"SYMBOL_TEXT",内容为当前交易品种名称。对于表头部分,我们定义了一个字符串数组"header_names",包含如"BUY"、"RSI"等标题。通过循环遍历该数组,创建位于y=75处的"HEADER_RECTANGLE"(x偏移量基于"WIDTH_SIGNAL"和"WIDTH_INDICATOR"),并使用 "create_label"函数在y=85处创建"HEADER_TEXT"标签。

我们通过遍历"timeframes_array"数组来搭建多时间周期网格布局。我们使用"create_rectangle"函数创建名为"TIMEFRAME_RECTANGLE" 的矩形对象,固定x坐标为 630,y坐标通过 (75 + HEIGHT_RECTANGLE) - (1 + timeframe_index) 动态计算,使用"timeframe_background"定义的动态颜色。通过"create_label"函数创建"TIMEFRAME_TEXT"标签,显示经过"truncate_timeframe_name"函数处理后的时间周期名称。对于每个单元格,使用"create_rectangle"函数创建单元格背景(如"BUY_RECTANGLE"、"RSI_RECTANGLE"等),背景色为"cell_background",并且使用"create_label"函数在单元格内添加初始标签“-/-”(表示无数据)。通过ArraySetAsSeries函数将指标数组设置为时间序列模式,便于后续数据处理。我们返回INIT_SUCCEEDED常量,确认仪表盘布局和数据结构初始化成功。编译后,呈现如下效果:

静态仪表盘

由图可见,仪表盘的基础布局已经搭建完成。当前,我们只需完成最后一步——接入指标数据并用于分析。虽然已有分析函数,但是我们需要先获取数据为其提供实时输入。为此,我们创建一个函数来处理所有的动态更新逻辑。

//+------------------------------------------------------------------+
//| Update indicator values                                          |
//+------------------------------------------------------------------+
void updateIndicators()                                                                               //--- Update dashboard indicators
{
   for(int timeframe_index = 0; timeframe_index < ArraySize(timeframes_array); timeframe_index++) {   //--- Loop through timeframes
      // Initialize indicator handles
      int rsi_indicator_handle = iRSI(_Symbol, timeframes_array[timeframe_index], 14, PRICE_CLOSE);   //--- Create RSI handle
      int stochastic_indicator_handle = iStochastic(_Symbol, timeframes_array[timeframe_index], 14, 3, 3, MODE_SMA, STO_LOWHIGH); //--- Create Stochastic handle
      int cci_indicator_handle = iCCI(_Symbol, timeframes_array[timeframe_index], 20, PRICE_TYPICAL); //--- Create CCI handle
      int adx_indicator_handle = iADX(_Symbol, timeframes_array[timeframe_index], 14);                //--- Create ADX handle
      int ao_indicator_handle = iAO(_Symbol, timeframes_array[timeframe_index]);                      //--- Create AO handle
      
      // Check for valid handles
      if(rsi_indicator_handle == INVALID_HANDLE || stochastic_indicator_handle == INVALID_HANDLE || 
         cci_indicator_handle == INVALID_HANDLE || adx_indicator_handle == INVALID_HANDLE || 
         ao_indicator_handle == INVALID_HANDLE) {                                                             //--- Check if any handle is invalid
         Print("Failed to create indicator handle for timeframe ", truncate_timeframe_name(timeframe_index)); //--- Log failure
         continue;                                                                                            //--- Skip to next timeframe
      }
      
      // Copy indicator values
      if(CopyBuffer(rsi_indicator_handle, 0, 0, 1, rsi_values) <= 0 ||                            //--- Copy RSI value
         CopyBuffer(stochastic_indicator_handle, 1, 0, 1, stochastic_values) <= 0 ||              //--- Copy Stochastic signal line value
         CopyBuffer(cci_indicator_handle, 0, 0, 1, cci_values) <= 0 ||                            //--- Copy CCI value
         CopyBuffer(adx_indicator_handle, 0, 0, 1, adx_values) <= 0 ||                            //--- Copy ADX value
         CopyBuffer(ao_indicator_handle, 0, 0, 1, ao_values) <= 0) {                              //--- Copy AO value
         Print("Failed to copy buffer for timeframe ", truncate_timeframe_name(timeframe_index)); //--- Log copy failure
         continue;                                                                                //--- Skip to next timeframe
      }
      
      // Update RSI
      color rsi_text_color = (rsi_values[0] < 30) ? clrBlue : (rsi_values[0] > 70) ? clrRed : COLOR_WHITE;         //--- Set RSI text color
      update_label(RSI_TEXT + IntegerToString(timeframe_index), DoubleToString(rsi_values[0], 2), rsi_text_color); //--- Update RSI label
      
      // Update Stochastic (Signal Line only)
      color stochastic_text_color = (stochastic_values[0] < 20) ? clrBlue : (stochastic_values[0] > 80) ? clrRed : COLOR_WHITE;    //--- Set Stochastic text color
      update_label(STOCH_TEXT + IntegerToString(timeframe_index), DoubleToString(stochastic_values[0], 2), stochastic_text_color); //--- Update Stochastic label
      
      // Update CCI
      color cci_text_color = (cci_values[0] < -100) ? clrBlue : (cci_values[0] > 100) ? clrRed : COLOR_WHITE;      //--- Set CCI text color
      update_label(CCI_TEXT + IntegerToString(timeframe_index), DoubleToString(cci_values[0], 2), cci_text_color); //--- Update CCI label
      
      // Update ADX
      color adx_text_color = (adx_values[0] > 25) ? clrBlue : COLOR_WHITE;                                         //--- Set ADX text color
      update_label(ADX_TEXT + IntegerToString(timeframe_index), DoubleToString(adx_values[0], 2), adx_text_color); //--- Update ADX label
      
      // Update AO
      color ao_text_color = (ao_values[0] > 0) ? clrGreen : (ao_values[0] < 0) ? clrRed : COLOR_WHITE;          //--- Set AO text color
      update_label(AO_TEXT + IntegerToString(timeframe_index), DoubleToString(ao_values[0], 2), ao_text_color); //--- Update AO label
      
      // Update Buy/Sell signals
      string buy_signal = calculate_signal_strength(rsi_values[0], stochastic_values[0], cci_values[0], 
                                                   adx_values[0], ao_values[0], true);                          //--- Calculate buy signal
      string sell_signal = calculate_signal_strength(rsi_values[0], stochastic_values[0], cci_values[0], 
                                                    adx_values[0], ao_values[0], false);                        //--- Calculate sell signal
      
      color buy_text_color = (buy_signal == "Strong Buy") ? COLOR_WHITE : COLOR_WHITE;                          //--- Set buy text color
      color buy_background = (buy_signal == "Strong Buy") ? clrGreen : 
                            (buy_signal == "Buy") ? clrSeaGreen : COLOR_DARK_GRAY;                              //--- Set buy background color
      update_rectangle(BUY_RECTANGLE + IntegerToString(timeframe_index), buy_background);                       //--- Update buy rectangle
      update_label(BUY_TEXT + IntegerToString(timeframe_index), buy_signal, buy_text_color);                    //--- Update buy label
      
      color sell_text_color = (sell_signal == "Strong Sell") ? COLOR_WHITE : COLOR_WHITE;                       //--- Set sell text color
      color sell_background = (sell_signal == "Strong Sell") ? clrRed : 
                             (sell_signal == "Sell") ? clrSalmon : COLOR_DARK_GRAY;                             //--- Set sell background color
      update_rectangle(SELL_RECTANGLE + IntegerToString(timeframe_index), sell_background);                     //--- Update sell rectangle
      update_label(SELL_TEXT + IntegerToString(timeframe_index), sell_signal, sell_text_color);                 //--- Update sell label
      
      // Release indicator handles
      IndicatorRelease(rsi_indicator_handle);                   //--- Release RSI handle
      IndicatorRelease(stochastic_indicator_handle);            //--- Release Stochastic handle
      IndicatorRelease(cci_indicator_handle);                   //--- Release CCI handle
      IndicatorRelease(adx_indicator_handle);                   //--- Release ADX handle
      IndicatorRelease(ao_indicator_handle);                    //--- Release AO handle
   }
}

为高效管理仪表盘数据更新,我们实现"updateIndicators"函数,用于动态刷新指标值及交易信号。我们通过循环遍历"timeframes_array",使用"timeframe_index"定位当前时间周期,确保每个时间周期的数据同步更新。我们通过调用iRSI、iStochastic、iCCI、iADX和"iAO"等内置函数,为当前交易品种和时间周期创建指标句柄(如"rsi_indicator_handle"),并配置参数,例如14周期的RSI指标、20周期的CCI指标。所有指标参数均可根据需求自定义调整,而无需局限于默认值。

接下来,我们检查每个指标句柄(如"rsi_indicator_handle")是否等于INVALID_HANDLE,如果为该值则表明句柄创建失败。如果存在无效句柄,我们使用"Print"函数记录错误信息(结合"truncate_timeframe_name"函数输出的时间周期名称),并跳过当前时间周期,继续处理下一个。我们再通过CopyBuffer函数将最新指标数据读取到数组(如"rsi_values")中,如果数据读取失败,同样记录错误并继续执行后续逻辑。随后,我们调用"update_label"函数更新指标显示内容。例如,我们根据"rsi_values[0]"的值设置"rsi_text_color":当低于30时显示为蓝色(超卖),当高于70时显示未红色(超买),其他情况显示为白色,使用"DoubleToString"函数格式化数值,并更新"RSI_TEXT"。对于其他指标(如"stochastic_values"、"cci_values"、"adx_values"和"ao_values")重复上述流程,并应用对应的颜色逻辑(例如 "ao_values"为正值时显示绿色)。

我们调用"calculate_signal_strength"函数计算交易信号,传入"rsi_values[0]" 等指标当前值,获取"buy_signal"和"sell_signal"。对于买入信号,根据信号强度设置背景颜色"buy_background"(例如“强烈买入”显示绿色背景),并通过"update_rectangle"函数更新矩形控件样式"BUY_RECTANGLE",同时使用"update_label"函数更新"BUY_TEXT"对卖出信号执行相同的操作:设置背景颜色"sell_background"(例如“强烈卖出”显示红色背景),并更新"SELL_RECTANGLE"和"SELL_TEXT"。最后,调用IndicatorRelease函数释放指标句柄,如"rsi_indicator_handle",确保资源高效回收。我们使用的辅助函数定义如下:

//+------------------------------------------------------------------+
//| Update rectangle background color                                |
//+------------------------------------------------------------------+
bool update_rectangle(string object_name, color background_color)//--- Function to update rectangle color
{
   int found = ObjectFind(0, object_name);                       //--- Find rectangle object
   if(found < 0) {                                               //--- Check if object not found
      ResetLastError();                                          //--- Reset error code
      Print("UNABLE TO FIND THE RECTANGLE: ", object_name, ". ERR Code: ", GetLastError()); //--- Log error
      return(false);                                             //--- Return failure
   }
   ObjectSetInteger(0, object_name, OBJPROP_BGCOLOR, background_color); //--- Set background color
   
   ChartRedraw(0);                                               //--- Redraw chart
   return(true);                                                 //--- Return success
}

//+------------------------------------------------------------------+
//| Update label text and color                                      |
//+------------------------------------------------------------------+
bool update_label(string object_name, string text, color text_color) //--- Function to update label
{
   int found = ObjectFind(0, object_name);                       //--- Find label object
   if(found < 0) {                                               //--- Check if object not found
      ResetLastError();                                          //--- Reset error code
      Print("UNABLE TO FIND THE LABEL: ", object_name, ". ERR Code: ", GetLastError()); //--- Log error
      return(false);                                             //--- Return failure
   }
   ObjectSetString(0, object_name, OBJPROP_TEXT, text);          //--- Set label text
   ObjectSetInteger(0, object_name, OBJPROP_COLOR, text_color);  //--- Set text color
   
   ChartRedraw(0);                                               //--- Redraw chart
   return(true);                                                 //--- Return success
}

我们定义"update_rectangle"函数,接收"object_name"和"background_color"作为参数,用于修改矩形控件的外观样式。首先,通过ObjectFind函数查找指定名称的矩形对象,并将返回值存储在"found"变量中。如果"found"小于0(表示对象未找到),则调用ResetLastError重置错误状态,使用"Print"函数结合"GetLastError"记录错误信息,并返回false表示操作失败。如果对象存在,则通过"ObjectSetInteger"函数将矩形的"OBJPROP_BGCOLOR"设置为传入的"background_color"值。最后,我们调用ChartRedraw函数刷新图表界面,并返回true表示操作成功。对于文本标签的更新,我们使用参数"object_name"、"text"和"text_color"定义"update_label"函数。

我们使用"ObjectFind"函数检查标签对象是否存在,如果返回值"found"为负数(表示对象未找到),则调用"ResetLastError"函数,并通过"Print"函数记录错误信息,最后返回false表示操作失败。如果对象存在,我们通过ObjectSetString函数将标签的"OBJPROP_TEXT"属性设置为传入的"text"参数,同时使用ObjectSetInteger函数将"OBJPROP_COLOR"属性设置为"text_color"参数。更新属性后,我们调用"ChartRedraw"函数刷新图表界面,并返回true表示操作成功,实现动态更新标签内容。现在,我们可以在每个tick到来时调用此更新函数,从而实现仪表盘的实时动态更新。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()                                                    //--- Handle tick events
{
   if (panel_is_visible) {                                       //--- Check if panel is visible
      updateIndicators();                                        //--- Update indicators
   }
}

OnTick事件处理器中,我们仅在仪表盘面板处于可见状态时调用"updateIndicators"函数,以执行更新操作。最后,我们需要删除所有已创建的图表对象,确保后续不再需要它们时从图表中彻底移除。

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)                                  //--- Deinitialize EA
{
   ObjectDelete(0, MAIN_PANEL);                                  //--- Delete main panel
   ObjectDelete(0, HEADER_PANEL);                                //--- Delete header panel
   ObjectDelete(0, HEADER_PANEL_ICON);                           //--- Delete header icon
   ObjectDelete(0, HEADER_PANEL_TEXT);                           //--- Delete header title
   ObjectDelete(0, CLOSE_BUTTON);                                //--- Delete close button

   ObjectsDeleteAll(0, SYMBOL_RECTANGLE);                        //--- Delete all symbol rectangles
   ObjectsDeleteAll(0, SYMBOL_TEXT);                             //--- Delete all symbol labels
   ObjectsDeleteAll(0, TIMEFRAME_RECTANGLE);                     //--- Delete all timeframe rectangles
   ObjectsDeleteAll(0, TIMEFRAME_TEXT);                          //--- Delete all timeframe labels
   ObjectsDeleteAll(0, HEADER_RECTANGLE);                        //--- Delete all header rectangles
   ObjectsDeleteAll(0, HEADER_TEXT);                             //--- Delete all header labels
   ObjectsDeleteAll(0, RSI_RECTANGLE);                           //--- Delete all RSI rectangles
   ObjectsDeleteAll(0, RSI_TEXT);                                //--- Delete all RSI labels
   ObjectsDeleteAll(0, STOCH_RECTANGLE);                         //--- Delete all Stochastic rectangles
   ObjectsDeleteAll(0, STOCH_TEXT);                              //--- Delete all Stochastic labels
   ObjectsDeleteAll(0, CCI_RECTANGLE);                           //--- Delete all CCI rectangles
   ObjectsDeleteAll(0, CCI_TEXT);                                //--- Delete all CCI labels
   ObjectsDeleteAll(0, ADX_RECTANGLE);                           //--- Delete all ADX rectangles
   ObjectsDeleteAll(0, ADX_TEXT);                                //--- Delete all ADX labels
   ObjectsDeleteAll(0, AO_RECTANGLE);                            //--- Delete all AO rectangles
   ObjectsDeleteAll(0, AO_TEXT);                                 //--- Delete all AO labels
   ObjectsDeleteAll(0, BUY_RECTANGLE);                           //--- Delete all buy rectangles
   ObjectsDeleteAll(0, BUY_TEXT);                                //--- Delete all buy labels
   ObjectsDeleteAll(0, SELL_RECTANGLE);                          //--- Delete all sell rectangles
   ObjectsDeleteAll(0, SELL_TEXT);                               //--- Delete all sell labels
   
   ChartRedraw(0);                                               //--- Redraw chart
}

最终,我们通过OnDeinit 函数实现清理逻辑,该函数会在智能交易系统(EA)被移除或程序终止时自动触发执行。我们使用ObjectDelete函数逐个删除用户界面(UI)元素,首先移除主面板矩形对象"MAIN_PANEL",随后依次删除标题栏组件"HEADER_PANEL"、"HEADER_PANEL_ICON"和"HEADER_PANEL_TEXT",以及关闭按钮 "CLOSE_BUTTON",确保从图表中彻底清除所有主面板和标题栏的相关元素。

我们通过ObjectsDeleteAll函数按元素类型系统化地移除所有仪表盘对象。移除所有类型为矩形的"SYMBOL_RECTANGLE"和对应文本标签"SYMBOL_TEXT",清除品种显示区域,以及"TIMEFRAME_RECTANGLE"和对应文本标签"TIMEFRAME_TEXT",清除时间框架显示区域,外加"HEADER_RECTANGLE"及其文本标签"HEADER_TEXT",清理标题区域。同时移除所有技术指标对应的矩形对象,包括 "RSI_RECTANGLE"、"STOCH_RECTANGLE"、"CCI_RECTANGLE"、"ADX_RECTANGLE" 和 "AO_RECTANGLE",并且删除这些指标的配套文本标签,如 "RSI_TEXT"。

我们通过"ObjectsDeleteAll"函数完成清理工作,删除所有"BUY_RECTANGLE"和"SELL_RECTANGLE"矩形对象,以及对应的"BUY_TEXT"和"SELL_TEXT"文本标签,彻底移除所有与交易信号相关的界面元素。最后,调用ChartRedraw函数刷新图表,确保在程序卸载后呈现干净的视觉效果。此外,我们需要设定取消按钮的交互逻辑:当用户点击该按钮时,关闭仪表盘并停止后续数据更新。

//+------------------------------------------------------------------+
//| Expert chart event handler                                       |
//+------------------------------------------------------------------+
void OnChartEvent(const int       event_id,                      //--- Event ID
                  const long&     long_param,                    //--- Long parameter
                  const double&   double_param,                  //--- Double parameter
                  const string&   string_param)                  //--- String parameter
{
   if (event_id == CHARTEVENT_OBJECT_CLICK) {                    //--- Check for object click event
      if (string_param == CLOSE_BUTTON) {                        //--- Check if close button clicked
         Print("Closing the panel now");                         //--- Log panel closure
         PlaySound("alert.wav");                                 //--- Play alert sound
         panel_is_visible = false;                               //--- Hide panel
         
         ObjectDelete(0, MAIN_PANEL);                            //--- Delete main panel
         ObjectDelete(0, HEADER_PANEL);                          //--- Delete header panel
         ObjectDelete(0, HEADER_PANEL_ICON);                     //--- Delete header icon
         ObjectDelete(0, HEADER_PANEL_TEXT);                     //--- Delete header title
         ObjectDelete(0, CLOSE_BUTTON);                          //--- Delete close button
      
         ObjectsDeleteAll(0, SYMBOL_RECTANGLE);                  //--- Delete all symbol rectangles
         ObjectsDeleteAll(0, SYMBOL_TEXT);                       //--- Delete all symbol labels
         ObjectsDeleteAll(0, TIMEFRAME_RECTANGLE);               //--- Delete all timeframe rectangles
         ObjectsDeleteAll(0, TIMEFRAME_TEXT);                    //--- Delete all timeframe labels
         ObjectsDeleteAll(0, HEADER_RECTANGLE);                  //--- Delete all header rectangles
         ObjectsDeleteAll(0, HEADER_TEXT);                       //--- Delete all header labels
         ObjectsDeleteAll(0, RSI_RECTANGLE);                     //--- Delete all RSI rectangles
         ObjectsDeleteAll(0, RSI_TEXT);                          //--- Delete all RSI labels
         ObjectsDeleteAll(0, STOCH_RECTANGLE);                   //--- Delete all Stochastic rectangles
         ObjectsDeleteAll(0, STOCH_TEXT);                        //--- Delete all Stochastic labels
         ObjectsDeleteAll(0, CCI_RECTANGLE);                     //--- Delete all CCI rectangles
         ObjectsDeleteAll(0, CCI_TEXT);                          //--- Delete all CCI labels
         ObjectsDeleteAll(0, ADX_RECTANGLE);                     //--- Delete all ADX rectangles
         ObjectsDeleteAll(0, ADX_TEXT);                          //--- Delete all ADX labels
         ObjectsDeleteAll(0, AO_RECTANGLE);                      //--- Delete all AO rectangles
         ObjectsDeleteAll(0, AO_TEXT);                           //--- Delete all AO labels
         ObjectsDeleteAll(0, BUY_RECTANGLE);                     //--- Delete all buy rectangles
         ObjectsDeleteAll(0, BUY_TEXT);                          //--- Delete all buy labels
         ObjectsDeleteAll(0, SELL_RECTANGLE);                    //--- Delete all sell rectangles
         ObjectsDeleteAll(0, SELL_TEXT);                         //--- Delete all sell labels
         
         ChartRedraw(0);                                         //--- Redraw chart
      }
   }
}

OnChartEvent事件处理器中,我们监听对象点击事件:当事件类型为CHARTEVENT_OBJECT_CLICK且被点击的对象是“取消按钮”时,通过PlaySound函数播放提示音,告知用户仪表盘即将关闭;随后隐藏仪表盘界面,并复用OnDeinit中的清理逻辑彻底清除图表上的所有仪表盘对象。编译后,呈现如下效果:

更新后的仪表盘

由图可见,仪表盘已成功更新并显示指标数据及交易方向指示。接下来,我们将在后续章节中验证项目的实际运行效果。


回测

我们已完成测试,以下是整合后的可视化结果,以单一的图形交换格式(GIF)位图图像形式呈现。

测试GIF


结论

总而言之,我们基于MQL5开发了一款多时间框架扫描仪表盘,集成了结构化网格布局、实时指标信号以及交互式关闭按钮,旨在为策略交易决策提供高效支持。我们详细阐述了这些功能的设计与实现过程,通过稳健的初始化逻辑和针对交易需求的动态更新机制,确保其有效性。该仪表盘支持高度的自定义,用户可根据个人偏好调整布局与功能,从而显著提升对多时间框架市场信号的监控能力与响应速度。

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

附加的文件 |
开发多币种 EA 交易(第 24 部分):添加新策略(二) 开发多币种 EA 交易(第 24 部分):添加新策略(二)
在本文中,我们将继续将新策略与创建的自动优化系统联系起来。让我们看看需要对优化项目创建 EA 以及第二和第三阶段 EA 进行哪些更改。
数据科学和机器学习(第 36 部分):与偏颇的金融市场打交道 数据科学和机器学习(第 36 部分):与偏颇的金融市场打交道
金融市场非是完美平衡。有些市场看涨,有些看跌,有些市场展现范围起伏行为,表明无论哪个方向都不确定,这些不平衡的信息在训练机器学习模型时可能会误导,在于市场频繁变化。在本文中,我们将讨论若干种途径来应对该问题。
MQL5自动化交易策略(第十八部分):基于包络线趋势反弹的剥头皮交易——核心架构与信号生成(1) MQL5自动化交易策略(第十八部分):基于包络线趋势反弹的剥头皮交易——核心架构与信号生成(1)
本文中,我们将构建包络线趋势反弹剥头皮EA的核心架构。我们初始化包络线等信号生成所需的指标。同时,我们还将搭建回测环境,为下一篇文章中的交易执行环节做好准备。
MQL5 中的策略可视化:在标准图表中展示优化结果 MQL5 中的策略可视化:在标准图表中展示优化结果
在本文中,我们编写了一个可视化优化过程的示例,并显示了四个优化标准的前三个步骤。我们还将提供一个机会,从三个最佳通过中选择一个,以便在表格和图表上显示其数据。