English Русский Deutsch 日本語
preview
MQL5 交易工具(第五部分):创建滚动行情条,实现交易品种实时监控

MQL5 交易工具(第五部分):创建滚动行情条,实现交易品种实时监控

MetaTrader 5交易 |
40 2
Allan Munene Mutiiria
Allan Munene Mutiiria

引言

前一篇文章(第四部分)中,我们使用MQL5对多周期扫描面板进行了改进,增加了动态定位与开关切换功能,实现了可移动、可最小化的界面显示,提升了使用体验。在第五部分中,我们将创建一款滚动行情条,用于实时监控多个交易品种,支持滚动显示买价、点差、日内涨跌幅,并提供可自定义的可视化样式,让交易者能够一目了然地掌握市场信息。我们将涵盖以下主题:

  1. 理解滚动行情条结构
  2. 在MQL5中的实现
  3. 回测
  4. 结论

到本文结束时,您将拥有一款灵活通用的MQL5行情滚动工具,可自定义并集成到您的交易系统中 —— 下面开始具体实现!


理解滚动行情条结构

我们即将创建的滚动行情条,是一款以滚动形式展示多品种实时数据的工具,它会显示买价、点差和日内涨跌幅,方便我们快速掌握市场动态。这一功能十分重要,它能以紧凑、动态的方式呈现市场波动,突出价格趋势与波动率,同时又不会让图表信息显得拥挤或过载,这对于快节奏交易中的快速决策至关重要。

实现方式如下:我们将展示区域划分为独立的滚动行,分别显示交易品种、价格、点差和涨跌幅,并通过自定义的速度与颜色来标识涨跌走势。我们将使用数组存储交易品种数据,借助定时器实现流畅滚动,确保行情条既能适配用户偏好,又能在后台高效运行。接下来,就让我们看一下具体如何实现。简单来说,我们希望呈现的可视化效果如下:

行情条设计方案


在MQL5中的实现

要在MQL5中创建该程序,我们首先需要定义程序的基础数据,然后设置一些输入参数。这些参数能让我们在不直接修改代码的情况下,轻松调整程序的运行逻辑。

//+------------------------------------------------------------------+
//|                                      ROLLING TICKER TIMER EA.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"

#include <Arrays\ArrayString.mqh> //--- Include ArrayString library for string array operations

//--- Input parameters
input string Symbols = "EURUSDm,GBPUSDm,USDJPYm,USDCHFm,AUDUSDm,BTCUSDm,TSLAm"; // Symbols to display
input int UpdateInterval = 50;                     // Update interval (milliseconds)
input int SymbolFontSize = 10;                     // Symbol font size (first line)
input string SymbolFont = "Arial Bold";            // Symbol font
input int AskFontSize = 10;                        // Ask font size (second line)
input string AskFont = "Arial";                    // Ask font
input int SpreadFontSize = 10;                     // Spread font size (third line)
input string SpreadFont = "Calibri";               // Spread font
input int SectionFontSize = 10;                    // Section currency, bid, and percent change font size
input string SectionFont = "Arial";                // Section currency, bid, and percent change font
input color FontColor = clrWhite;                  // Base font color
input color UpColor = clrLime;                     // Color for price increase (Bid text and positive % change)
input color DownColor = clrRed;                    // Color for price decrease (Bid text and negative % change)
input color ArrowUpColor = clrBlue;                // Color for up arrow
input color ArrowDownColor = clrRed;               // Color for down arrow
input int Y_Position = 30;                         // Starting Y position (pixels)
input int SymbolHorizontalSpacing = 160;           // Horizontal spacing for Symbol line (pixels)
input int AskHorizontalSpacing = 150;              // Horizontal spacing for Ask line (pixels)
input int SpreadHorizontalSpacing = 200;           // Horizontal spacing for Spread line (pixels)
input int SectionHorizontalSpacing = 170;          // Horizontal spacing for Section line (pixels)
input double SymbolScrollSpeed = 3.0;              // Symbol line scroll speed (pixels per update)
input double AskScrollSpeed = 1.3;                 // Ask line scroll speed (pixels per update)
input double SpreadScrollSpeed = 10.0;             // Spread line scroll speed (pixels per update)
input double SectionScrollSpeed = 2.7;             // Section scroll speed (pixels per update)
input bool ShowSpread = true;                      // Show spread line
input color BackgroundColor = clrBlack;            // Background rectangle color
input int BackgroundOpacity = 100;                 // Background opacity (0-255, limited effect)

现在,我们开始在MQL5中实现实时监控滚动行情条:首先引入"<Arrays\ArrayString.mqh>"库,并定义用于自定义的输入参数。从而实现高效的字符串数组操作,这对于处理和拆分待显示的交易品种列表至关重要。输入参数"Symbols"默认设置为"EURUSDm、GBPUSDm、USDJPYm、USDCHFm、AUDUSDm、BTCUSDm、TSLAm",用于指定需要监控的交易品种,我们可以自由配置在行情条中显示的资产。我们将"UpdateInterval"设置为50毫秒,在响应速度与运行性能之间取得平衡。

在界面自定义方面,我们定义了以下参数:交易品种行(将"SymbolFontSize"设置为 10,"SymbolFont"设置为"Arial Bold")、卖价行(将"AskFontSize"设置为10,"AskFont"设置为"Arial")、点差行(将"SpreadFontSize"设置为10,"SpreadFont"设置为"Calibri")以及货币、买价、涨跌幅区域(将"SectionFontSize"设置为 10,"SectionFont"设置为"Arial")

我们将基础文本的"FontColor"设置为clrWhite ,价格变动的"UpColor"设为"clrLime", "DownColor"设置为"clrRed",方向箭头的"ArrowUpColor"设置为"clrBlue","ArrowDownColor"设置为"clrRed"。定位和间距设置输入项包括:将起始垂直位置"Y_Position"设置为30像素,"SymbolHorizontalSpacing"设置为160像素,"AskHorizontalSpacing"设置为150像素,"SpreadHorizontalSpacing"设置为200像素,"SectionHorizontalSpacing"设置为170像素,用于控制布局。

滚动速度设置如下:将"SymbolScrollSpeed"设置为每次更新3.0像素,"AskScrollSpeed"设置为1.3,"SpreadScrollSpeed"设置为10.0,"SectionScrollSpeed"设置为2.7,以实现各行独立移动。我们将"ShowSpread"设置为true以启用点差线,"BackgroundColor"设为"clrBlack",并将"BackgroundOpacity"设置为100,用作背景矩形。这些输入将使我们能够调整行情显示的样式、滚动逻辑和展示内容,以实现最佳的实时监控。编译后,我们得到了以下输入集。

输入集

在定义好这些输入项之后,我们可以继续定义一些全局变量和结构体,它们将在整个程序中使用,并分别用于存储滚动行情条所需的通用信息。

//--- Global variables
string symbolArray[];                              //--- Array to store symbol names
int totalSymbols;                                  //--- Total number of symbols
struct SymbolData                                  //--- Structure to hold symbol price data
{
   double bid;                                     //--- Current bid price
   double ask;                                     //--- Current ask price
   double spread;                                  //--- Current spread
   double prev_bid;                                //--- Previous bid price
   double daily_open;                              //--- Daily opening price
   color bid_color;                                //--- Color for bid price display
   double percent_change;                          //--- Daily percentage change
   color percent_color;                            //--- Color for percentage change
   string arrow_char;                              //--- Arrow character for price direction
   color arrow_color;                              //--- Color for arrow
};
SymbolData prices[];                               //--- Array of symbol data structures
string dashboardName = "TickerDashboard";          //--- Name for dashboard objects
string backgroundName = "TickerBackground";        //--- Name for background object
CArrayString objManager;                           //--- Object manager for text and image objects
datetime lastDay = 0;                              //--- Track last day for daily open update

这里,我们定义全局变量结构体,用于管理交易品种数据和仪表盘元素。我们声明字符串数组"symbolArray",用于存储输入项"Symbols"中的交易品种名称。整型变量"totalSymbols"用于记录拆分输入字符串后的交易品种总数。我们定义"SymbolData"结构体,用于存储每个交易品种的价格信息,其中包含:当前买价"bid"、当前卖价"ask" 、计算得出的点差"spread"、上一笔买价"prev_bid"、当日开盘价"daily_open"、买价显示的颜色"bid_color"、当日涨跌幅"percent_change"、涨跌幅的显示颜色"percent_color" 、价格方向箭头的符号"arrow_char"和箭头的颜色"arrow_color"。

我们构建"prices"数组,其元素为"SymbolData"结构体,用于存储所有交易品种的数据。将字符串变量"dashboardName"设置为"TickerDashboard",用于命名仪表盘对象;将字符串变量"backgroundName"设置为"TickerBackground",用于命名背景矩形。我们使用"CArrayString objManager"来统一管理所有文本与图形对象名称,以便后续轻松地清理。最后,通过"lastDay"记录最后一次更新的当日开盘价日期。这些全局变量负责组织交易品种数据与对象管理,以实现高效的实时更新与滚动效果。接下来,我们将定义一些全局工具函数,用于创建核心行情滚动面板,具体如下:

//+------------------------------------------------------------------+
//| Utility Functions                                                |
//+------------------------------------------------------------------+
void LogError(string message)                      // Log error messages
{
   Print(message);                                 //--- Output message to log
}

//+------------------------------------------------------------------+
//| Create Text Label Function                                       |
//+------------------------------------------------------------------+
bool createText(string objName, string text, int x, int y, color clrTxt, int fontsize, string font)
{
   ResetLastError();                               //--- Clear last error code
   if(ObjectFind(0, objName) < 0)                  //--- Check if object does not exist
   {
      if(!ObjectCreate(0, objName, OBJ_LABEL, 0, 0, 0)) //--- Create text label object
      {
         LogError(__FUNCTION__ + ": Failed to create label: " + objName + ", Error: " + IntegerToString(GetLastError())); //--- Log creation failure
         return false;                             //--- Return failure
      }
      objManager.Add(objName);                     //--- Add object name to manager
   }
   ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, x);       //--- Set x-coordinate
   ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, y);       //--- Set y-coordinate
   ObjectSetInteger(0, objName, OBJPROP_CORNER, CORNER_LEFT_UPPER); //--- Set corner alignment
   ObjectSetString(0, objName, OBJPROP_TEXT, text);          //--- Set text content
   ObjectSetInteger(0, objName, OBJPROP_COLOR, clrTxt);      //--- Set text color
   ObjectSetInteger(0, objName, OBJPROP_FONTSIZE, fontsize); //--- Set font size
   ObjectSetString(0, objName, OBJPROP_FONT, font);          //--- Set font type
   ObjectSetInteger(0, objName, OBJPROP_BACK, false);        //--- Disable background
   ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, false);  //--- Disable selection
   ObjectSetInteger(0, objName, OBJPROP_ZORDER, 0);          //--- Set z-order
   return true;                                              //--- Return success
}

//+------------------------------------------------------------------+
//| Create Panel Function                                            |
//+------------------------------------------------------------------+
bool createPanel(string objName, int y, int width, int height, color clr)
{
   ResetLastError();                               //--- Clear last error code
   if(ObjectFind(0, objName) < 0)                  //--- Check if panel does not exist
   {
      if(!ObjectCreate(0, objName, OBJ_RECTANGLE_LABEL, 0, 0, 0)) //--- Create rectangle panel
      {
         LogError(__FUNCTION__ + ": Failed to create panel: " + objName + ", Error: " + IntegerToString(GetLastError())); //--- Log creation failure
         return false;                             //--- Return failure
      }
      objManager.Add(objName);                     //--- Add panel to object manager
   }
   ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, 0);  //--- Set x-coordinate to 0
   ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, y);  //--- Set y-coordinate
   ObjectSetInteger(0, objName, OBJPROP_XSIZE, width);  //--- Set panel width
   ObjectSetInteger(0, objName, OBJPROP_YSIZE, height); //--- Set panel height
   ObjectSetInteger(0, objName, OBJPROP_BGCOLOR, clr);  //--- Set background color
   ObjectSetInteger(0, objName, OBJPROP_FILL, true);    //--- Enable fill
   ObjectSetInteger(0, objName, OBJPROP_COLOR, clr);    //--- Set border color
   ObjectSetInteger(0, objName, OBJPROP_STYLE, STYLE_SOLID); //--- Set border style
   ObjectSetInteger(0, objName, OBJPROP_WIDTH, 1);      //--- Set border width
   ObjectSetInteger(0, objName, OBJPROP_BACK, false);   //--- Enable background drawing
   ObjectSetInteger(0, objName, OBJPROP_ZORDER, -1);    //--- Set z-order behind other objects
   return true;                                         //--- Return success
}

我们通过实现工具函数来处理错误日志记录、创建文本标签以及初始化面板,确保生成可靠的用户界面(UI)元素并支持调试工作。我们首先实现"LogError"函数:该函数接收一个字符串类型的参数"message",并通过Print函数将信息输出至日志中。接下来,我们创建"createText"函数,为行情滚动显示生成文本标签。该函数的参数包括:"objName"、"text"、"x"、"y"、"clrTxt"、"fontsize"和"font"。

我们调用ResetLastError清除上一次的错误信息,并通过ObjectFind函数检查图表对象是否已存在。如果对象不存在,则使用ObjectCreate函数创建一个标签对象OBJ_LABEL;如果创建失败则记录日志,并返回false。我们将"objName"添加到"objManager"中进行统一管理,随后通过ObjectSetInteger设置OBJPROP_XDISTANCE及所有其他整数类型属性,通过"ObjectSetString"设置"OBJPROP_TEXT"和 "OBJPROP_FONT"等字符串类型属性。该函数可确保交易品种、价格及涨跌幅在呈现效果上保持一致。

接下来,我们定义"createPanel"函数,用于创建背景面板。该函数接收"objName"、"y"、"width"、"height"和"clr"作为参数,采用与"createText"函数相同的代码结构;为行情滚动条提供可自定义的背景,并通过颜色选择实现模拟透明度的效果。现在我们可以进一步创建行情纸带面板,但首先需要整理所需数据:将输入的交易品种字符串拆分为彼此独立、可供调用的单个品种名称,并初始化价格与颜色数据。我们将在OnInit事件处理器中执行这些操作。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   //--- Split symbols string into array
   totalSymbols = StringSplit(Symbols, ',', symbolArray); //--- Split input symbols into array
   ArrayResize(prices, totalSymbols);            //--- Resize prices array to match symbol count
   
   //--- Verify symbols exist and initialize data
   for(int i = 0; i < totalSymbols; i++)         //--- Iterate through all symbols
   {
      if(!SymbolSelect(symbolArray[i], true))    //--- Select symbol for market watch
      {
         LogError("OnInit: Symbol " + symbolArray[i] + " not found"); //--- Log symbol not found
         return(INIT_FAILED);                    //--- Return initialization failure
      }
      prices[i].bid = 0;                         //--- Initialize bid price
      prices[i].ask = 0;                         //--- Initialize ask price
      prices[i].spread = 0;                      //--- Initialize spread
      prices[i].prev_bid = 0;                    //--- Initialize previous bid
      prices[i].daily_open = iOpen(symbolArray[i], PERIOD_D1, 0); //--- Set daily opening price
      prices[i].bid_color = FontColor;           //--- Set initial bid color
      prices[i].percent_change = 0;              //--- Initialize percentage change
      prices[i].percent_color = FontColor;       //--- Set initial percent color
      prices[i].arrow_char = CharToString(236);  //--- Set default up arrow
      prices[i].arrow_color = FontColor;         //--- Set initial arrow color
   }
   ArrayPrint(symbolArray);
   ArrayPrint(prices);
}

OnInit事件处理器中,我们对程序进行初始化,配置交易品种与数据结构。首先,我们使用StringSplit函数,以逗号作为分隔符,将输入的字符串"Symbols"拆分并存入"symbolArray"数组,并将交易品种数量存入"totalSymbols"变量。如果您之前定义了其他分隔符,直接在这里使用即可。我们通过ArrayResize函数将"prices"数组的长度调整为"totalSymbols",使其与交易品种数量匹配。接下来,我们遍历"symbolArray"数组中的每一个交易品种,通过SymbolSelect函数将其添加到市场报价列表中;如果添加失败,则通过"LogError"函数记录错误信息,并返回"INIT_FAILED"。

针对每个交易品种,我们将"prices[i].bid"、"prices[i].ask"、"prices[i].spread"以及"prices[i].prev_bid"初始化为0;通过iOpen函数将"prices[i].daily_open"设置为"PERIOD_D1"的每日开盘价;同时为以下属性分配初始颜色与数值:"prices[i].bid_color"、"prices[i].percent_change"、"prices[i].percent_color"、"prices[i].arrow_char"(使用CharToString函数将向上箭头转换为字符串)和"prices[i].arrow_color"。我们使用ArrayPrint函数打印输出"symbolArray"和"prices",以便调试排查。这样可以确保所有的交易品种有效,并为实时更新准备好数据。编译后,我们会得到以下输出。

初始化输出

由图可见,我们已经成功初始化所有交易品种和数据变量,意味着一切准备就绪。现在,我们可以开始构建仪表盘背景。

//+------------------------------------------------------------------+
//| Create background function                                       |
//+------------------------------------------------------------------+
void CreateBackground()
{
   int width = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); //--- Get chart width
   int height = (ShowSpread ? 4 : 3) * (MathMax(MathMax(MathMax(SymbolFontSize, AskFontSize), SpreadFontSize), SectionFontSize) + 2) + 40; //--- Calculate panel height
   createPanel(backgroundName, Y_Position - 5, width, height, BackgroundColor); //--- Create background panel
}

这里,我们实现"CreateBackground"函数,并为行情滚动显示创建背景面板。首先,我们通过ChartGetInteger函数,以CHART_WIDTH_IN_PIXELS为参数获取图表宽度,并将其转换为整数后存入变量"width"。我们通过三元运算符判断"ShowSpread"(是否显示点差)的状态,以确定面板包含4行还是3行内容,之后将行数乘以这四种字体"SymbolFontSize"、"AskFontSize"、"SpreadFontSize"和"SectionFontSize"中的最大值(加上2作为内边距),最后再额外加上40像素的间距,以此计算出面板高度,并存入变量"height"中。最后,我们调用"createPanel"函数绘制背景矩形,传入参数"backgroundName"、"Y_Position - 5"(垂直对齐位置)、 "width"、"height"和"BackgroundColor",为滚动文本元素提供统一的基础背景。在初始化阶段调用该函数后,呈现的效果如下:

面板背景

完成背景后,我们可以继续创建仪表盘的其他元素。我们通过创建一个函数将所有内容整合起来,如下所示:

//+------------------------------------------------------------------+
//| Create dashboard function                                        |
//+------------------------------------------------------------------+
void CreateDashboard()
{
   //--- Create text and image objects for each symbol
   for(int i = 0; i < totalSymbols; i++)         //--- Iterate through all symbols
   {
      // Determine image based on symbol
      string imageFile;                          //--- Variable for image file path
      if(symbolArray[i] == "EURUSDm")            //--- Check for EURUSDm
         imageFile = "\\Images\\euro.bmp";       //--- Set EURUSD image
      else if(symbolArray[i] == "GBPUSDm")       //--- Check for GBPUSDm
         imageFile = "\\Images\\gbpusd.bmp";     //--- Set GBPUSD image
      else if(symbolArray[i] == "USDJPYm")       //--- Check for USDJPYm
         imageFile = "\\Images\\usdjpy.bmp";     //--- Set USDJPY image
      else if(symbolArray[i] == "USDCHFm")       //--- Check for USDCHFm
         imageFile = "\\Images\\usdchf.bmp";     //--- Set USDCHF image
      else if(symbolArray[i] == "AUDUSDm")       //--- Check for AUDUSDm
         imageFile = "\\Images\\audusd.bmp";     //--- Set AUDUSD image
      else if(symbolArray[i] == "BTCUSDm")       //--- Check for BTCUSDm
         imageFile = "\\Images\\btcusd.bmp";     //--- Set BTCUSD image
      else if(symbolArray[i] == "TSLAm")         //--- Check for TSLAm
         imageFile = "\\Images\\tesla.bmp";      //--- Set Tesla image
      else
         imageFile = "\\Images\\euro.bmp";       //--- Set default image
      
      // Symbol line (first line)
      createText(dashboardName + "_Symbol_" + IntegerToString(i), "", (i * SymbolHorizontalSpacing), Y_Position, FontColor, SymbolFontSize, SymbolFont); //--- Create symbol text label
      
      // Ask line (second line)
      createText(dashboardName + "_Ask_" + IntegerToString(i), "", (i * AskHorizontalSpacing), Y_Position + SymbolFontSize + 2, FontColor, AskFontSize, AskFont); //--- Create ask price text label
      
      // Spread line (third line, if enabled)
      if(ShowSpread)                             //--- Check if spread display is enabled
      {
         createText(dashboardName + "_Spread_" + IntegerToString(i), "", (i * SpreadHorizontalSpacing), Y_Position + SymbolFontSize + 2 + AskFontSize + 2, FontColor, SpreadFontSize, SpreadFont); //--- Create spread text label
      }
      
      // Section: Image (left)
      string imageName = dashboardName + "_Image_" + IntegerToString(i); //--- Define image object name
      if(ObjectFind(0, imageName) < 0)           //--- Check if image object does not exist
      {
         if(!ObjectCreate(0, imageName, OBJ_BITMAP_LABEL, 0, 0, 0)) //--- Create image object
         {
            LogError("CreateDashboard: Failed to create image: " + imageName + ", Error: " + IntegerToString(GetLastError())); //--- Log image creation failure
            return;                              //--- Exit function
         }
         objManager.Add(imageName);              //--- Add image to object manager
      }
      ObjectSetInteger(0, imageName, OBJPROP_XDISTANCE, (i * SectionHorizontalSpacing)); //--- Set image x-coordinate
      ObjectSetInteger(0, imageName, OBJPROP_YDISTANCE, Y_Position + (ShowSpread ? SymbolFontSize + 2 + AskFontSize + 2 + SpreadFontSize + 14 : SymbolFontSize + 2 + AskFontSize + 14)); //--- Set image y-coordinate
      ObjectSetString(0, imageName, OBJPROP_BMPFILE, imageFile); //--- Set image file
      ObjectSetInteger(0, imageName, OBJPROP_CORNER, CORNER_LEFT_UPPER); //--- Set image corner alignment
      
      // Section: Currency (top, right of image)
      string currencyName = dashboardName + "_Currency_" + IntegerToString(i); //--- Define currency text object name
      createText(currencyName, StringFormat("%-10s", symbolArray[i]), (i * SectionHorizontalSpacing) + 35, Y_Position + (ShowSpread ? SymbolFontSize + 2 + AskFontSize + 2 + SpreadFontSize + 14 : SymbolFontSize + 2 + AskFontSize + 14), FontColor, SectionFontSize, SectionFont); //--- Create currency text label
      
      // Section: Percent Change (next to currency, horizontal)
      string percentChangeName = dashboardName + "_PercentChange_" + IntegerToString(i); //--- Define percent change object name
      string percentText = prices[i].percent_change >= 0 ? StringFormat("+%.2f%%", prices[i].percent_change) : StringFormat("%.2f%%", prices[i].percent_change); //--- Format percent change text
      createText(percentChangeName, percentText, (i * SectionHorizontalSpacing) + 105, Y_Position + (ShowSpread ? SymbolFontSize + 2 + AskFontSize + 2 + SpreadFontSize + 14 : SymbolFontSize + 2 + AskFontSize + 14), prices[i].percent_color, SectionFontSize, SectionFont); //--- Create percent change text label
      
      // Section: Arrow (below currency, right of image, Wingdings)
      string arrowName = dashboardName + "_Arrow_" + IntegerToString(i); //--- Define arrow object name
      createText(arrowName, prices[i].arrow_char, (i * SectionHorizontalSpacing) + 35, Y_Position + (ShowSpread ? SymbolFontSize + 2 + AskFontSize + 2 + SpreadFontSize + 14 : SymbolFontSize + 2 + AskFontSize + 14) + SectionFontSize + 2, prices[i].arrow_color, SectionFontSize, "Wingdings"); //--- Create arrow text label
      
      // Section: Bid Price (next to arrow, horizontal)
      string bidName = dashboardName + "_Bid_" + IntegerToString(i); //--- Define bid price object name
      createText(bidName, StringFormat("%.5f", prices[i].bid), (i * SectionHorizontalSpacing) + 50, Y_Position + (ShowSpread ? SymbolFontSize + 2 + AskFontSize + 2 + SpreadFontSize + 14 : SymbolFontSize + 2 + AskFontSize + 14) + SectionFontSize + 2, prices[i].bid_color, SectionFontSize, SectionFont); //--- Create bid price text label
   }
}

这里,我们实现"CreateDashboard"函数,用于搭建行情显示的可视化元素,包括每个交易品种的文本标签与图片。我们首先遍历"totalSymbols",通过if-else条件判断,根据"symbolArray[i]"确定"imageFile",为"EURUSDm"等交易品种指定专属的位图(BMP)文件,其他品种则使用默认图片。我们调用"createText"创建交易品种行的文本,对象名称为"dashboardName + "Symbol" + IntegerToString(i)",坐标位置:X 轴("i * SymbolHorizontalSpacing")和Y轴("Y_Position")。

对于卖价行,我们再次调用"createText"创建文本标签,对象名称为 dashboardName + "Ask" + IntegerToString(i),坐标位置:X 轴("i * AskHorizontalSpacing")和Y轴("Y_Position + SymbolFontSize + 2")。如果"ShowSpread"为true,我们会通过"createText"额外添加点差行文本,对象名称为 "dashboardName + "Spread" + IntegerToString(i)",并且按对应的规则设置位置。

对于分区模块,如果对象不存在,我们先通过ObjectCreate函数创建一个OBJ_BITMAP_LABEL类型的图片对象,将其添加到"objManager"中统一管理,再通过"ObjectSetInteger"设置位置,并使用 "ObjectSetString"函数为对象指定"imageFile"。请注意,您需要准备BMP格式的图片文件。我们使用的默认路径如下,您也可以使用自定义路径。

图片文件目录

接下来,我们使用"createText"函数创建货币相关文本,对象名称为"dashboardName + "Currency" + IntegerToString(i)",并通过StringFormat函数对文本进行格式化处理。对于涨跌幅,我们基于"prices[i].percent_change"格式化生成"percentText",再通过"createText"函数创建文本。我们使用"prices[i].arrow_char",并指定"Wingdings"字体,通过"createText"函数添加箭头标签。最后,我们通过"createText"函数,结合"StringFormat"格式化"prices[i].bid",创建买价文本。该函数将搭建出多行滚动行情面板布局,包含图片与动态文本,用于展示实时滚动数据。现在只需在初始化时调用该函数,即可得到如下的输出效果:

静态仪表盘

到目前为止我们创建的只是一个静态仪表盘。现在,我们需要做的是让这个仪表盘实现数据更新。为了实现实时更新,我们不想依赖逐笔价格(tick)驱动的更新方式,因为此方式完全取决于程序所挂载交易品种的报价频率。我们希望采用基于定时器的更新机制,从而确保更新能够高频稳定地执行。首先,让我们定义相关函数,以便在需要时更新仪表盘和背景。

//+------------------------------------------------------------------+
//| Update background function                                       |
//+------------------------------------------------------------------+
void UpdateBackground()
{
   int width = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); //--- Get current chart width
   int height = (ShowSpread ? 4 : 3) * (MathMax(MathMax(MathMax(SymbolFontSize, AskFontSize), SpreadFontSize), SectionFontSize) + 2) + 40; //--- Recalculate panel height
   ObjectSetInteger(0, backgroundName, OBJPROP_XSIZE, width);  //--- Update panel width
   ObjectSetInteger(0, backgroundName, OBJPROP_YSIZE, height); //--- Update panel height
}

//+------------------------------------------------------------------+
//| Update dashboard function                                        |
//+------------------------------------------------------------------+
void UpdateDashboard()
{
   static double symbolOffset = 0;                //--- Track symbol line offset
   static double askOffset = 0;                   //--- Track ask line offset
   static double spreadOffset = 0;                //--- Track spread line offset
   static double sectionOffset = 0;               //--- Track section offset
   int totalWidthSymbol = totalSymbols * SymbolHorizontalSpacing;   //--- Calculate total symbol line width
   int totalWidthAsk = totalSymbols * AskHorizontalSpacing;         //--- Calculate total ask line width
   int totalWidthSpread = totalSymbols * SpreadHorizontalSpacing;   //--- Calculate total spread line width
   int totalWidthSection = totalSymbols * SectionHorizontalSpacing; //--- Calculate total section width
   int rightEdge = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);  //--- Get chart right boundary
   
   //--- Update text and image objects
   for(int i = 0; i < totalSymbols; i++)         //--- Iterate through all symbols
   {
      // Symbol line (first line)
      string symbolName = dashboardName + "_Symbol_" + IntegerToString(i); //--- Define symbol object name
      double symbolXPos = (i * SymbolHorizontalSpacing) - symbolOffset; //--- Calculate symbol x-position
      if(symbolXPos < -SymbolHorizontalSpacing) symbolXPos += totalWidthSymbol; //--- Wrap around if off-screen
      createText(symbolName, StringFormat("%-10s", symbolArray[i]), (int)symbolXPos, Y_Position, FontColor, SymbolFontSize, SymbolFont); //--- Update symbol text
      ObjectSetInteger(0, symbolName, OBJPROP_HIDDEN, symbolXPos > rightEdge || symbolXPos < 0); //--- Hide if off-screen
      
      // Ask line (second line)
      string askName = dashboardName + "_Ask_" + IntegerToString(i); //--- Define ask object name
      double askXPos = (i * AskHorizontalSpacing) - askOffset;       //--- Calculate ask x-position
      if(askXPos < -AskHorizontalSpacing) askXPos += totalWidthAsk;  //--- Wrap around if off-screen
      createText(askName, StringFormat("%.5f", prices[i].ask), (int)askXPos, Y_Position + SymbolFontSize + 2, clrMagenta, AskFontSize, AskFont); //--- Update ask text
      ObjectSetInteger(0, askName, OBJPROP_HIDDEN, askXPos > rightEdge || askXPos < 0); //--- Hide if off-screen
      
      // Spread line (third line)
      if(ShowSpread)                             //--- Check if spread display is enabled
      {
         string spreadName = dashboardName + "_Spread_" + IntegerToString(i);      //--- Define spread object name
         double spreadXPos = (i * SpreadHorizontalSpacing) - spreadOffset;         //--- Calculate spread x-position
         if(spreadXPos < -SpreadHorizontalSpacing) spreadXPos += totalWidthSpread; //--- Wrap around if off-screen
         createText(spreadName, StringFormat("%.1f", prices[i].spread), (int)spreadXPos, Y_Position + SymbolFontSize + 2 + AskFontSize + 2, clrAqua, SpreadFontSize, SpreadFont); //--- Update spread text
         ObjectSetInteger(0, spreadName, OBJPROP_HIDDEN, spreadXPos > rightEdge || spreadXPos < 0); //--- Hide if off-screen
      }
      
      // Section (Image, Currency, Percent Change, Arrow, Bid Price)
      double sectionXPos = (i * SectionHorizontalSpacing) - sectionOffset;          //--- Calculate section x-position
      if(sectionXPos < -SectionHorizontalSpacing) sectionXPos += totalWidthSection; //--- Wrap around if off-screen
      
      // Image (left)
      string imageName = dashboardName + "_Image_" + IntegerToString(i); //--- Define image object name
      ObjectSetInteger(0, imageName, OBJPROP_XDISTANCE, (int)sectionXPos); //--- Update image x-coordinate
      ObjectSetInteger(0, imageName, OBJPROP_YDISTANCE, Y_Position + (ShowSpread ? SymbolFontSize + 2 + AskFontSize + 2 + SpreadFontSize + 14 : SymbolFontSize + 2 + AskFontSize + 14)); //--- Update image y-coordinate
      ObjectSetInteger(0, imageName, OBJPROP_HIDDEN, sectionXPos > rightEdge || sectionXPos < 0); //--- Hide if off-screen
      
      // Currency (top, right of image)
      string currencyName = dashboardName + "_Currency_" + IntegerToString(i); //--- Define currency object name
      createText(currencyName, StringFormat("%-10s", symbolArray[i]), (int)sectionXPos + 35, Y_Position + (ShowSpread ? SymbolFontSize + 2 + AskFontSize + 2 + SpreadFontSize + 14 : SymbolFontSize + 2 + AskFontSize + 14), FontColor, SectionFontSize, "Arial Bold"); //--- Update currency text
      
      // Percent Change (next to currency, horizontal)
      string percentChangeName = dashboardName + "_PercentChange_" + IntegerToString(i); //--- Define percent change object name
      string percentText = prices[i].percent_change >= 0 ? StringFormat("+%.2f%%", prices[i].percent_change) : StringFormat("%.2f%%", prices[i].percent_change); //--- Format percent change
      createText(percentChangeName, percentText, (int)sectionXPos + 105, Y_Position + (ShowSpread ? SymbolFontSize + 2 + AskFontSize + 2 + SpreadFontSize + 14 : SymbolFontSize + 2 + AskFontSize + 14), prices[i].percent_color, SectionFontSize, SectionFont); //--- Update percent change text
      
      // Arrow (below currency, right of image, Wingdings)
      string arrowName = dashboardName + "_Arrow_" + IntegerToString(i); //--- Define arrow object name
      createText(arrowName, prices[i].arrow_char, (int)sectionXPos + 35, Y_Position + (ShowSpread ? SymbolFontSize + 2 + AskFontSize + 2 + SpreadFontSize + 14 : SymbolFontSize + 2 + AskFontSize + 14) + SectionFontSize + 2, prices[i].arrow_color, SectionFontSize, "Wingdings"); //--- Update arrow text
      
      // Bid Price (next to arrow, horizontal)
      string bidName = dashboardName + "_Bid_" + IntegerToString(i); //--- Define bid object name
      createText(bidName, StringFormat("%.5f", prices[i].bid), (int)sectionXPos + 50, Y_Position + (ShowSpread ? SymbolFontSize + 2 + AskFontSize + 2 + SpreadFontSize + 14 : SymbolFontSize + 2 + AskFontSize + 14) + SectionFontSize + 2, prices[i].bid_color, SectionFontSize, SectionFont); //--- Update bid price text
   }
   
   //--- Increment offsets for scrolling effect
   symbolOffset = fmod(symbolOffset + SymbolScrollSpeed, totalWidthSymbol);     //--- Update symbol line offset
   askOffset = fmod(askOffset + AskScrollSpeed, totalWidthAsk);                 //--- Update ask line offset
   spreadOffset = fmod(spreadOffset + SpreadScrollSpeed, totalWidthSpread);     //--- Update spread line offset
   sectionOffset = fmod(sectionOffset + SectionScrollSpeed, totalWidthSection); //--- Update section offset
   
   //--- Redraw chart
   ChartRedraw();                                //--- Refresh chart display
}

这里,我们实现"UpdateBackground"函数,用于在图表尺寸改变时自动调整背景面板。我们使用ChartGetInteger函数,以CHART_WIDTH_IN_PIXELS为参数获取当前图表宽度,并将其转换为整数后存入变量"width"中。我们通过三元运算符来判断"ShowSpread"的状态,重新计算面板行数为4行或3行,将行数乘以这四种字体"SymbolFontSize"、"AskFontSize"、"SpreadFontSize"和"SectionFontSize"中的最大值(加上2作为内边距),再额外加上40像素的间距,以此得出面板高度,并存入变量"height"中。最后,我们通过ObjectSetInteger函数,为"backgroundName"更新"OBJPROP_XSIZE"和OBJPROP_YSIZE属性。

接下来,我们实现"UpdateDashboard"函数,用于处理文本与图片对象的滚动和数据更新。我们定义静态偏移量"symbolOffset"、"askOffset"、"spreadOffset"和"sectionOffset" ,用于跟踪各行的滚动位置。我们将"totalSymbols"与各自的水平间距参数相乘,计算出总宽度"totalWidthSymbol"、"totalWidthAsk"、"totalWidthSpread"和"totalWidthSection" 。我们通过"ChartGetInteger"函数,以"CHART_WIDTH_IN_PIXELS"为参数获取图表右边界坐标。我们遍历所有交易品种,根据"symbolOffset"调整"symbolXPos"来更新每个品种的位置,如果超出屏幕下边界,则通过取模运算实现循环滚动效果,并调用"createText"函数更新文本;如果超出右边界"rightEdge",则通过"ObjectSetInteger"函数设置"OBJPROP_HIDDEN"属性将对象隐藏。

我们对卖价、点差(如果开启"ShowSpread")以及分区模块执行类似的更新操作,包括:通过"ObjectSetInteger"设置图片的 OBJPROP_XDISTANCE和"OBJPROP_YDISTANCE",更新货币名称、涨跌幅(通过 StringFormat格式化)、方向箭头(使用"prices[i].arrow_char")以及买价文本。我们利用"fmod"(取模函数)结合滚动速度来递增偏移量,并调用ChartRedraw刷新图表显示。这些函数能确保行情滚动器自适应各类变化,并实现流畅滚动,满足实时监控的需求。接下来,我们可以在OnTimer事件处理器中调用这些功能,但首先需要设置定时器的时间间隔。这一步是不可或缺的。

//--- Set timer
EventSetMillisecondTimer(UpdateInterval);     //--- Set timer for updates

//--- Initialize last day
lastDay = TimeCurrent() / 86400;              //--- Set current day for daily open tracking

这里,我们只需调用EventSetMillisecondTimer函数来设置定时器的时间间隔,并传入已定义的更新间隔参数,最后,初始化用于追踪新交易日的lastDay变量。现在,我们可以定义定时器的执行逻辑了。

//+------------------------------------------------------------------+
//| Expert timer function                                            |
//+------------------------------------------------------------------+
void OnTimer()
{
   //--- Check for new day to update daily open
   datetime currentDay = TimeCurrent() / 86400//--- Calculate current day
   if(currentDay > lastDay)                      //--- Check if new day
   {
      for(int i = 0; i < totalSymbols; i++)      //--- Iterate through symbols
      {
         prices[i].daily_open = iOpen(symbolArray[i], PERIOD_D1, 0); //--- Update daily open price
      }
      lastDay = currentDay;                      //--- Update last day
   }
   
   //--- Update background size in case chart is resized
   UpdateBackground();                           //--- Update background dimensions
   
   //--- Update dashboard display
   UpdateDashboard();                            //--- Update dashboard visuals
}

这里,我们实现 OnTimer事件处理器,用于管理实时交易品种监控滚动行情条的周期性更新;该函数会按照"UpdateInterval"设置的时间间隔自动触发。首先,我们通过将TimeCurrent除以86400来计算"currentDay",将其转换为天数(1 天 = 24 小时 × 60 分钟 × 60 秒)。如果"currentDay"大于"lastDay",说明进入新的交易日,我们遍历所有的交易品种,通过iOpen函数获取"PERIOD_D1"位移0的最新开盘价,更新每个品种的"prices[i].daily_open",之后,将"lastDay"赋值为"currentDay",完成新日期的追踪。这一步可以确保在进入新交易日后,日内涨跌幅基于新的开盘价重新计算。

接下来,我们调用"UpdateBackground",在图表尺寸改变时自动调整背景面板。最后,我们调用"UpdateDashboard",根据最新数据与滚动位置刷新所有文本和图片对象,让行情条保持动态响应并随时间实时更新。运行效果如下:

稳定运行的行情条

从可视化效果中可以看出,我们已经实现了一个能够正常滚动的行情条。现在我们只需要更新价格数据,整个功能就全部完成了。我们也把这段更新逻辑封装到一个函数里来实现。

//+------------------------------------------------------------------+
//| Update prices function                                           |
//+------------------------------------------------------------------+
void UpdatePrices()
{
   for(int i = 0; i < totalSymbols; i++)         //--- Iterate through all symbols
   {
      double bid = SymbolInfoDouble(symbolArray[i], SYMBOL_BID); //--- Retrieve current bid price
      double ask = SymbolInfoDouble(symbolArray[i], SYMBOL_ASK); //--- Retrieve current ask price
      
      //--- Validate prices
      if(bid == 0 || ask == 0)                   //--- Check for invalid prices
      {
         LogError("UpdatePrices: Failed to retrieve prices for " + symbolArray[i]); //--- Log price retrieval failure
         continue;                               //--- Skip to next symbol
      }
      
      //--- Update color and arrow based on price change (tick-to-tick for bid and arrow)
      if(bid > prices[i].prev_bid && prices[i].prev_bid != 0) //--- Check if bid increased
      {
         prices[i].bid_color = UpColor;            //--- Set bid color to up color
         prices[i].arrow_char = CharToString(236); //--- Set up arrow character
         prices[i].arrow_color = ArrowUpColor;     //--- Set arrow to up color
      }
      else if(bid < prices[i].prev_bid && prices[i].prev_bid != 0) //--- Check if bid decreased
      {
         prices[i].bid_color = DownColor;          //--- Set bid color to down color
         prices[i].arrow_char = CharToString(238); //--- Set down arrow character
         prices[i].arrow_color = ArrowDownColor;   //--- Set arrow to down color
      }
      else                                         //--- Handle no change or first tick
      {
         prices[i].bid_color = FontColor;          //--- Set bid color to default
         prices[i].arrow_char = CharToString(236); //--- Set default up arrow
         prices[i].arrow_color = FontColor;        //--- Set arrow to default color
      }
      
      //--- Calculate daily percentage change
      prices[i].percent_change = prices[i].daily_open != 0 ? ((bid - prices[i].daily_open) / prices[i].daily_open) * 100 : 0; //--- Compute percentage change
      prices[i].percent_color = prices[i].percent_change >= 0 ? UpColor : DownColor; //--- Set percent color based on change
      
      //--- Update data
      prices[i].bid = bid;                       //--- Store current bid
      prices[i].ask = ask;                       //--- Store current ask
      prices[i].spread = (ask - bid) * MathPow(10, SymbolInfoInteger(symbolArray[i], SYMBOL_DIGITS)); //--- Calculate spread
      prices[i].prev_bid = bid;                  //--- Update previous bid
   }
}

我们实现"UpdatePrices"函数,用于刷新交易品种数据。我们遍历所有交易品种"totalSymbols",并通过"SymbolInfoDouble"函数,以SYMBOL_BID和 "SYMBOL_ASK"为参数,获取每个交易品种"symbolArray[i]"的买价和卖价。如果"bid"或"ask"为 0,我们通过"LogError"记录错误,并跳过当前品种,处理下一个。我们根据当前"bid"与上一笔"prev_bid"的大小关系(忽略初始值0),更新"bid_color"、"arrow_char"(通过CharToString函数生成向上或向下箭头)和"arrow_color"。这些箭头使用的是MQL5默认的Wingdings符号字体,具体定义如下:

MQL5 Wingdings字符说明

您也可以使用您喜欢的箭头字符代码。接下来,我们使用当日开盘价"daily_open"计算涨跌幅"percent_change",并通过三元运算符根据涨跌设置百分比颜色"percent_color"。最后,我们更新以下数据:"prices[i].bid"、"prices[i].ask"、"spread"(使用MathPowSymbolInfoInteger函数获取报价小数位数"SYMBOL_DIGITS"后计算得出)和"prev_bid",确保用于显示和判断变化的数据都是最新的,让行情显示的价格和指标在每一次报价变动时都保持实时更新。现在,可以在每一次报价变动时调用该函数来处理价格变化,也可以选择在OnTimer定时器函数中调用。由您决定。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
   //--- Update prices on every tick for live changes
   UpdatePrices();                               //--- Update symbol prices
}

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   //--- Clean up objects
   for(int i = objManager.Total() - 1; i >= 0; i--) //--- Iterate through all managed objects
   {
      string name = objManager.At(i);               //--- Get object name
      if(ObjectFind(0, name) >= 0)                  //--- Check if object exists
      {
         if(!ObjectDelete(0, name))                 //--- Delete object
            LogError("OnDeinit: Failed to delete object: " + name + ", Error: " + IntegerToString(GetLastError())); //--- Log deletion failure
      }
      objManager.Delete(i);                        //--- Remove object from manager
   }
   EventKillTimer();                               //--- Stop timer
}

OnTick事件处理器中,我们调用"UpdatePrices"来刷新所有交易品种的买价、卖价、点差和变动数据,确保行情显示能及时反映市场的实时波动。接下来,我们实现OnDeinit函数,用于在卸载程序时执行清理工作。我们通过"Total"对"objManager"进行倒序遍历,并使用"At"获取每个对象的名称。根据ObjectFind判断对象是否存在,如果存在则使用ObjectDelete函数将其删除;如果删除失败,则通过"LogError"记录错误信息。我们使用Delete操作符将对象名称从"objManager"中移除。最后,通过 EventKillTimer停止定时器,终止周期性更新。这一步是不可或缺的。这样可以确保所有对象都被正确清理,避免在图表上残留无用的元素。运行程序后,效果如下:

最终效果

由运行效果可见,所有功能均按预期正常运行,我们也实现了最初的目标。当前仅需完成项目可操作性的测试工作,该部分内容已在前文章节中详细阐述。


回测

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

回测


结论

总体而言,我们已基于MQL5开发了一款滚动行情条,用于实时监控交易品种。它具备买价、点差、日内涨跌幅的滚动显示功能,并支持自定义字体、颜色和滚动速度,可高效地突显市场的波动。我们讲解了程序的架构与实现方式,从"SymbolData"等数据结构,到"UpdateDashboard" 、 "UpdatePrices"等功能函数,确保了界面流畅滚动与数据精准更新,为交易分析提供有效的支持。您可以根据自身需求自定义这款行情工具,大幅提升同时监控多个品种、实时响应价格趋势的能力。

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

附加的文件 |
最近评论 | 前往讨论 (2)
Edson Kennedy
Edson Kennedy | 22 7月 2025 在 13:11
不工作
Allan Munene Mutiiria
Allan Munene Mutiiria | 22 7月 2025 在 13:12
Edson Kennedy #:
不工作

你读过这篇文章吗?

MetaTrader 5 机器学习蓝图(第一部分):数据泄露与时间戳修正 MetaTrader 5 机器学习蓝图(第一部分):数据泄露与时间戳修正
在开始将机器学习用于 MetaTrader 5 交易之前,必须先处理一个常被忽视的关键问题:数据泄露。本文深入剖析了数据泄露,尤其是 MetaTrader 5 时间戳陷阱,说明它如何扭曲模型表现并导致不可靠的交易信号。通过深入研究这一问题的机理并提出预防策略,我们为构建稳健的机器学习模型铺平了道路,这些模型能够在实时交易环境中提供值得信赖的预测结果。
价格行为分析工具包开发(第 32 部分):基于 Python 的 K 线识别引擎(二)—— 使用 TA-Lib 进行检测 价格行为分析工具包开发(第 32 部分):基于 Python 的 K 线识别引擎(二)—— 使用 TA-Lib 进行检测
本文中,我们已从在 Python 中手动编写 K 线形态检测代码,转向使用 TA-Lib 库,该库可识别六十余种不同的K线形态。这些形态能为预判市场潜在反转与趋势延续提供极具价值的参考。下面继续详细说明。
经典策略重构(第14部分):多策略分析 经典策略重构(第14部分):多策略分析
在本文中,我们继续探讨如何构建多策略组合体系,并使用 MT5 遗传算法优化器对策略参数进行调优。本次我们使用 Python 对数据进行分析,结果表明:我们的模型能更准确地预判哪一个策略会表现更优,其预测精度高于直接预测市场收益率。然而,当我们使用这些统计模型对应用程序进行测试时,性能却大幅下滑。我们随后发现,遗憾的是,遗传优化器偏向了高度相关的策略,这促使我们修改方案:将投票权重固定,转而让优化器专注于优化指标参数。
从基础到中级:结构(五) 从基础到中级:结构(五)
在本文中,我们将探讨如何重载结构化代码。我知道一开始理解起来可能会相当有挑战性,尤其是当你第一次看到它的时候。在尝试深入探讨更复杂、更精细的主题之前,掌握并理解这些概念是非常重要的。