处理 tpl 图表模板
MQL5 API 提供了两个用于处理模板的函数。模板是扩展名为 tpl的文件,用于保存图表的所有内容,即图表的全部设置,以及已绘制的图形、指标和 EA(如有)。
bool ChartSaveTemplate(long chartId, const string filename)
该函数将当前图表的设置保存为指定名称的 .tpl 模板文件。
图表由 chartId参数指定,其中 0 表示当前活动图表。
保存模板的文件名(filename)可以不指定“.tpl”扩展名:系统会自动添加该扩展名。默认情况下,模板会被保存到terminal_dir/Profiles/Templates/文件夹中,随后可以在终端中手动应用这些模板。不过,除了指定文件名外,还可以指定相对于 MQL5 目录的路径,尤其是以“/Files/”开头的路径。因此,我们可以使用 文件 操作函数打开保存的模板,对其进行分析,并在必要时对它们进行编辑(请参阅后续的 ChartTemplate.mq5示例)。
如果指定路径中已存在同名文件,其内容将被覆盖。
稍后,我们将看到一个保存和应用模板的综合示例。
bool ChartApplyTemplate(long chartId, const string filename)
该函数将指定文件中的模板应用到 chartId对应的图表上。
将按照以下规则搜索模板文件:
- 如果 filename包含路径(以反斜杠“\”或正斜杠“/”开头),则模板将相对于目录进行匹配 terminal_data_directory/MQL5.
- 如果文件名中不包含路径,则将在调用该函数的 EX5 文件可执行文件所在的同一位置搜索模板。
- 如果在前两个位置未找到模板,则会在标准模板文件夹 terminal_dir/Profiles/Templates/中搜索模板。
请注意,terminal_data_directory指的是存储修改后文件的文件夹,其位置可能因操作系统类型、用户名和计算机安全设置而异。通常,它与terminal_dir文件夹不同,尽管在某些情况下(例如使用管理员组账户操作时),它们可能会相同。可以使用 TerminalInfoString 函数查找 terminal_data_directory和 terminal_directory 文件夹的位置(请分别参阅 TERMINAL_DATA_PATH 和 TERMINAL_PATH 常量)。
ChartApplyTemplate 调用将创建一个命令,该命令会被添加到图表的消息队列中,且仅在所有先前命令处理完成后才会执行。
加载模板会停止图表上运行的所有 MQL 程序,包括启动加载操作的程序。如果模板中包含指标和 EA 交易,将启动它们的新实例。
出于安全考虑,将包含 EA 交易的模板应用于图表时,可以限制 交易权限 。如果调用ChartApplyTemplate函数的 MQL 程序没有交易权限,则通过模板加载的 EA 交易也将无交易权限,此限制不受模板设置影响。如果调用 ChartApplyTemplate的 MQL 程序被允许交易,但模板设置中禁止交易,则加载的 EA 交易仍无法进行交易。
脚本 ChartDuplicate.mq5的一个示例允许创建当前图表的副本。
void OnStart()
|
首先,使用 ChartSaveTemplate创建一个临时 tpl 模板文件,然后打开新图表(调用 ChartOpen),最后通过 ChartApplyTemplate 函数将该模板应用于新图表。
但在许多情况下,程序员面临更具挑战性的任务:不仅仅是应用模板,还要预先编辑模板内容。
通过使用模板,你可以更改许多无法通过其他 MQL5 API 函数修改的图表特性。例如,在不同时间范围下指标的可见性、指标子窗口的排列顺序以及应用于这些子窗口的对象。
tpl 文件格式与终端在会话之间存储图表时使用的 .chr 文件格式完全相同(位于 terminal_directory/Profiles/Charts/profile_name文件夹中)。
tpl 文件是具有特定语法的文本文件。其中的特性可以是单行书写的 key=value 键值对,也可以是包含多个 key=value 键值对特性的某种分组结构。这类分组在下文中将统称为“容器”,因为除了包含独立特性外,它们还可以包含其他嵌套的容器。
容器以类似“<tag>”的行开头,其中tag是预定义容器类型之一,并以配套的类似“</tag>”行结束(标签名称必须匹配)。换言之,这种格式在某种程度上类似于 XML(但没有头文件),其中所有词法单元必须必须单独成行,且标签特性并非通过其特性(如 XML 中在开始部分 <tag attribute1=value1...>)来表示,而是通过标签内部文本来表示。
支持的标签列表:
- chart - 根容器,包含主图表特性及所有子容器;
- expert - 包含 EA 交易通用特性的容器,例如交易权限(位于 chart 内);
- window - 包含窗口/子窗口特性的容器及其从属容器(位于 chart 内);
- object - 包含图形对象特性的容器(位于 window 内);
- indicator - 包含指标特性的容器(位于 window 内);
- graph - 包含指标图表特性的容器(位于 indicator 内);
- level - 包含指标水平位特性的容器(位于 indicator 内);
- period - 包含对象或指标在特定时间范围下的可见性特性的容器(位于 object 或 indicator 内);
- inputs - 包含自定义指标和 EA 交易的设置的容器(input 变量)。
key=value 键值对形式的特性列表可能非常广泛,且没有官方文档记录。如果有必要,你可以自行处理平台的这些特性。
以下是一个 tpl 文件的片段(格式中的缩进用于直观展示容器的嵌套结构):
<chart>
|
我们有用于处理 tpl 文件的TplFile.mqh头文件,通过该文件,你可以分析和修改模板。它包含两个类:
- Container 用于读取并存储文件元素(考虑层级结构/嵌套关系),以及在可能的修改后写入文件;
- Selector 用于按顺序遍历层级结构中的元素(容器对象),以查找与特定查询匹配的元素,该查询以类似于 XPath 选择器的字符串形式编写(例如 "/path/element[attribute=value]")。
Container类的对象通过构造函数创建,该构造函数将用于读取的 tpl 文件描述符作为第一个参数,将标签名称作为第二个参数。默认情况下,标签名称为 NULL,这表示根容器(整个文件)。因此,容器本身会在读取文件的过程中自动填充内容(请参阅 read方法)。
当前元素的特性(即直接位于此容器内的 "key=value" 键值对)应被添加到MapArray<string,string> properties映射中。嵌套容器会被添加到 Container *children[]数组中。
#include <MQL5Book/MapArray.mqh>
|
在read方法中,我们将读取文件并逐行解析。如果遇到类似 <tag> 的开始标签,我们将创建一个新的容器对象,并在其中继续读取内容。如果遇到类似 </tag> 且名称相同的结束标签,我们将返回一个成功标志(true),表示该容器已生成完毕。在剩余行中,我们将读取 "键 = 值" 键值对并将它们添加到 properties数组中。
我们已经准备好Selector用于在模板中搜索元素。一个包含被搜索标签层级结构的字符串将被传递给其构造函数。例如,字符串 "/chart/window/indicator" 对应一个图表,该图表包含一个窗口/子窗口,而该窗口又包含任意指标。搜索结果将是第一个匹配项。此查询通常会找到报价图表,因为它在模板中作为名为 "Main" 的指标存储,且位于文件开头、其他子窗口之前。
更实用的查询是指定特定特性的名称和值。具体来说,修改后的字符串 "/chart/window/indicator[name=Momentum]" 将仅查找Momentum指标。这种搜索方式与调用 ChartWindowFind不同,因为在此处指定名称不带参数,而 ChartWindowFind 使用指标简称,该简称通常包含参数值,但参数值可能会变化。
对于内置指标,name特性包含指标自身名称;而对于自定义指标,则显示为 "Custom Indicator"。自定义指标的链接在 path特性中以可执行文件路径的形式提供,例如:"Indicators\MQL5Book\IndTripleEMA.ex5"。
我们来看看 Selector类的内部结构。
class Selector
|
在构造函数中,我们将 selector查询分解为独立的组件,并将它们保存到path 数组中。在模式中,当前正在匹配的路径组件由cursor变量提供。搜索开始时,我们位于根容器(即考虑整个 tpl 文件),且cursor为 0。当找到匹配项时,cursor应递增(请参阅下文 accept 方法)。
在该类中,运算符 [] 被重载,通过该运算符可以获取路径的第 i 个片段。它还考虑了在片段中,方括号内可以指定 "[key=value]" 对。
string operator[](int i) const
|
accept方法用于检查元素的名称(tag)及其特性(properties)是否与选择器路径中当前光标位置指定的数据相匹配。this[cursor]记录使用了上述重载的 [] 运算符。
bool accept(const string tag, MapArray<string,string> &properties)
|
如果标签名称与路径的当前片段不匹配,或者片段中包含某个参数的值且该值不相等或不存在于数组 properties中,该方法将返回false。在其他情况下,我们将获得条件匹配,其结果是游标将向前移动(cursor++)且该方法将返回 true。
当游标到达请求中的最后一个片段时,搜索过程将成功完成,因此我们需要一个方法来判断这一时刻,即 isComplete方法。
bool isComplete() const
|
此外,在模板分析过程中可能会出现以下情况:我们遍历了路径的部分容器层级(即找到多个匹配项),但后续的请求片段不再匹配。在这种情况下,需要“返回”到请求的前一级别,为此实现了 unwind方法。
bool unwind()
|
现在,我们已准备好使用Selector对象在容器层级结构(读取 tpl 文件后得到的结构)中执行搜索。所有必要操作都将由 Container类中的find 方法执行。它将 Selector对象作为输入参数,并在根据Selector::accept 方法找到匹配项时递归调用自身。到达请求的末尾意味着成功,此时 find方法会将当前容器返回给调用代码。
Container *find(Selector *selector)
|
请注意,当我们沿着对象树移动时,find方法会记录当前对象的标签名称和嵌套对象的数量,并且记录时会添加与对象嵌套层级成比例的缩进。如果该项与请求匹配,则会在日志条目中追加 "accepted" 一词。
还需要注意的是,此实现会返回第一个匹配的元素,并不再继续搜索其他候选项,从理论上讲,这对模板很有用,因为模板中同一容器内通常会有多个相同类型的标签。例如,一个窗口可能包含许多对象,而 MQL 程序可能需要解析对象的完整列表。这一方面建议作为可选研究内容。
为简化搜索调用,已添加一个同名方法,该方法接受字符串参数并在本地创建 Selector对象。
Container *find(const string selector)
|
由于我们要编辑模板,因此应提供修改容器的方法,特别是用于添加键值对和具有指定标签的新嵌套容器的方法。
void assign(const string key, const string value)
|
编辑完成后,需要将容器的内容写回文件(相同或不同文件)。辅助方法 save将按上述 tpl 格式保存对象:以开始标签 "<tag>" 开头,接着导出所有 key=value 键值对特性,然后对嵌套对象调用save 方法,最后以结束标签 "</tag>" 结尾。文件描述符作为保存操作的参数传入。
bool save(const int h)
|
将整个模板写入文件的高级方法称为write。其输入参数(文件描述符)可以等于 0,这意味着写入的文件就是之前读取的文件。但是,文件必须以可写权限打开。
需要注意的是,当覆盖 Unicode 文本文件时,MQL5 不会写入初始 UTF 标记(即所谓的 BOM,字节顺序标记),因此我们必须自行处理这一步骤。否则,没有该标记的话,终端将无法读取和应用我们的模板。
如果调用代码向 h参数传入另一个专门以 Unicode 格式打开用于写入的文件,MQL5 将自动写入 BOM。
bool write(int h = 0)
|
为了演示新类的功能,我们以隐藏特定指标窗口为例进行说明。如你所知,用户可以通过在指标特性对话框(Display选项卡)中重置时间范围的可见性标志来实现这一点。从编程角度来看,这无法直接实现。这正是模板编辑能力发挥作用的地方。
在模板中,指标对时间范围的可见性是在 <indicator> 容器中指定的,其中为每个可见的时间范围单独编写了一个 <period> 容器。例如,M15 时间范围的可见性如下:
<period>
|
在 <period> 容器内部,会使用 period_type和period_size 特性。 period_type 是一个度量单位,可选值如下:
- 0 表示分钟
- 1 表示小时
- 2 表示周
- 3 表示月
period_size 是时间范围中的度量单位数量。需要注意的是,日线时间范围被指定为 24 小时。
当 <indicator> 容器中没有嵌套的 <period> 容器时,该指标将在所有时间范围中显示。
本书附带了脚本 ChartTemplate.mq5,该脚本可向图表添加 Momentum 指标(如果还不存在),并将其设置为仅在月线时间范围下可见。
void OnStart()
|
接下来,我们将当前图表模板保存到一个文件中,然后打开该文件进行写入和读取操作。可以分配一个单独的文件用于写入。
const string filename = _Symbol + "-" + PeriodToString(_Period) + "-momentum-rw";
|
在获得文件描述符后,我们将创建一个根容器 main,并将整个文件读取到该容器中(嵌套容器及其所有特性将自动读取)。
Container main(handle);
|
然后,我们将定义一个选择器来搜索Momentum指标。理论上,更严谨的方法还需要检查指定周期 (14),但我们的类不支持同时查询多个特性(此可能性留作自主研究)。
使用选择器搜索目标对象,输出找到的结果(仅作参考),并为其添加嵌套的 <period> 容器以设置月线时间范围的显示。
Container *found = main.find("/chart/window/indicator[name=Momentum]");
|
最后,我们将修改后的模板写入同一个文件,关闭该文件并将其应用到图表上。
main.write(); // or main.write(writer);
|
在空白图表上运行该脚本时,我们将在日志中看到如下条目。
ChartSaveTemplate(0,/Files/+filename)=true / ok
|
由此可见,在找到所需指标(标记为 "accepted")之前,算法已先在主窗口中找到该指标,但该指标不符合要求,因为其名称所需的 "Momentum" 不一致。
现在,如果打开图表上的指标列表,将看到 momentum,并且在其特性对话框的 Display 选项卡上,唯一启用的时间范围是 Month。
本书附有TplFileFull.mqh文件的扩展版本,该版本支持在标签选择条件中使用不同的比较运算,以及将多个匹配标签选择到数组中。在脚本 ChartUnfix.mq5中可以找到使用该功能的示例,该脚本可解除所有图表子窗口的固定大小。