English Русский Español Deutsch 日本語
preview
从新手到专家:使用MQL5实现动态新闻标题(X)——新闻交易的多品种图表视图

从新手到专家:使用MQL5实现动态新闻标题(X)——新闻交易的多品种图表视图

MetaTrader 5 — 示例 |
47 0
Clemence Benjamin
Clemence Benjamin

内容:


引言

在前一篇文章中,我们介绍了多品种管理功能,交易者可以快速选择自己偏好的交易品种。该功能减少了为切换交易品种而反复切换图表的耗时。

但仍存在一个痛点:交易者虽然可以管理选中的品种,却无法直接调用这些品种的图表 API,因此不方便查看它们的价格以供分析。为解决该问题,本文对多品种交易功能进行优化,开发专门用于多图表可视化的类。该类可在图表内流畅控制,并作为新闻标题EA整套功能的组成部分集成进去。

如果您读到这里觉得思路跟不上,下面就来快速回顾一下我们之前的开发进度。同时建议您回看本主题的往期文章,以便获得更深入的理解。

新闻标题EA的最初版本,核心功能是在图表上划分清晰的栏目,按重要等级展示财经日历的预定事件。“栏目”(lanes)这个设计思路后续不断拓展:我们接入了Alpha Vantage的新闻信息流API、增加本地部署AI模型的分析提示,还新增栏目用于展示内置指标的分析信号。在此基础上,我们推出自动化新闻交易功能,包含专为NFP(非农就业数据)交易开发的专属逻辑,并集成手工交易按钮。

近期,我们进一步开发了多品种交易功能,用于行情剧烈波动时的快速决策。该功能使得交易者在快速变化的市场环境下,管理和交易多个货币对变得简单很多。

而本文,我们将继续优化这套多品种交易方案,让交易者可以直接在EA内查看实时多图表视图。


策略实施

我们的核心原则之一是即使新手读者也能清晰理解相关概念。在本节中,我们拆解为实现该目标所采用的思路与代码实现方案。

随着程序功能的不断扩充,新增功能的集成必须采用结构化、易于维护的开发方式。模块化是我很常用的开发手段,它可以保障开发流程顺畅,同时以头文件和类的形式封装可复用工具。

对于新闻标题EA这类复杂程序,我遵循一套固定开发流程:

  1. 先开发独立的小型测试程序,专门用来验证新功能;
  2. 当功能验证可行且运行稳定后,再集成入主EA。

这套开发流程目标明确、能够减少缺陷,并且保证新功能可以平稳接入,不会破坏原有功能。

本次我们的设计任务是创建CChartMiniTiles类,用来在同一张主图表内渲染多个品种的子图表,每个子图尺寸支持自定义。之后我们在一个测试EA(MiniChartsEA)中调用这个类,验证方案可行性。验证通过后,再把该类集成到新闻标题EA中,并做适配,保证运行流畅。

最后需要说明的是,CChartMiniTiles、MiniChartsEA这类名称只是本次演示用的占位名称,您可以自行修改命名,前提是理解程序的运行逻辑。

接下来的四个小节,我们进入代码实现环节:

  1. ChartMiniTiles头文件
  2. 用于测试该头文件的示例EA(MiniChartsEA)
  3. 初步测试
  4. 将ChartsMiniTiles集成至新闻标题EA

1.0ChartMiniTiles头文件

该头文件定义了CChartMiniTiles类,这是一个可复用工具类,用于在单张MetaTrader 5主图表内渲染和管理多个迷你子图表。该类的作用是方便将多品种图表视图集成到大型项目(例如新闻标题EA)中,同时保持代码模块化、易于维护。

将该功能独立封装在单独的头文件里,就可以单独对这个功能进行测试(比如用一个测试EA),后续再无缝集成到更复杂的系统中。该方法可以优化开发流程,降低开发阶段出错的概率。

在后续小节中,我们会把代码开发流程拆分为带编号的步骤,每一步聚焦程序结构与功能的某一个特定部分。

1.1. 类的概述与用途

开头注释区块说明了头文件的用途,并定义整个类所使用的编译期默认参数。借助宏定义统一管理布局、定时参数、开关选项的默认值,维护人员无需深入底层实现细节,就能快速调整组件行为。头部注释列出全部功能特性:基于OBJ_CHART创建底部锚定的迷你子图表、适配不同经纪商的品种自动解析、用于快速显示隐藏的切换按钮、自适应布局计算,还支持顶部预留区域,避免和其他UI组件发生遮挡冲突。这一部分帮助读者提前理解该类的组织结构与设计目标。

//+------------------------------------------------------------------+
//| ChartMiniTilesClass.mqh                                          |
//| Class wrapper for ChartMiniTiles functionality                   |
//| - CChartMiniTiles class                                           |
//| - bottom-anchored mini-chart tiles using OBJ_CHART                |
//| - broker-adaptive symbol lookup, toggle button, responsive layout |
//| - supports top-reserved area to avoid overlapping top UI     |
//+------------------------------------------------------------------+
#ifndef __CHART_MINI_TILES_CLASS_MQH__
#define __CHART_MINI_TILES_CLASS_MQH__

//--- compile-time defaults (macros are safe in MQL5)
#define CMT_DEFAULT_WIDTH       120
#define CMT_DEFAULT_HEIGHT      112    // quadrupled default
#define CMT_DEFAULT_X_OFFSET    10
#define CMT_DEFAULT_Y_OFFSET    40     // bottom offset from bottom
#define CMT_DEFAULT_SPACING     6
#define CMT_DEFAULT_PERIOD      PERIOD_M1
#define CMT_DEFAULT_CHART_SCALE 2

// toggle button defaults
#define CMT_TOG_X               8
#define CMT_TOG_Y               6
#define CMT_TOG_W               72
#define CMT_TOG_H               20
#define CMT_TOG_NAME            "CMT_ToggleButton"

1.2. 构造函数与析构函数

构造函数借助编译期默认参数,将类初始化至一个确定、安全的状态。它初始化内部数组、子图的默认尺寸与偏移量、切换按钮配置,并将m_top_reserved设置为0(默认不预留顶部区域)。析构函数会调用Delete()方法,保证类实例销毁时,由该类创建的所有图表对象全部被清除。这套确定式的初始化与销毁机制,可以避免图表上残留冗余对象,减少调试麻烦。

//+------------------------------------------------------------------+
//| CChartMiniTiles class declaration                                |
//+------------------------------------------------------------------+
class CChartMiniTiles
  {
public:
                     CChartMiniTiles(void);
                    ~CChartMiniTiles(void);

   bool              Init(const string majorSymbols,
                          const int width = -1,
                          const int height = -1,
                          const int xOffset = -1,
                          const int yOffset = -1,        // bottom offset (pixels from bottom)
                          const int spacing = -1,
                          const int period = -1,
                          const bool dateScale = true,
                          const bool priceScale = false,
                          const int chartScale = -1);

   void              UpdateLayout(void);
   void              Delete(void);

   bool              HandleEvent(const int id,const string sparam); // forward OnChartEvent
   void              SetToggleButtonPos(const int x,const int y);
   void              SetTilesVisible(const bool visible);
   void              Toggle(void);

   // NEW: reserve top area height (pixels from top) that tiles must NOT cover
   void              SetTopReservedHeight(const int pixels);

private:
   // state
   string            m_object_names[];   // object names created
   string            m_symbols[];        // resolved broker symbols
   int               m_count;

   int               m_width;
   int               m_height;
   int               m_xoffset;
   int               m_yoffset;         // bottom offset
   int               m_spacing;
   int               m_period;
   bool              m_date_scale;
   bool              m_price_scale;
   int               m_chart_scale;
   bool              m_visible;

   int               m_tog_x;
   int               m_tog_y;
   int               m_tog_w;
   int               m_tog_h;
   string            m_tog_name;

   // NEW
   int               m_top_reserved;    // pixels from top that must be left free for other UI

private:
   // helpers
   string            MakeObjectName(const string base);
   string            TrimString(const string s);
   string            FindBrokerSymbol(const string baseSymbol);
   int               ComputeBaseYFromTop(void);

   void              CreateToggleButton(void);
   void              DeleteToggleButton(void);
   void              CollapseAll(void);
  };

//+------------------------------------------------------------------+
//| Constructor / Destructor                                         |
//+------------------------------------------------------------------+
CChartMiniTiles::CChartMiniTiles(void)
  {
   m_count       = 0;
   ArrayResize(m_object_names,0);
   ArrayResize(m_symbols,0);

   m_width       = CMT_DEFAULT_WIDTH;
   m_height      = CMT_DEFAULT_HEIGHT;
   m_xoffset     = CMT_DEFAULT_X_OFFSET;
   m_yoffset     = CMT_DEFAULT_Y_OFFSET;
   m_spacing     = CMT_DEFAULT_SPACING;
   m_period      = CMT_DEFAULT_PERIOD;
   m_date_scale  = false;
   m_price_scale = false;
   m_chart_scale = CMT_DEFAULT_CHART_SCALE;
   m_visible     = true;

   m_tog_x = CMT_TOG_X;
   m_tog_y = CMT_TOG_Y;
   m_tog_w = CMT_TOG_W;
   m_tog_h = CMT_TOG_H;
   m_tog_name = CMT_TOG_NAME;

   m_top_reserved = 0; // default: no reserved area
  }

CChartMiniTiles::~CChartMiniTiles(void)
  {
   Delete();
  }

1.3. 辅助方法

这些专用辅助函数用于规范化输入、屏蔽各经纪商之间的差异化细节,从而简化类里其余部分的代码。MakeObjectName :对字符串进行规范化处理,生成安全的对象名称(替换空格与特殊字符)。TrimString:剔除字符串首尾空白字符,包含制表符和换行符。FindBrokerSymbol:先调用SymbolSelect尝试精确匹配;如果匹配失败,再在经纪商的品种列表中进行不区分大小写的搜索。对于存在后缀、命名规则不同的经纪商,该功能是实现跨经纪商兼容的关键。ComputeBaseYFromTop:计算底部锚定迷你子图的垂直基准位置,同时对非法的图表高度值做容错保护。

//+------------------------------------------------------------------+
//| Helpers implementation                                           |
//+------------------------------------------------------------------+
string CChartMiniTiles::MakeObjectName(const string base)
  {
   string name = base;
   StringReplace(name, " ", "_");
   StringReplace(name, ".", "_");
   StringReplace(name, ":", "_");
   StringReplace(name, "/", "_");
   StringReplace(name, "\\", "_");
   return(name);
  }

string CChartMiniTiles::TrimString(const string s)
  {
   if(s == NULL) return("");
   int len = StringLen(s);
   if(len == 0) return("");
   int left = 0;
   int right = len - 1;
   while(left <= right)
     {
      int ch = StringGetCharacter(s, left);
      if(ch == 32 || ch == 9 || ch == 10 || ch == 13) left++;
      else break;
     }
   while(right >= left)
     {
      int ch = StringGetCharacter(s, right);
      if(ch == 32 || ch == 9 || ch == 10 || ch == 13) right--;
      else break;
     }
   if(left > right) return("");
   return StringSubstr(s, left, right - left + 1);
  }

string CChartMiniTiles::FindBrokerSymbol(const string baseSymbol)
  {
   if(StringLen(baseSymbol) == 0) return("");
   if(SymbolSelect(baseSymbol, true))
      return(baseSymbol);

   string baseUpper = baseSymbol; StringToUpper(baseUpper);

   int total = SymbolsTotal(true);
   for(int i = 0; i < total; i++)
     {
      string s = SymbolName(i, true);
      if(StringLen(s) == 0) continue;
      string sUpper = s; StringToUpper(sUpper);
      if(StringFind(sUpper, baseUpper) >= 0)
         return(s);
     }
   return("");
  }

int CChartMiniTiles::ComputeBaseYFromTop(void)
  {
   int chartTotalHeight = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
   if(chartTotalHeight <= 0) chartTotalHeight = 600;
   int base_y_from_top = chartTotalHeight - m_yoffset - m_height;
   if(base_y_from_top < 0) base_y_from_top = 0;
   if(base_y_from_top + m_height > chartTotalHeight) base_y_from_top = MathMax(0, chartTotalHeight - m_height);
   return base_y_from_top;
  }

1.4. 切换按钮控制

交互操作由一个切换按钮实现。CreateToggleButton确保按钮对象存在(不存在则新建),并设置外观属性:位置、尺寸、字体、背景色、文字颜色、是否可选中,按钮初始显示文字由成员变量m_visible决定。DeleteToggleButton用于彻底删除按钮。CollapseAll是轻量隐藏方法:它并不删除迷你子图,而是把它们移出可视区域,并将尺寸置零来实现隐藏。这种方式保留了子图状态,便于快速重新显示,省去重复创建对象带来的性能开销。

//+------------------------------------------------------------------+
//| Toggle button helpers                                            |
//+------------------------------------------------------------------+
void CChartMiniTiles::CreateToggleButton(void)
  {
   if(ObjectFind(ChartID(), m_tog_name) == -1)
      ObjectCreate(ChartID(), m_tog_name, OBJ_BUTTON, 0, 0, 0, 0, 0);

   ObjectSetInteger(ChartID(), m_tog_name, OBJPROP_XDISTANCE, m_tog_x);
   ObjectSetInteger(ChartID(), m_tog_name, OBJPROP_YDISTANCE, m_tog_y);
   ObjectSetInteger(ChartID(), m_tog_name, OBJPROP_XSIZE, m_tog_w);
   ObjectSetInteger(ChartID(), m_tog_name, OBJPROP_YSIZE, m_tog_h);

   ObjectSetString(ChartID(), m_tog_name, OBJPROP_FONT, "Arial");
   ObjectSetInteger(ChartID(), m_tog_name, OBJPROP_FONTSIZE, 10);
   ObjectSetInteger(ChartID(), m_tog_name, OBJPROP_BGCOLOR, clrDarkSlateGray);
   ObjectSetInteger(ChartID(), m_tog_name, OBJPROP_COLOR, clrWhite);
   ObjectSetInteger(ChartID(), m_tog_name, OBJPROP_SELECTABLE, 1);
   ObjectSetInteger(ChartID(), m_tog_name, OBJPROP_STATE, m_visible ? 1 : 0);
   ObjectSetString(ChartID(), m_tog_name, OBJPROP_TEXT, m_visible ? "Tiles: ON" : "Tiles: OFF");
  }

void CChartMiniTiles::DeleteToggleButton(void)
  {
   if(ObjectFind(ChartID(), m_tog_name) >= 0)
      ObjectDelete(ChartID(), m_tog_name);
  }

void CChartMiniTiles::CollapseAll(void)
  {
   for(int i = 0; i < ArraySize(m_object_names); i++)
     {
      string name = m_object_names[i];
      if(StringLen(name) == 0) continue;
      if(ObjectFind(ChartID(), name) == -1) continue;
      ObjectSetInteger(ChartID(), name, OBJPROP_XDISTANCE, -1);
      ObjectSetInteger(ChartID(), name, OBJPROP_YDISTANCE, -1);
      ObjectSetInteger(ChartID(), name, OBJPROP_XSIZE, 0);
      ObjectSetInteger(ChartID(), name, OBJPROP_YSIZE, 0);
     }
  }

1.5. 迷你子图表初始化

Init是核心初始化流程:首先清空原有状态,载入传入参数,无传参则使用默认值;解析以逗号分隔的majorSymbols品种列表,逐个修剪字符串并通过FindBrokerSymbol解析为对应经纪商的真实品种名称,存入m_symbols数组。该方法根据图表宽度、子图尺寸计算布局约束,同时限制最大行数,防止子图侵入m_top_reserved(顶部预留区域)。对于每一个解析成功的品种,创建OBJ_CHART对象,并配置各项属性:交易品种、周期、位置坐标、尺寸、时间/价格坐标轴以及图表缩放模式,同时保存对象名称,供后续更新或删除使用。子图创建完成后,生成切换按钮,遵循初始显示标记,重绘图表,最后返回成功状态。该方法内置日志输出,记录交易品种解析结果与已创建对象,方便直观掌握运行时行为。

//+------------------------------------------------------------------+
//| NEW: set top reserved height (pixels from top)                   |
//+------------------------------------------------------------------+
void CChartMiniTiles::SetTopReservedHeight(const int pixels)
  {
   m_top_reserved = MathMax(0, pixels);
  }

//+------------------------------------------------------------------+
//| Init: create mini tiles                                           |
//+------------------------------------------------------------------+
bool CChartMiniTiles::Init(const string majorSymbols,
                           const int width,
                           const int height,
                           const int xOffset,
                           const int yOffset,
                           const int spacing,
                           const int period,
                           const bool dateScale,
                           const bool priceScale,
                           const int chartScale)
  {
   Delete();

   m_width       = (width  <= 0) ? CMT_DEFAULT_WIDTH  : width;
   m_height      = (height <= 0) ? CMT_DEFAULT_HEIGHT : height;
   m_xoffset     = (xOffset <= 0) ? CMT_DEFAULT_X_OFFSET : xOffset;
   m_yoffset     = (yOffset <= 0) ? CMT_DEFAULT_Y_OFFSET : yOffset;
   m_spacing     = (spacing <= 0) ? CMT_DEFAULT_SPACING : spacing;
   m_period      = (period <= 0) ? CMT_DEFAULT_PERIOD : period;
   m_date_scale  = dateScale;
   m_price_scale = priceScale;
   m_chart_scale = (chartScale <= 0) ? CMT_DEFAULT_CHART_SCALE : chartScale;

   ArrayFree(m_object_names);
   ArrayFree(m_symbols);
   m_count = 0;

   string raw[]; StringSplit(majorSymbols, ',', raw);
   int rawCount = ArraySize(raw);
   if(rawCount == 0) return(false);

   for(int i = 0; i < rawCount; i++)
     {
      string base = TrimString(raw[i]);
      if(StringLen(base) == 0) continue;
      string resolved = FindBrokerSymbol(base);
      if(resolved == "")
        {
         PrintFormat("CMT: symbol not found on this broker: %s", base);
         continue;
        }
      int n = ArraySize(m_symbols);
      ArrayResize(m_symbols, n + 1);
      m_symbols[n] = resolved;
     }

   m_count = ArraySize(m_symbols);
   PrintFormat("CMT: %d symbols resolved for mini-tiles.", m_count);
   for(int i=0;i<m_count;i++) PrintFormat("CMT: symbol[%d] = %s", i, m_symbols[i]);

   if(m_count == 0) return(false);

   ArrayResize(m_object_names, m_count);
   for(int i = 0; i < m_count; i++) m_object_names[i] = "";

   int base_y_from_top = ComputeBaseYFromTop();

   int chartW = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
   if(chartW <= 0) chartW = 800;
   int columns = MathMax(1, chartW / (m_width + m_spacing));

   // --- NEW: limit rows to avoid top reserved region
   int rows = (m_count + columns - 1) / columns;
   int availableAbove = MathMax(0, base_y_from_top - m_top_reserved);
   int maxRowsAllowed = 1 + (availableAbove / (m_height + m_spacing)); // bottom row + how many rows can fit above
   if(maxRowsAllowed < 1) maxRowsAllowed = 1;
   if(rows > maxRowsAllowed)
     {
      // increase columns to fit within allowed rows
      columns = (m_count + maxRowsAllowed - 1) / maxRowsAllowed;
      if(columns < 1) columns = 1;
      rows = (m_count + columns - 1) / columns;
     }
   // ---

   int createdCount = 0;
   for(int i = 0; i < m_count; i++)
     {
      string sym = m_symbols[i];
      string objName = MakeObjectName("CMT_" + sym + "_" + IntegerToString(i));
      m_object_names[i] = objName;

      int col = i % columns;
      int row = i / columns;

      int xdist = m_xoffset + col * (m_width + m_spacing);
      int ydist = base_y_from_top - row * (m_height + m_spacing);
      if(ydist < 0) ydist = 0;

      bool created = ObjectCreate(ChartID(), objName, OBJ_CHART, 0, 0, 0, 0, 0);
      if(!created)
        {
         PrintFormat("CMT: failed to create OBJ_CHART for %s (obj=%s)", sym, objName);
         m_object_names[i] = "";
         continue;
        }

      ObjectSetString(ChartID(), objName, OBJPROP_SYMBOL, sym);
      ObjectSetInteger(ChartID(), objName, OBJPROP_PERIOD, m_period);
      ObjectSetInteger(ChartID(), objName, OBJPROP_XDISTANCE, xdist);
      ObjectSetInteger(ChartID(), objName, OBJPROP_YDISTANCE, ydist);
      ObjectSetInteger(ChartID(), objName, OBJPROP_XSIZE, m_width);
      ObjectSetInteger(ChartID(), objName, OBJPROP_YSIZE, m_height);
      ObjectSetInteger(ChartID(), objName, OBJPROP_DATE_SCALE, (int)m_date_scale);
      ObjectSetInteger(ChartID(), objName, OBJPROP_PRICE_SCALE, (int)m_price_scale);
      ObjectSetInteger(ChartID(), objName, OBJPROP_SELECTABLE, 1);
      ObjectSetInteger(ChartID(), objName, OBJPROP_CHART_SCALE, m_chart_scale);

      createdCount++;
     }

   PrintFormat("CMT: created %d / %d mini-chart objects.", createdCount, m_count);

   CreateToggleButton();

   if(!m_visible)
     SetTilesVisible(false);

   ChartRedraw();
   return(true);
  }

1.6. 布局更新

UpdateLayout会在图表尺寸或者显示状态发生变化时,重新计算各个子图的位置与大小。首先处理隐藏状态:将所有迷你子图收起,并把切换按钮状态更新为“关闭”。当设置为显示时,它根据图表宽度重新计算列数,遵守顶部预留区域的约束(保证子图行不会侵入预留区域),并为每一个OBJ_CHART更新对应的 OBJPROP_XDISTANCE、OBJPROP_YDISTANCE、尺寸以及图表缩放参数。最后将切换按钮状态更新为“开启”,调用ChartRedraw()刷新UI。当图表窗口大小改变,或是EA需要刷新布局时,都应当调用该方法。

//+------------------------------------------------------------------+
//| UpdateLayout - reposition tiles (respects m_visible and top reserved)|
//+------------------------------------------------------------------+
void CChartMiniTiles::UpdateLayout(void)
  {
   if(!m_visible)
     {
      CollapseAll();
      if(ObjectFind(ChartID(), m_tog_name) >= 0)
        {
         ObjectSetInteger(ChartID(), m_tog_name, OBJPROP_XDISTANCE, m_tog_x);
         ObjectSetInteger(ChartID(), m_tog_name, OBJPROP_YDISTANCE, m_tog_y);
         ObjectSetInteger(ChartID(), m_tog_name, OBJPROP_STATE, 0);
         ObjectSetString(ChartID(), m_tog_name, OBJPROP_TEXT, "Tiles: OFF");
        }
      ChartRedraw();
      return;
     }

   if(m_count == 0) return;

   int chartW = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
   if(chartW <= 0) chartW = 800;
   int chartTotalHeight = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
   if(chartTotalHeight <= 0) chartTotalHeight = 600;

   int columns = MathMax(1, chartW / (m_width + m_spacing));

   int base_y_from_top = ComputeBaseYFromTop();

   // --- NEW: ensure rows don't surpass top-reserved area
   int rows = (m_count + columns - 1) / columns;
   int availableAbove = MathMax(0, base_y_from_top - m_top_reserved);
   int maxRowsAllowed = 1 + (availableAbove / (m_height + m_spacing));
   if(maxRowsAllowed < 1) maxRowsAllowed = 1;
   if(rows > maxRowsAllowed)
     {
      columns = (m_count + maxRowsAllowed - 1) / maxRowsAllowed;
      if(columns < 1) columns = 1;
      rows = (m_count + columns - 1) / columns;
     }
   // ---

   for(int i = 0; i < m_count; i++)
     {
      string name = m_object_names[i];
      if(StringLen(name) == 0) continue;
      if(ObjectFind(ChartID(), name) == -1) continue;

      int col = i % columns;
      int row = i / columns;
      int xdist = m_xoffset + col * (m_width + m_spacing);
      int ydist = base_y_from_top - row * (m_height + m_spacing);
      if(ydist < 0) ydist = 0;

      ObjectSetInteger(ChartID(), name, OBJPROP_XDISTANCE, xdist);
      ObjectSetInteger(ChartID(), name, OBJPROP_YDISTANCE, ydist);
      ObjectSetInteger(ChartID(), name, OBJPROP_XSIZE, m_width);
      ObjectSetInteger(ChartID(), name, OBJPROP_YSIZE, m_height);
      ObjectSetInteger(ChartID(), name, OBJPROP_CHART_SCALE, m_chart_scale);
     }

   if(ObjectFind(ChartID(), m_tog_name) >= 0)
     {
      ObjectSetInteger(ChartID(), m_tog_name, OBJPROP_XDISTANCE, m_tog_x);
      ObjectSetInteger(ChartID(), m_tog_name, OBJPROP_YDISTANCE, m_tog_y);
      ObjectSetInteger(ChartID(), m_tog_name, OBJPROP_STATE, 1);
      ObjectSetString(ChartID(), m_tog_name, OBJPROP_TEXT, "Tiles: ON");
     }

   ChartRedraw();
  }

1.7. 显示与切换控制

这一组精简方法用于控制迷你子图的显示状态。SetTilesVisible更新成员标记m_visible,并根据新状态重新放置子图或者将其收起;同时更新切换按钮的状态与显示文字。Toggle是便捷封装函数,用来翻转显示状态并调用SetTilesVisible。SetToggleButtonPos支持动态调整切换按钮位置;如果按钮已存在,则更新它的OBJPROP_XDISTANCE 和 OBJPROP_YDISTANCE。这些方法是代码层面与UI上,对子图组件执行显示、隐藏、重定位操作的入口。

//+------------------------------------------------------------------+
//| Set visibility programmatically                                  |
//+------------------------------------------------------------------+
void CChartMiniTiles::SetTilesVisible(const bool visible)
  {
   m_visible = visible;
   if(m_count == 0)
     {
      if(ObjectFind(ChartID(), m_tog_name) >= 0)
        {
         ObjectSetInteger(ChartID(), m_tog_name, OBJPROP_STATE, m_visible ? 1 : 0);
         ObjectSetString(ChartID(), m_tog_name, OBJPROP_TEXT, m_visible ? "Tiles: ON" : "Tiles: OFF");
         ChartRedraw();
        }
      return;
     }

   if(m_visible)
     UpdateLayout();
   else
     {
      CollapseAll();
      ChartRedraw();
     }

   if(ObjectFind(ChartID(), m_tog_name) >= 0)
     {
      ObjectSetInteger(ChartID(), m_tog_name, OBJPROP_STATE, m_visible ? 1 : 0);
      ObjectSetString(ChartID(), m_tog_name, OBJPROP_TEXT, m_visible ? "Tiles: ON" : "Tiles: OFF");
     }
  }

//+------------------------------------------------------------------+
//| Toggle                                                           |
//+------------------------------------------------------------------+
void CChartMiniTiles::Toggle(void)
  {
   SetTilesVisible(!m_visible);
  }

//+------------------------------------------------------------------+
//| Set toggle position                                               |
//+------------------------------------------------------------------+
void CChartMiniTiles::SetToggleButtonPos(const int x,const int y)
  {
   m_tog_x = x;
   m_tog_y = y;
   if(ObjectFind(ChartID(), m_tog_name) >= 0)
     {
      ObjectSetInteger(ChartID(), m_tog_name, OBJPROP_XDISTANCE, m_tog_x);
      ObjectSetInteger(ChartID(), m_tog_name, OBJPROP_YDISTANCE, m_tog_y);
     }
  }

1.8. 资源清理与事件处理

最后,Delete方法执行完整的清理工作:遍历对象名称数组,逐个删除已创建的OBJ_CHART,移除切换按钮,通过ArrayFree释放数组内存,重置计数变量m_count,并重绘图表。HandleEvent是事件转发方法,由EA的OnChartEvent调用;它筛选CHARTEVENT_OBJECT_CLICK事件以及切换按钮的名称。如果检测到该切换按钮被点击,则调用Toggle(),并返回true,表示该事件已处理完毕。这样简化了EA集成流程:只需要转发事件,并在OnInit/OnDeinit中分别调用Init和Delete即可。

//+------------------------------------------------------------------+
//| Delete all objects                                                |
//+------------------------------------------------------------------+
void CChartMiniTiles::Delete(void)
  {
   for(int i = 0; i < ArraySize(m_object_names); i++)
     {
      string name = m_object_names[i];
      if(StringLen(name) == 0) continue;
      if(ObjectFind(ChartID(), name) >= 0) ObjectDelete(ChartID(), name);
     }
   DeleteToggleButton();
   ArrayFree(m_object_names);
   ArrayFree(m_symbols);
   m_count = 0;
   ChartRedraw();
  }

//+------------------------------------------------------------------+
//| HandleEvent - forward OnChartEvent to class (returns true if handled)|
//+------------------------------------------------------------------+
bool CChartMiniTiles::HandleEvent(const int id,const string sparam)
  {
   if(id != CHARTEVENT_OBJECT_CLICK) return(false);
   if(StringLen(sparam) == 0) return(false);
   if(sparam == m_tog_name)
     {
      Toggle();
      return(true);
     }
   return(false);
  }

//+------------------------------------------------------------------+
#endif // __CHART_MINI_TILES_CLASS_MQH__

读完本文,您可以找到该头文件的完整源码,以及文中提到的其他配套文件。

2.0用于测试该头文件的示例EA(MiniChartsEA)

2.1. EA概述与用途

本EA是CChartMiniTiles类的测试载体。它实例化此类,为多个品种初始化迷你子图表,在集成到大型项目之前,验证切换、窗口缩放以及布局刷新等功能是否正常。

//+------------------------------------------------------------------+
//|                                                  MiniChartsEA.mq5|
//|          Dummy EA to test CChartMiniTiles (ChartMiniTilesClass)  |
//+------------------------------------------------------------------+
#property copyright "2025"
#property link      "https://www.mql5.com/en/users/billionaire2024/seller"
#property version   "1.00"
#property description "Mini-charts EA using CChartMiniTiles class"

// --- Include the class header
#include <ChartMiniTiles.mqh>

2.2. 输入参数与全局变量

本节定义用于控制迷你子图表的参数和状态变量(品种列表、尺寸、更新行为)。

//--- Inputs
input string MajorSymbols      = "EURUSD,GBPUSD,USDJPY,USDCHF,USDCAD,AUDUSD,NZDUSD"; 
input int    BarsWidth         = 20;   // bars used to estimate tile pixel width
input int    TileHeightPx      = 112;  // tile height in pixels
input int    HorizontalSpacing = 6;    // spacing between tiles
input int    UpdateInterval    = 1000; // ms timer update interval
input int    XOffset           = 10;   // left margin in pixels
input int    BottomOffset      = 40;   // distance from bottom in pixels
input int    ToggleButtonX     = 8;    // toggle button X
input int    ToggleButtonY     = 6;    // toggle button Y
input bool   DateScale         = false;// show date scale
input bool   PriceScale        = false;// show price scale
input int    ChartScale        = 2;    // chart zoom level

//--- object instance of our class
CChartMiniTiles tiles;

//--- internal state
int pixelWidth = 120;

2.3. 辅助方法

该方法根据当前可见的K线数量,以像素为单位动态估算子图的宽度。

//+------------------------------------------------------------------+
//| Helper: estimate pixel width from BarsWidth                      |
//+------------------------------------------------------------------+
int CalculateChartWidthFromBars(int barsWidth)
{
   int mainChartWidth = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
   int visibleBars    = (int)ChartGetInteger(0, CHART_VISIBLE_BARS);

   if(visibleBars <= 0 || mainChartWidth <= 0)
      return MathMax(80, BarsWidth * 6); // fallback

   return MathMax(80, barsWidth * mainChartWidth / visibleBars);
}

2.4. 切换按钮控制

由迷你子图对象(CChartMiniTiles)负责处理。EA本身仅负责设置切换按钮的初始位置。

// Inside OnInit we call:
tiles.SetToggleButtonPos(ToggleButtonX, ToggleButtonY);

2.5. 迷你子图表初始化

该阶段根据用户自定义的参数创建全部迷你子图表。

//+------------------------------------------------------------------+
//| Expert initialization                                            |
//+------------------------------------------------------------------+
int OnInit()
{
   // compute pixel width from BarsWidth heuristic
   pixelWidth = CalculateChartWidthFromBars(BarsWidth);

   // set toggle button position
   tiles.SetToggleButtonPos(ToggleButtonX, ToggleButtonY);

   // initialize tiles
   bool ok = tiles.Init(MajorSymbols,
                        pixelWidth,
                        TileHeightPx,
                        XOffset,
                        BottomOffset,
                        HorizontalSpacing,
                        PERIOD_M1,
                        DateScale,
                        PriceScale,
                        ChartScale);

   if(!ok)
   {
      Print("MiniChartsEA: tiles.Init() failed. Check Experts log for symbol issues.");
      return(INIT_FAILED);
   }

   // start timer for adaptive updates
   EventSetMillisecondTimer(UpdateInterval);

   Print("MiniChartsEA initialized.");
   return(INIT_SUCCEEDED);
}

2.6. 布局更新

这样可确保图表尺寸变化或时间推移时,迷你子图都能正确调整大小并重新排布位置。

//+------------------------------------------------------------------+
//| Timer: update layout                                             |
//+------------------------------------------------------------------+
void OnTimer()
{
   tiles.UpdateLayout();
}

2.7. 显示与切换控制

这里,EA将切换按钮的点击事件交由该类处理,同时监听图表变动事件,保证所有组件布局对齐。

//+------------------------------------------------------------------+
//| Chart events - forward to tiles                                  |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
{
   // Let tiles handle toggle button clicks
   if(tiles.HandleEvent(id, sparam))
      return;

   // If chart resized or layout changed, reflow tiles
   if(id == CHARTEVENT_CHART_CHANGE)
      tiles.UpdateLayout();
}

2.8. 资源清理与事件处理

这样确保EA停止时,所有图表对象能够被干净地移除。

//+------------------------------------------------------------------+
//| Deinitialization                                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   EventKillTimer();
   tiles.Delete();
   Print("MiniChartsEA deinitialized.");
}

3.0初步测试

这一步,我们把示例EA加载到图表上并观察运行效果。在我的测试中,图表成功展示了我选定主流货币对的迷你子图,所有品种名称都自动适配了当前经纪商的格式(后缀带.0)。切换按钮运行稳定,可以轻松开启或关闭迷你子图。该功能为交易者提供了灵活度:需要时可完整查看主图表 —— 这是一个简洁却功能强大的控件。

等到将此类集成至新闻标题EA后,该功能的价值会进一步凸显 —— 新闻标题EA内部组件繁多,图表环境更加复杂。支持快速切换迷你子图的显示状态,能够让工作界面更加整洁、便于管理。

MiniChartsEA的部署效果与运行画面如下。

测试MiniChatTilesEA

图1:MiniChartsEA在EURUSD图表上的测试结果

现在,我们将CChartMiniTiles类集成至新闻标题EA中。下一节会详细讲解更新后的代码,并展示图表上的最终运行效果。

4.0将ChartsMiniTiles集成至新闻标题EA中

4.1. 引入ChartMiniTiles头文件,在EA中启用该类

想要使用迷你子图类,必须在EA代码顶部引入其头文件。引入操作会将类的声明与实现并入EA的编译单元,之后就可以创建类实例并调用它的方法。把这条引入语句和其他头文件(交易按钮、画布、交易模块)放在一起,方便统一管理依赖,后续维护人员也能一眼看清依赖关系。如果该头文件不在MQL5/Include/目录下,编译器会在此处报错,因此这是集成工作的第一步。

#include <TradingButtons.mqh>
#include <Trade\Trade.mqh>
#include <Canvas\Canvas.mqh>
#include <ChartMiniTiles.mqh>   // <-- CTM class include (make sure this file is in MQL5/Include/)

4.2. 声明CChartMiniTiles实例 —— 创建EA的子图管理器

声明一个全局CChartMiniTiles tiles对象,让整个EA都可以访问这个唯一的子图管理器。该实例保存运行状态(已创建对象名称、品种列表、显示标记、顶部预留高度),同时对外提供初始化、布局更新、事件处理以及资源清理的相关方法。将它和其他全局变量放在一起声明,方便在OnInit/OnTimer/OnChartEvent/OnDeinit回调函数中管控它的生命周期。

//+------------------------------------------------------------------+
//| ChartMiniTiles instance (CTM)                                     |
//+------------------------------------------------------------------+
CChartMiniTiles tiles;   // class instance for mini tiles

4.3. 构建品种列表并估算子图像素宽度 —— 准备CMT输入

在调用tiles.Init之前,您需要准备一个逗号分隔的品种字符串,以及一个合理的子图像素宽度。本段代码通过JoinSymbolArray()从majorPairs[]数组拼接得到ctmSymbols,并基于主图表宽度与可视K线数量,采用K线—像素换算经验算法计算pixelWidth。在初始化阶段完成该操作,可以让子图尺寸自适应当前图表布局;而经纪商的品种名称适配工作,会交由类内部的FindBrokerSymbol完成。

   // Build a comma-separated symbol list from majorPairs[] and initialize CTM.
   string ctmSymbols = JoinSymbolArray(majorPairs);
   // Estimate pixel width from Bars heuristic (simple fallback)
   int mainChartWidth = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
   int visibleBars    = (int)ChartGetInteger(0, CHART_VISIBLE_BARS);
   int pixelWidth = 120;
   if(visibleBars > 0 && mainChartWidth > 0)
      pixelWidth = MathMax(80, CTM_TileWidthBars * mainChartWidth / visibleBars);

4.4. 放置切换按钮并设置位置 —— 保证UI便于操作

切换按钮是供用户使用的控件,用来显示或隐藏迷你子图。特意将它布置在交易面板的正下方,避免和其他重要UI控件发生重叠。计算出tradingPanelBottomY(交易面板底部Y坐标)之后,EA调用tiles.SetToggleButtonPos(toggleX, toggleY),通知类在该坐标位置创建按钮。该布局设定放在子图初始化之前执行,确保按钮能按此位置创建,并与后续的顶部预留区域逻辑保持一致。

   // Place the CTM toggle button JUST BELOW the trading panel bottom
   int toggleX = 8;
   int toggleY = tradingPanelBottomY + 6; // +6 px margin so it doesn't touch trading controls
   tiles.SetToggleButtonPos(toggleX, toggleY);

 4.5. 为交易控件预留顶部UI区域 —— 防止界面重叠

为避免迷你子图遮挡交易面板与切换按钮,由EA计算出topReserve(顶部预留高度),并调用tiles.SetTopReservedHeight(topReserve)。子图类会依据该数值,限制从底部向上排布的子图最大行数,以此为按钮、画布以及其他UI组件保留顶部空间。在调用Init之前完成顶部区域预留,能够保证布局计算从一开始就遵循该约束。

   // Reserve the area above tiles so the trading UI remains free. We reserve up to the toggle bottom.
   int topReserve = toggleY + CMT_TOG_H + 4; // leave a few px extra
   tiles.SetTopReservedHeight(topReserve);

4.6. 初始化迷你子图 —— 为每个交易品种创建OBJ_CHART对象

这是集成环节的核心调用。EA把解析好的品种列表、计算得到的子图像素宽度、子图高度、偏移量、间距、图表周期以及缩放参数传入tiles.Init(...)中。类内部会适配经纪商专属的品种名称、创建OBJ_CHART对象,并配置对象各项属性(交易品种、周期、刻度、尺寸),同时生成切换按钮。EA会校验布尔类型返回值ctm_ok,妥善地处理初始化失败的情况(例如当前经纪商环境下没有匹配到任何交易品种)。

   // Initialize tiles: (symbols, widthPx, heightPx, xOffset, bottomOffset, spacing, period, dateScale, priceScale, chartScale)
   bool ctm_ok = tiles.Init(ctmSymbols, pixelWidth, CTM_TileHeightPx, CTM_XOffset, CTM_BottomOffset, CTM_Spacing, PERIOD_M1, false, false, CTM_ChartScale);
   if(!ctm_ok)
   {
      Print("CTM: initialization failed (no matching symbols?); tiles disabled.");
   }

4.7. 保持布局自适应 —— 图表变动或定时器触发时调用UpdateLayout

初始化完成后,如果图表尺寸、布局发生变化,EA必须保证迷你子图位置保持正确。集成逻辑包含三处调用位置:

  • 转发图表变动事件,重新排布子图(响应用户拖动缩放窗口或工作区调整等操作);
  • 在OnTimer循环中周期性调用tiles.UpdateLayout(),应对动态UI偏移,或是修复外部改动带来的布局异常;
  • 在主OnTimer处理逻辑末尾调用tiles.UpdateLayout(),保证画布、新闻组件、AI分析面板等其他UI更新完成后,迷你子图管理器(CTM)同步刷新。

在EA中实现上述逻辑的完整代码段如下。

OnChartEvent事件转发与图表变动响应:

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
   // Let CTM handle object clicks first (toggle button)
   if(tiles.HandleEvent(id, sparam))
      return;

   // Forward to the TradingButtons header afterward
   buttonsEA.HandleChartEvent(id, sparam, majorPairs, pairSelected);

   // Also respond to chart change events for CTM layout
   if(id == CHARTEVENT_CHART_CHANGE)
      tiles.UpdateLayout();
}

OnTimer末尾更新(保证每次定时器tick都同步CTM):

   // Keep CTM updated every tick (timer-driven)
   tiles.UpdateLayout();
}

4.8. 反初始化阶段清理资源 —— 删除迷你子图与切换按钮

当EA停止或被卸载时,必须清除它创建的所有图表对象。调用tiles.Delete()删除全部OBJ_CHART内嵌子图以及切换按钮,释放数组内存并重绘图表。这是集成的最后一步,用来防止图表上遗留孤立对象。

   // delete CTM tiles and toggle button
   tiles.Delete();

快速集成核对清单(汇总)

  1. 在代码头部引入:#include <ChartMiniTiles.mqh>;
  2. 全局声明:CChartMiniTiles tiles;
  3. 在Init之前拼接ctmSymbols品种字符串并计算子图像素宽度;
  4. 通过tiles.SetToggleButtonPos(...)设置切换按钮位置;
  5. 通过tiles.SetTopReservedHeight(...)预留顶部UI区域;
  6. 调用tiles.Init(...)完成初始化并校验返回值;
  7. 将OnChartEvent事件转发至tiles.HandleEvent(...);图表变动时以及定时周期内调用tiles.UpdateLayout();
  8. 在OnDeinit中调用tiles.Delete()。


测试结果

这标志着集成后的新闻标题EA进入最终测试阶段。我们在MetaEditor 5中编译程序,编译无报错后,将其部署至MetaTrader 5终端图表上,观察运行效果。我的测试结果截图如下。

图2:在AUDUSD图表上部署新闻标题EA,包含多品种交易迷你子图

多功能新闻标题EA

图 3:ChartMiniTiles在新闻标题EA中正常运行

回顾以上测试结果,我们已经成功通过这套方案解决了之前提出的需求。交易者现在可以在单张主图表上查看多个交易品种,各类控制按钮均按预期工作。本次开发还有一个亮点:依托合理的布局管理以及MQL5语言提供的能力,新增功能完成集成后,并没有遮挡、干扰新闻标题EA原有的功能。下一节为结论部分,总结本次开发的核心成果。


结论

我们成功将一个子图管理类集成到新闻标题EA中,实现了在单张主图表上渲染多个迷你子图表视图。该方案为交易者提供了实用工具,可以同时监控、交易多个品种,提升交易效率,尤其适合重大新闻发布等高波动行情。当在多个交易品种上同时持仓时,交易者可以在同一个界面统一管理、跟踪仓位,快速执行交易操作。

从教学角度来看,本文带来了很有价值的参考思路,包括自定义类开发、多品种交易策略、快速下单以及品种选择功能,所有操作都无需离开当前图表。

虽然本文验证了多品种交易方案的可行性,但仍存在很大的优化空间。尤其是需要进一步优化相关技术,让交易者对选中品种的分析更加灵活。后续迭代开发时,该算法可以接入自动分析模块,进一步增强现有系统。相关升级内容将在后续文章中讲解。

欢迎大家在评论区分享观点、参与讨论。文末附有源码以及一张展示主图表搭配ChartMiniTiles组件效果的图片。



关键经验

关键经验描述:
多交易品种管理交易者可以高效选择并监控多个交易品种,无需反复切换图表。
专用多图表类采用独立类管理多个迷你子图表,简化集成流程,保持代码模块化。
集成前独立测试在单独的测试EA中开发并验证功能,确认稳定后再合并至主EA中。
可自定义迷你子图布局迷你子图支持按照用户偏好调整大小与排布方式,提升可视性、优化操作流程。
多交易品种实时可视化在单张图表上同时展示多个交易品种行情,帮助交易者在剧烈波动的行情下更快做出反应。
模块化编程将功能拆分为头文件与类,便于维护,并且可以在多个项目中复用。
增量式功能开发逐步新增功能,能够减少出错,确保新功能和原有系统平稳集成。
迷你子图的事件处理每一个迷你子图都可以响应用户交互,支持交互式动态分析。
集成至主EA中测试完成后,子图类被合并入主EA,提供流畅的多品种交易功能。
可复用工具类CChartMiniTiles这类类属于模块化工具,可以改造后用于其他项目与EA。
高效开发流程规范采用“先原型、再测试、后集成”的标准化流程,能够提升开发速度并减少缺陷。
快速决策支持迷你子图为交易者提供多市场汇总视图,辅助更快地做出交易决策。
动态对象管理本文代码演示了如何为各个迷你子图动态创建、更新、销毁图表对象,这对于设计可扩展的EA至关重要。
高效内存利用将迷你子图作为独立对象管理,并且仅刷新可视元素,EA以此优化内存与CPU占用。
无缝事件传递该子图类演示了分层处理图表事件的实现方式,保证事件正确传递到对应的子图实例,不会发生冲突。


附件

文件名版本描述
ChartMiniTiles.mqh1.0定义CChartMiniTiles类,该类负责在单张主图表内管理并展示多个迷你子图。提供模块化的多交易品种可视化与交互能力。
MiniChartsEA.mq51.0测试EA,用于在集成入主新闻标题EA之前,独立验证CChartMiniTiles类。校验子图布局、窗口自适应缩放以及事件处理功能。
NewsHeadlineEA.mq51.14集成多项功能的主EA:新闻栏目可视化、基于财经日历事件的自动交易,以及带迷你子图的多品种交易。
TradingButtons.mqh1.0提供图表交互按钮控件,可直接在图表上执行交易、选择交易品种和管理订单。支持EA内多交易品种快速交易交互。
terminal64_Dp0JGQhX5.pngN/A一张宽幅终端图表截图,展示新闻标题EA的ChartMiniTiles功能,呈现多品种交易、实时监控与快速可视化分析效果。

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

附加的文件 |
ChartMiniTiles.mqh (38.64 KB)
MiniChartsEA.mq5 (9.85 KB)
NewsHeadlineEA.mq5 (68.81 KB)
TradingButtons.mqh (38.59 KB)
交易策略 交易策略
各种交易策略的分类都是任意的,下面这种分类强调从交易的基本概念上分类。
交易中的神经网络:分解,而非扩大模型规模(结论) 交易中的神经网络:分解,而非扩大模型规模(结论)
本文将介绍一种算法,可以将时间序列分解为具备明确含义的多层分量,并基于这些分量构建简约模型。我们会系统阐述模型架构、基于MQL5/OpenCL的实际实现,以及基于历史行情数据开展的实际测试。
新手在交易中的10个基本错误 新手在交易中的10个基本错误
新手在交易中会犯的10个基本错误: 在市场刚开始时交易, 获利时不适当地仓促, 在损失的时候追加投资, 从最好的仓位开始平仓, 翻本心理, 最优越的仓位, 用永远买进的规则进行交易, 在第一天就平掉获利的仓位,当发出建一个相反的仓位警示时平仓, 犹豫。
价格行为分析工具包开发(第三十九部分):在MQL5中实现BOS与ChoCH的自动识别 价格行为分析工具包开发(第三十九部分):在MQL5中实现BOS与ChoCH的自动识别
本文介绍分形响应系统(Fractal Reaction System),这是一套精简的MQL5工具,能够将分形拐点转化为可用于交易的市场结构信号。该EA采用K线收盘判定逻辑,避免信号重绘;可以识别行情性质转变(ChoCH)预警、确认结构突破(BOS),在图表上绘制持久图形对象,并对每一个已确认事件生成日志与多渠道告警(桌面端、移动端和声音提醒)。下文将呈现算法设计、实现要点、测试结果以及完整EA源码,您可以自行编译、回测并部署这套信号识别工具。