从新手到专家:使用MQL5实现动态新闻标题(X)——新闻交易的多品种图表视图
内容:
引言
在前一篇文章中,我们介绍了多品种管理功能,交易者可以快速选择自己偏好的交易品种。该功能减少了为切换交易品种而反复切换图表的耗时。
但仍存在一个痛点:交易者虽然可以管理选中的品种,却无法直接调用这些品种的图表 API,因此不方便查看它们的价格以供分析。为解决该问题,本文对多品种交易功能进行优化,开发专门用于多图表可视化的类。该类可在图表内流畅控制,并作为新闻标题EA整套功能的组成部分集成进去。
如果您读到这里觉得思路跟不上,下面就来快速回顾一下我们之前的开发进度。同时建议您回看本主题的往期文章,以便获得更深入的理解。
新闻标题EA的最初版本,核心功能是在图表上划分清晰的栏目,按重要等级展示财经日历的预定事件。“栏目”(lanes)这个设计思路后续不断拓展:我们接入了Alpha Vantage的新闻信息流API、增加本地部署AI模型的分析提示,还新增栏目用于展示内置指标的分析信号。在此基础上,我们推出自动化新闻交易功能,包含专为NFP(非农就业数据)交易开发的专属逻辑,并集成手工交易按钮。
近期,我们进一步开发了多品种交易功能,用于行情剧烈波动时的快速决策。该功能使得交易者在快速变化的市场环境下,管理和交易多个货币对变得简单很多。
而本文,我们将继续优化这套多品种交易方案,让交易者可以直接在EA内查看实时多图表视图。
策略实施
我们的核心原则之一是即使新手读者也能清晰理解相关概念。在本节中,我们拆解为实现该目标所采用的思路与代码实现方案。
随着程序功能的不断扩充,新增功能的集成必须采用结构化、易于维护的开发方式。模块化是我很常用的开发手段,它可以保障开发流程顺畅,同时以头文件和类的形式封装可复用工具。
对于新闻标题EA这类复杂程序,我遵循一套固定开发流程:
- 先开发独立的小型测试程序,专门用来验证新功能;
- 当功能验证可行且运行稳定后,再集成入主EA。
这套开发流程目标明确、能够减少缺陷,并且保证新功能可以平稳接入,不会破坏原有功能。
本次我们的设计任务是创建CChartMiniTiles类,用来在同一张主图表内渲染多个品种的子图表,每个子图尺寸支持自定义。之后我们在一个测试EA(MiniChartsEA)中调用这个类,验证方案可行性。验证通过后,再把该类集成到新闻标题EA中,并做适配,保证运行流畅。
最后需要说明的是,CChartMiniTiles、MiniChartsEA这类名称只是本次演示用的占位名称,您可以自行修改命名,前提是理解程序的运行逻辑。
接下来的四个小节,我们进入代码实现环节:
- ChartMiniTiles头文件
- 用于测试该头文件的示例EA(MiniChartsEA)
- 初步测试
- 将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的部署效果与运行画面如下。

图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();快速集成核对清单(汇总)
- 在代码头部引入:#include <ChartMiniTiles.mqh>;
- 全局声明:CChartMiniTiles tiles;
- 在Init之前拼接ctmSymbols品种字符串并计算子图像素宽度;
- 通过tiles.SetToggleButtonPos(...)设置切换按钮位置;
- 通过tiles.SetTopReservedHeight(...)预留顶部UI区域;
- 调用tiles.Init(...)完成初始化并校验返回值;
- 将OnChartEvent事件转发至tiles.HandleEvent(...);图表变动时以及定时周期内调用tiles.UpdateLayout();
- 在OnDeinit中调用tiles.Delete()。
测试结果
这标志着集成后的新闻标题EA进入最终测试阶段。我们在MetaEditor 5中编译程序,编译无报错后,将其部署至MetaTrader 5终端图表上,观察运行效果。我的测试结果截图如下。

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

图 3:ChartMiniTiles在新闻标题EA中正常运行
回顾以上测试结果,我们已经成功通过这套方案解决了之前提出的需求。交易者现在可以在单张主图表上查看多个交易品种,各类控制按钮均按预期工作。本次开发还有一个亮点:依托合理的布局管理以及MQL5语言提供的能力,新增功能完成集成后,并没有遮挡、干扰新闻标题EA原有的功能。下一节为结论部分,总结本次开发的核心成果。
结论
我们成功将一个子图管理类集成到新闻标题EA中,实现了在单张主图表上渲染多个迷你子图表视图。该方案为交易者提供了实用工具,可以同时监控、交易多个品种,提升交易效率,尤其适合重大新闻发布等高波动行情。当在多个交易品种上同时持仓时,交易者可以在同一个界面统一管理、跟踪仓位,快速执行交易操作。
从教学角度来看,本文带来了很有价值的参考思路,包括自定义类开发、多品种交易策略、快速下单以及品种选择功能,所有操作都无需离开当前图表。
虽然本文验证了多品种交易方案的可行性,但仍存在很大的优化空间。尤其是需要进一步优化相关技术,让交易者对选中品种的分析更加灵活。后续迭代开发时,该算法可以接入自动分析模块,进一步增强现有系统。相关升级内容将在后续文章中讲解。
欢迎大家在评论区分享观点、参与讨论。文末附有源码以及一张展示主图表搭配ChartMiniTiles组件效果的图片。
关键经验
| 关键经验 | 描述: |
|---|---|
| 多交易品种管理 | 交易者可以高效选择并监控多个交易品种,无需反复切换图表。 |
| 专用多图表类 | 采用独立类管理多个迷你子图表,简化集成流程,保持代码模块化。 |
| 集成前独立测试 | 在单独的测试EA中开发并验证功能,确认稳定后再合并至主EA中。 |
| 可自定义迷你子图布局 | 迷你子图支持按照用户偏好调整大小与排布方式,提升可视性、优化操作流程。 |
| 多交易品种实时可视化 | 在单张图表上同时展示多个交易品种行情,帮助交易者在剧烈波动的行情下更快做出反应。 |
| 模块化编程 | 将功能拆分为头文件与类,便于维护,并且可以在多个项目中复用。 |
| 增量式功能开发 | 逐步新增功能,能够减少出错,确保新功能和原有系统平稳集成。 |
| 迷你子图的事件处理 | 每一个迷你子图都可以响应用户交互,支持交互式动态分析。 |
| 集成至主EA中 | 测试完成后,子图类被合并入主EA,提供流畅的多品种交易功能。 |
| 可复用工具类 | CChartMiniTiles这类类属于模块化工具,可以改造后用于其他项目与EA。 |
| 高效开发流程规范 | 采用“先原型、再测试、后集成”的标准化流程,能够提升开发速度并减少缺陷。 |
| 快速决策支持 | 迷你子图为交易者提供多市场汇总视图,辅助更快地做出交易决策。 |
| 动态对象管理 | 本文代码演示了如何为各个迷你子图动态创建、更新、销毁图表对象,这对于设计可扩展的EA至关重要。 |
| 高效内存利用 | 将迷你子图作为独立对象管理,并且仅刷新可视元素,EA以此优化内存与CPU占用。 |
| 无缝事件传递 | 该子图类演示了分层处理图表事件的实现方式,保证事件正确传递到对应的子图实例,不会发生冲突。 |
附件
| 文件名 | 版本 | 描述 |
|---|---|---|
| ChartMiniTiles.mqh | 1.0 | 定义CChartMiniTiles类,该类负责在单张主图表内管理并展示多个迷你子图。提供模块化的多交易品种可视化与交互能力。 |
| MiniChartsEA.mq5 | 1.0 | 测试EA,用于在集成入主新闻标题EA之前,独立验证CChartMiniTiles类。校验子图布局、窗口自适应缩放以及事件处理功能。 |
| NewsHeadlineEA.mq5 | 1.14 | 集成多项功能的主EA:新闻栏目可视化、基于财经日历事件的自动交易,以及带迷你子图的多品种交易。 |
| TradingButtons.mqh | 1.0 | 提供图表交互按钮控件,可直接在图表上执行交易、选择交易品种和管理订单。支持EA内多交易品种快速交易交互。 |
| terminal64_Dp0JGQhX5.png | N/A | 一张宽幅终端图表截图,展示新闻标题EA的ChartMiniTiles功能,呈现多品种交易、实时监控与快速可视化分析效果。 |
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/19299
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
交易中的神经网络:分解,而非扩大模型规模(结论)
新手在交易中的10个基本错误
价格行为分析工具包开发(第三十九部分):在MQL5中实现BOS与ChoCH的自动识别