概论
当开发并使用不同的交易工具时, 我们有时会遇到一些情况, 通过 extern 和 input 修饰符创建输入的标准方法难以满足需求。虽然我们有一个通用的解决方案, 以满足我们的所有需求, 但它有时相当繁琐和不灵活。让我们考虑以下情况作为例子。
-
使用标准工具来创建很少改变或根本不会改变的参数 (即, 订单魔幻数字或滑点值)。
-
如果在指标或 EA 的属性里, 从诸如 RSI, WPR 等十几款指标里选择唯一的一款指标作为数据源, 那么其中每款指标的所需参数均应在属性窗口里创建并可用, 尽管事实上我们仅需其中一套参数。这种类型的另一个例子是每周的 EA 操作日程。在周三时没必要保存周一和周二 (或周五) 相关的日期。但我们不得不这样做, 因为整个日程表位于输入部分。换言之, 在参数里描述动态创建的对象十分困难, 且是效率低下的任务。
-
如果我们需要在工具的属性列表中放置一张便条, 注释或题头, 我们要创建一个带有相应内容的新字符串属性。在某些时候这很不方便。
如何在文本文件中存储参数
纯文本文件可作为附加的便利方法来创建并保存输入参数。您可以把您想要的任何东西放入其中, 除非它们可以很容易地编辑和移动。它们的结构可以如同 INI 文件那样安排。例如, 在文本文件里如何保存整数类数组如下所示:
/*array size*/ {array_size},5 /*array*/ {array_init},0,1,2,3,4
在上述例子里, "锚点" 或 "分区名" 被写在字符串的起始, 紧跟的分区内容以逗号分隔。任何字符组成的字符串可以作为一个 "锚点"。这些文件被生成并保存在终端的 "沙箱" 里。下一步, 我们在指标、EA 或脚本的初始化模块里按照只读模式打开这个文件 (作为 CSV 文件)。
当读取保存的数组时, 我们使用已知的名字 {array_size} 在文件里搜索 "锚点"。我们应当通过调用 FileSeek(handle,0,SEEK_SET) 移动文件指针到文件开始。现在, 让我们使用下一个已知名 {array_init} 来搜索下一个 "锚点"。当有所发现时, 我们简单地读取字符串, 这是根据数组类型进行转换的必要次数 (在此情况下, 执行整数转换)。ConfigFiles.mqh 包含文件包括一些简单函数, 实现了 "锚点" 搜索和数据搜索。
换言之, 我们在所需的 CSV 格式文件里保存的对象一般应按照 "锚点" 紧跟逗号分隔的数据来描述。若有必要, 您可以使用尽可能多地 "锚点" 和随后数据, 但请记住, 一个 "锚点" 仅对应一个字符串。
注释以及便条和指导可以按照各种格式写到文件的任何位置。不过, 这里有一个明显的需求: 文本不得破坏 "锚点" – 数据序列。其它期望条件 – 任何大的文本应位于文件末尾以便简洁地搜索 "锚点"。
您可以在下边看到上述 EA 操作日程保存时的可能排布:
…....... {d0},0,0,22 {d1},0,0,22 {d2},1,0,22 …........ {d6},0,0,22
此处, "锚点" 名反映周内日期数, 使用确定的数字 (例如, {d1} 代表当天的日期号为一, 等等)。"锚点" 名紧随一个布尔类型值, 定义 EA 是否能够在相应的日子里进行交易 (在我们的例子里, 它表示在 d1 不可进行交易)。如果没有交易, 以下数值可以忽略, 但它们依然要保留。最后, 末尾的两个整数值代表 EA 的操作开始和结束时刻。
在日线蜡烛开盘时, EA 读取新一天的操作日程。当然, 每天的数据字符串可依据操作逻辑以其它值修改和补充。因此, 只有当日数据存储在存储器中。
您可以任意设置 "锚点" 名称, 但我建议使用有意义的。给 "锚点" 分配一个抽象名字无关紧要。相反, ta们应能够传达一定的意义并同时保持独特。请记住, 数据搜索是通过名字执行的。根据一个特定实现, 单一文件可以拥有若干相同名称的 "锚点"。
让我们来审查取自完整操作指标的代码片段。该指标需要若干货币对的数据以便进行正确地操作。所以, 它定期请求数据并按照其逻辑处理它 (指标逻辑在此对于我们不重要)。请记住, 券商有时候在品名上添加不同的后缀和前缀 (例如, EURUSD 也许转换为 #.EURUSD.ch)。这个应予考虑, 以便 EA 能够正确参照其它品名。我们的行动的顺序如下。
1. 创建一个文本文件 broker.cfg 内容如下:
[PREFIX_SUFFIX],#.,.ch
2. 创建一个文本文件 <indicator_name>.cfg 内容如下:
/*货币对的数量, 带有 "锚点" 字符串 [CHARTNAMES] */ [CHARTCOUNT],7 /*货币对名称, 其图表由指标读取数据*/ [CHARTNAMES],USDCAD,AUDCAD,NZDCAD,GBPCAD,EURCAD,CADCHF,CADJPY /*在货币对中基础货币的位置 (在此情况下它是 CAD) – 第一个或第二个*/ [DIRECT],0,0,0,0,0,1,13. 将两个文件放入 "沙盒"。
4. 在指标代码里定义定义若干辅助函数 (或包含 ConfigFiles.mqh 文件):
// 按照指定名称打开文件, 比如 CSV 文件并返回其句柄 int __OpenConfigFile(string name) { int h= FileOpen(name,FILE_READ|FILE_SHARE_READ|FILE_CSV,','); if(h == INVALID_HANDLE) PrintFormat("无效文件名 %s",name); return (h); } //+-------------------------------------------------------------------------------------+ //| 函数在包含的分区里读取一个长整数型的 iRes 值 | //| strSec "锚点" 名 位于句柄文件内。如果成功, | //| 返回 true, 否则 – false。 | //+-------------------------------------------------------------------------------------+ bool _ReadIntInConfigSection(string strSec,int handle,long &iRes) { if(!FileSeek(handle,0,SEEK_SET) ) return (false); string s; while(!FileIsEnding(handle)) { if(StringCompare(FileReadString(handle),strSec)==0) { iRes=StringToInteger(FileReadString(handle)); return (true); } } return (false); } //+-------------------------------------------------------------------------------------+ //| 函数在包含的分区里读取已计算长度的 sArray | //| strSec 在句柄文件内的 "锚点" 名。如果成功, | //| 返回 true, 否则 – false。 | //+-------------------------------------------------------------------------------------+ bool _ReadStringArrayInConfigSection(string strSec,int handle,string &sArray[],int count) { if(!FileSeek(handle,0,SEEK_SET) ) return (false); while(!FileIsEnding(handle)) { if(StringCompare(FileReadString(handle),strSec)==0) { ArrayResize(sArray,count); for(int i=0; i<count; i++) sArray[i]=FileReadString(handle); return (true); } } return (false); } //+-------------------------------------------------------------------------------------+ //| 函数在包含的分区里读取已计算长度的布尔型 bArray | //| strSec 在句柄文件内的 "锚点" 名。如果成功, | //| 返回 true, 否则 – false。 | //+-------------------------------------------------------------------------------------+ bool _ReadBoolArrayInConfigSection(string strSec,int handle,bool &bArray[],int count) { string sArray[]; if(!_ReadStringArrayInConfigSection(strSec, handle, sArray, count) ) return (false); ArrayResize(bArray,count); for(int i=0; i<count; i++) { bArray[i]=(bool)StringToInteger(sArray[i]); } return (true); }此外, 指标包含以下代码:
….. input string strBrokerFname = "broker.cfg"; // 包含券商数据的文件名 input string strIndiPreFname = "some_name.cfg"; // 包含指标设置的文件名 ….. string strName[]; // 携带品名的数组 ([CHARTNAMES]) int iCount; // 货币对数量 bool bDir[]; // "顺序" 数组 ([DIRECT]) ….. int OnInit(void) { string prefix,suffix; // 前缀和后缀。用于初始化的必要本地变量 // 但仅使用一次。 prefix= ""; suffix = ""; int h = _OpenConfigFile(strBrokerFname); // 从配置文件里读取前缀和后缀数据。如果出错 // 是固定值, 按照省缺值继续。 if(h!=INVALID_HANDLE) { if(!_GotoConfigSection("[PREFIX_SUFFIX]",h)) { PrintFormat("配置文件出错 %s",strBrokerFname); } else { prefix = FileReadString(h); suffix = FileReadString(h); } FileClose(h); } …. // 读取指标设置。 if((h=__OpenConfigFile(strIndiPreFname))==INVALID_HANDLE) return (INIT_FAILED); // 读取品种数量 if(!_ReadIntInConfigSection("CHARTCOUNT]",h,iCount)) { FileClose(h); return (INIT_FAILED); } // 在读取品种数量后读取品名数组 if(!_ReadStringArrayInConfigSection("[CHARTNAMES]",h,strName,iCount)) { FileClose(h); return (INIT_FAILED); } // 转换品名至相应的样子 for(int i=0; i<iCount; i++) { strName[i]=prefix+strName[i]+suffix; } // 读取布尔型参数数组 if(!_ReadBoolArrayInConfigSection("[DIRECT]",h,bDir,iCount)) { FileClose(h); return (INIT_FAILED); } …. return(INIT_SUCCEEDED); }
两个动态数组和两个紧要的局部变量在代码执行过程中已被初始化。生成的 broker.cfg 文件在您经常有必要手工输入前缀和后缀时派上用场。
可能的应用领域。瑕疵和替代品
除了已经提及的情况, 此建议方法可以令您便利地管理操纵不同货币对的若干指标或 EA 的多个实例。它也可以用于类似单一 "控制中心" 需求的任务。当您无需访问终端本身, 只需要编辑或替换远程配置文本文件时, 此方法也可以派上用场。当只用 FTP 访问安装了终端的电脑时, 这并不罕见。
另一种可能的应用领域出现, 譬如交易员管理着十多台位于不同地点的终端时 (甚至也许在不同国家)。简单地一次性准备好配置文件并发送给所有的终端 "沙箱"。如果我们返回如前所述的涉及日程的例子, 它可以安排每周发送一次来确保所有电脑上的 EA 同步操作。如上所示的指标代码片段, 操作 28 对, 只用所述的两个文件管理同步。
然而, 另一个有趣的应用领域涉及在一个文件中同时保存变量, 和属性区域。在这种情况下, 您能够实现一种读取优先逻辑。为了说明这一点, 让我们考虑一个基于魔幻数字初始化的伪代码片段:
….. extern int Magic=0; ….. int OnInit(void) { ….. if(Magic==0) { // 这意味着用户尚未初始化魔幻数字并 // 保留省缺数值。让我们在文件里搜索魔幻数字变量, 然后 …... } if(Magic==0) { // 依然为零。所以, 在文件里没有这个变量 // 处理当前状况, // 错误退出 return (INIT_FAILED); }
本例揭示了这种方法的另一个优点。在文件正在被多个指标/EA 的实例占用时, 此方法可以略过文件的中心配置, 单独为每个实例执行更彻底地精调。
为了保持客观, 我们先描述方法的缺点。
首先一个 是速度低。不过, 如果您仅在指标/EA 的初始化期间, 或者偶然地 (例如, 在日线蜡烛开盘时) 应用此方法, 这不是太大的问题。
第二点 – 深入细致的方式来支持文档是非常关键的。它难以向交易员证明如何运用这种方式来定制管理工具。这就是在文件里保存罕有变更设置的另一个原因。当然, 最应注意的是准备和编辑文本文件本身。在这个阶段出错可能导致严重的财务后果。
现在, 让我们来看看替代提及任务的解决方案。
第一种是使用 INI 文件。这是一种可靠的数据存储方法。当您不希望写入注册表时, 可使用 INI 文件。它们的结构清晰, 容易理解。以下您可以在 INI 文件里看到我们的第一个例子 (操作日程) 输出:
…....... [d0] Allowed=0 BeginWork=0 EndWork=22 ......... [d2] Allowed=1 BeginWork=0 EndWork=22
在所有的其它方面, 操纵 INI 文件都极其相似 - 它们应被放置在 "沙盒" 里, 其操纵速度也相当低 (虽然比 CSV 文件稍好)。就像使用 CSV 文件, 建议在初始化期间或新蜡烛开盘时使用它。但请记住, 您不能按照您喜欢的方式将注释加进 INI 文件的文本, 因为 INI 文件对于注释有它自己的规则。无论如何, 您最有可能面临的支持文档问题是由文件格式造成的。
这种格式的缺点是您需要包含第三方库。MQL 语言不提供直接操纵 INI 文件的手段, 因此, 您必须从 kernell32.dll 导入它们。您可以在 MQL5 网站上找到若干个提供相关可能性的库, 所以在这里附带一个是没有意义的。这类库很简单, 因为只需导入两个函数。然而, 它们仍然可能包含错误。此外, 您永远无法确定整体构造是否与 Linux 兼容。如果您不在乎第三方库的话, 欢迎您如同 CSV 那样使用 INI 文件。
除了 INI 文件, 还有其它可能的解决方案。让我们来简要描述它们。
- 使用注册表。在我看来, 这是最有问题的解决方案, 因为注册表对于操作系统的正常运行至关重要。因此, 允许脚本和指标在其内写入, 在策略上极其错误。
- 使用该数据库。这是一种存储任意体量数据的可靠方法。但是一款 EA 或指标真的需要如此数量的数据?在大多数情况下, 答案是 "否"。数据库的功能为达到自身的目标明显是冗余的。不过, 若您要存储的数据真的达到几千兆字节, 数据库仍然是便利的。
- 使用 XML。事实上, 这同样是内含不同语法的文本文件, 您需要事先掌握它。另外, 您应该开发一个库 (或下载现成的) 来处理 XML 文件的操纵。最困难的任务是在工作结束时准备支持文档。我相信, 在此情况下是得不偿失。
结论
最后, 我想指出, 文本文件已经广泛地用于交易的各个方面, 例如, 在跟单机制中。MetaTrader 终端生成并使用各种不同的文本文件, 包括配置文件, 各种日志, 邮件和模板。
本文表述了尝试在配置交易工具里采用通用 CSV 文本文件 – EA, 指标和脚本。我已经用几个简单的例子来说明这类文件所提供的新机遇。此外, 对一些替代方案和已检测到的缺点进行了分析。
不管怎样, 应始终按照开发者的当前任务来选择输入存储方法。这种选择的效率是证明开发者专业知识的因素之一。