基于快速数学计算的自定义策略测试器·进阶篇
(2/3)·标准测试器跑轻量想法太重?数学计算模式加字节存储才是真捷径
把目录资源批量编译进 mqh 的实现路径
做 EA 或指标时,若想把一批图片、二进制文件直接以字节数组形式嵌进代码,可以写一个资源转换器类来批量处理。下面这段逻辑展示了从源目录扫描文件、转成 mqh 数组、再聚合成统一包含文件的骨架。 初始化阶段用 for(uchar i='A'; i<'Z'; i++) m_ch[i]=true 预置标志位,SetCommonDirectory 则通过三元表达式切换 FILE_COMMON 标志,决定文件落在终端公共目录还是本地数据目录。 DirectoryToMqhArray 是核心:用 FileFindFirst 拿句柄,do-while 配合 FileFindNext 遍历 src_dir 下所有文件;每个文件经 FileToByteArray 读成 uchar 数组,再由 ByteArrayToMqhArray 写出为 "m_原名.mqh"。实测中若源目录为空,FileFindFirst 返回 INVALID_HANDLE 并在日志打印“目录未发现, 或它未包含文件”,此时函数直接 return,不会生成任何 mqh。 CreateInclude 把前面记录的 CResInfo 列表写成 #include 行,汇成单个 include.mqh。这样在 MT5 里只要包含这一个头文件,就能调用所有资源数组——开 MT5 建个脚本跑这套,验证生成文件数量是否等于源目录文件数。
for(class="type">uchar i = &class="macro">#x27;A&class="macro">#x27;; i < &class="macro">#x27;Z&class="macro">#x27;; i++) m_ch[i] = true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 设置 FILE_COMMON 标志, 或删除它 | class=class="str">"cmt">//+------------------------------------------------------------------+ CCreator::SetCommonDirectory(class="type">bool common) { m_common = common ? FILE_COMMON : class="num">0; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 将 src_dir 目录中包含字节表示的所有文件 | class=class="str">"cmt">//| 转换为 mqh 文件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CCreator::DirectoryToMqhArray(class="type">class="kw">string src_dir,class="type">class="kw">string dst_dir, class="type">bool create_include = false) { class="type">class="kw">string file_name; class="type">class="kw">string file_mqh; CArrayObj list_info; class="type">long h = FileFindFirst(src_dir+"\\*", file_name, m_common); if(h == INVALID_HANDLE) { printf("目录" + src_dir + " 未发现, 或它未包含文件"); class="kw">return; } do { class="type">uchar array[]; if(FileToByteArray(src_dir+file_name, array)) { class="type">class="kw">string norm_name = ToMqhName(file_name); file_mqh = dst_dir + norm_name + ".mqh"; ByteArrayToMqhArray(array, file_mqh, "m_"+norm_name); printf("创建资源: " + file_mqh); class=class="str">"cmt">// 添加有关创建资源的信息 CResInfo* info = new CResInfo(); list_info.Add(info); info.FileName = file_name; info.MqhFileName = norm_name + ".mqh"; info.ArrayName = "m_"+norm_name; } }while(FileFindNext(h, file_name)); if(create_include) CreateInclude(&list_info, dst_dir+"include.mqh"); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建一个包含所有生成文件的 mqh 文件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CCreator::CreateInclude(CArrayObj *list_info, class="type">class="kw">string file_name) { class="type">int handle = FileOpen(file_name, FILE_WRITE|FILE_TXT|m_common); if(handle == INVALID_HANDLE) { printf("创建包含文件失败 " + file_name); class="kw">return; } class=class="str">"cmt">// 创建包含头文件 for(class="type">int i = class="num">0; i < list_info.Total(); i++) { CResInfo* info = list_info.At(i); class="type">class="kw">string line = "class="macro">#include \"" + info.MqhFileName + "\"\n"; FileWriteString(handle, line); } class=class="str">"cmt">// 创建一个函数, 将资源数组复制到调用代码 FileWriteString(handle, "\n");
「把资源文件编译进 MQH 的生成逻辑」
这段 CCreator 类的代码负责把散落的资源文件自动拼成一份可包含的 MQL5 头文件,核心在循环里用 FileWriteString 逐行吐出 CopyResource 函数体。 循环遍历 list_info 里的每个资源项,第一项写 if(file_name == ...) 分支,其余写 else if,再统一用 ArrayCopy 把对应字节数组塞进目标 uchar 数组。这样生成的 MQH 在 EA 里调用一次 CopyResource 就能按文件名取资源,省去手动维护数组映射。 ToMqhName 做文件名到合法变量名的转换:空格替换成下划线,非法字符靠 m_ch 查表过滤,最后 CharArrayToString 收尾。注意它内部留了 int dbg = 4 的死代码,实际没用到,复制时可直接删掉。 FileToByteArray 用 FileOpen 以 FILE_READ|FILE_BIN 读二进制,FileReadArray 整块读进 byte_array,失败则 printf 报错并返回 false。外汇与贵金属 EA 嵌入资源时走这套 IO,回测与实盘都属高风险环境,参数路径错可能引发加载失败。
FileWriteString(handle, "class="type">void CopyResource(class="type">class="kw">string file_name, class="type">uchar &array[])\n"); FileWriteString(handle, "{\n"); for(class="type">int i = class="num">0; i < list_info.Total(); i++) { CResInfo* info = list_info.At(i); if(i == class="num">0) FileWriteString(handle, " if(file_name == \"" + info.FileName + "\")\n"); else FileWriteString(handle, " else if(file_name == \"" + info.FileName + "\")\n"); FileWriteString(handle, " ArrayCopy(array, " + info.ArrayName + ");\n"); } FileWriteString(handle, "}\n"); FileClose(handle); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 将传递的名称转换为 MQL 变量的正确名称 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CCreator::ToMqhName(class="type">class="kw">string name) { class="type">uchar in_array[]; class="type">uchar out_array[]; class="type">int total = StringToCharArray(name, in_array); ArrayResize(out_array, total); class="type">int t = class="num">0; for(class="type">int i = class="num">0; i < total; i++) { class="type">uchar ch = in_array[i]; if(m_ch[ch]) out_array[t++] = ch; else if(ch == &class="macro">#x27; &class="macro">#x27;) out_array[t++] = &class="macro">#x27;_&class="macro">#x27;; class="type">uchar d = out_array[t-class="num">1]; class="type">int dbg = class="num">4; } class="type">class="kw">string line = CharArrayToString(out_array, class="num">0, t); class="kw">return line; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回所传递文件的字节表达作为 | class=class="str">"cmt">//| byte_array 数组 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CCreator::FileToByteArray(class="type">class="kw">string file_name, class="type">uchar& byte_array[]) { class="type">int handle = FileOpen(file_name, FILE_READ|FILE_BIN|m_common); if(handle == -class="num">1) { printf("打开文件失败 " + file_name + ". Reason: " + (class="type">class="kw">string)GetLastError()); class="kw">return false; } FileReadArray(handle, byte_array, WHOLE_ARRAY); FileClose(handle); class="kw">return true; }
◍ 把字节流落盘成可编译的 MQH 数组
做指标或 EA 的资源内嵌时,常需要把二进制字节流写成 uchar 数组头文件,上面这段函数就是干这个的:传入字节数组、文件名和数组名,它会在公共目录(m_common 指定的沙盒路径)下生成一份 .mqh。 关键参数里 chaptersLine = 32 控制每行排 32 个数值,超出就换行并补逗号,这样生成的数组在 MetaEditor 里肉眼可读,不至于一行几万字符卡界面。若字节数组长度为 0 或文件打开失败(handle == -1),函数直接返回 false,调用方得自己判返回值。 外汇与贵金属 EA 涉及文件写操作属高危动作,误写路径可能污染 common 目录;上 MT5 跑之前先确认 m_common 指向的 FileCommon 权限已开,再拿一张小于 1KB 的图测一遍生成结果。
class="type">bool CCreator::ByteArrayToMqhArray(class="type">uchar& byte_array[], class="type">class="kw">string file_name, class="type">class="kw">string array_name) { class="type">int size = ArraySize(byte_array); if(size == class="num">0) class="kw">return false; class="type">int handle = FileOpen(file_name, FILE_WRITE|FILE_TXT|m_common, ""); if(handle == -class="num">1) class="kw">return false; class="type">class="kw">string strSize = (class="type">class="kw">string)size; class="type">class="kw">string strArray = "class="type">uchar " +array_name + "[" + strSize + "] = \n{\n"; FileWriteString(handle, strArray); class="type">class="kw">string line = " "; class="type">int chaptersLine = class="num">32; for(class="type">int i = class="num">0; i < size; i++) { class="type">class="kw">ushort ch = byte_array[i]; line += (class="type">class="kw">string)ch; if(i == size - class="num">1) line += "\n"; if(i>class="num">0 && i%chaptersLine == class="num">0) { if(i < size-class="num">1) line += ",\n"; FileWriteString(handle, line); line = " "; } else if(i < size - class="num">1) line += ","; } if(line != "") FileWriteString(handle, line); FileWriteString(handle, "};"); FileClose(handle); class="kw">return true; }
把压缩字节还原成可遍历的报价数组
历史数据被压成字节数组后,光用 #include "rates.mqh" 把数据塞进程序还不够——rates.mqh 必须放在 EA 源码同目录,否则编译期找不到。真正让数据复活的是 LoadRates:它接收一个空 MqlRates 数组引用,从压缩包里按品种名解包,再逐根填回传统 K 线结构。 函数先用 CZip 从全局 rates 字符数组重建压缩包,再用 ElementByName 按 symbol_name 抽对应文件;抽不到直接返回 -1。解包后的 uchar 流长度除以 sizeof(MqlRates) 就是柱线数,ArrayResize 先扩好目标数组,随后用 union URateToByte 做内存级拷贝,避开逐字段解析。 跑一个极简数学计算模式 EA 验证:OnInit 里 LoadRates 装数据,OnTester 里累加收盘价求平均并回传。策略测试器选「数学计算」跑完,日志显示加载柱线数正确,EURUSD 去年全年限均价算出来是 1.12660。外汇与贵金属属高风险品种,回测均值仅反映历史样本,对未来走势无必然指示。 让小布替你跑这套 把 LoadRates 抄进你自己的 Mtester.mqh,改 symbol_name 为 XAUUSD 重跑,能立刻看到黄金去年均价的差异,比读文档直观得多。
class="macro">#include "rates.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Mtester.mqh | class=class="str">"cmt">//| 版权所有 class="num">2017, MetaQuotes 软件公司| class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "版权所有 class="num">2017, MetaQuotes 软件公司" class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#include <Zip\Zip.mqh> class="macro">#include "rates.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 将 class="type">MqlRates 投射到 class="type">uchar[] | class=class="str">"cmt">//+------------------------------------------------------------------+ union URateToByte { class="type">MqlRates bar; class="type">uchar bar_array[class="kw">sizeof(class="type">MqlRates)]; }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 转换压缩数据至一个 class="type">MqlRates 报价数组 | class=class="str">"cmt">//| 返回接收的柱线数量, 如若失败 | class=class="str">"cmt">//| 返回 -class="num">1 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int LoadRates(class="type">class="kw">string symbol_name, class="type">MqlRates &mql_rates[]) { CZip Zip; Zip.CreateFromCharArray(rates); CZipFile* file = class="kw">dynamic_cast<CZipFile*>(Zip.ElementByName(symbol_name)); if(file == NULL) class="kw">return -class="num">1; class="type">uchar array_rates[]; file.GetUnpackFile(array_rates); URateToByte RateToBar; ArrayResize(mql_rates, ArraySize(array_rates)/class="kw">sizeof(class="type">MqlRates)); for(class="type">int start = class="num">0, i = class="num">0; start < ArraySize(array_rates); start += class="kw">sizeof(class="type">MqlRates), i++) { ArrayCopy(RateToBar.bar_array, array_rates, class="num">0, start, class="kw">sizeof(class="type">MqlRates)); mql_rates[i] = RateToBar.bar; } class="kw">return ArraySize(mql_rates); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+
「用 OnTester 回吐收盘价均值」
EA 初始化时通过 LoadRates 把当前品种的柱线塞进全局 Rates[] 数组,若取不到就直接返回 INIT_PARAMETERS_INCORRECT,这一步决定了后续统计能不能跑起来。 OnInit 里打印的已加载柱线数是真实回测产物:2017.12.13 在 EURUSD 上一次性载入了 354159 根柱线,说明历史深度足够喂满数组而不报空。 测试器函数 OnTester 干的事很直白——把 Rates 里每根 close 累加再除以总数,得到全样本收盘价均值,并以 double 形式返回给优化框架。那次运行返回的均值为 1.126596405653942,也就是 EURUSD 长周期收盘中枢大约在 1.1266 一带。 别把正态当圣经 这个均值只是描述性统计,不代表价格会回归该值;外汇和贵金属杠杆高、跳空频繁,拿它当进场锚点前先在 MT5 策略测试器里复跑一遍确认样本量。
class="macro">#class="kw">property copyright "版权所有 class="num">2017, MetaQuotes 软件公司" class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#include "Mtester.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 测试中用到的报价 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">MqlRates Rates[]; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 智能系统初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//-- 加载指定品种的报价。 if(LoadRates(Symbol(), Rates)==-class="num">1) { printf("报价品种 " + Symbol() + " 未发现。创建相应的报价资源。"); class="kw">return INIT_PARAMETERS_INCORRECT; } printf("已加载 " + (class="type">class="kw">string)ArraySize(Rates) + " 根柱线, 品种为 " + Symbol()); class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 测试器函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double OnTester() { class="type">class="kw">double mean = class="num">0.0; for(class="type">int i = class="num">0; i < ArraySize(Rates); i++) mean += Rates[i].close; mean /= ArraySize(Rates); class="kw">return mean; } class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 从日志看数学校验耗时
在 MT5 策略测试器中跑一段纯数学校验,EURUSD M15 周期下总耗时约 0:00:00.733,而测试器整体工作时间为 0:00:01.342,说明数学部分只占流程的一半多。 峰值内存占用显示 217 Mb,对现代终端不算压力,但在多核并行跑几十个品种时要注意累加。 把这类日志贴到「小布盯盘」的回放面板,能快速判断是代码算法拖慢还是数据加载拖慢;外汇与贵金属波动剧烈,回测结论仅代表历史概率,实盘仍属高风险。
class="num">2017.12.class="num">13 class="num">15:class="num">12:class="num">25.127 Core class="num">2 EURUSD,M15: mathematical test passed in class="num">0:class="num">00:class="num">00.733 (total tester working time class="num">0:class="num">00:class="num">01.342) class="num">2017.12.class="num">13 class="num">15:class="num">12:class="num">25.127 Core class="num">2 class="num">217 Mb memory used