基于快速数学计算的自定义策略测试器(基础篇)
用矩阵运算自己跑一遍策略回测
MT5 自带的策略测试器在批量验证思路时仍受可视化与事件循环拖累,对纯数学型策略并不高效。一种做法是绕开图形层,直接在 MQL5 里用数组和矩阵把历史报价读进来,自己写回测循环。
以 2018 年 3 月 5 日发布的示例来看,该自定义测试器在社区获得 7604 次查看与 36 条讨论,说明用快算替代原生测试器的需求真实存在。外汇与贵金属杠杆高、滑点跳空频繁,自写回测得出的胜率与回撤都只是历史概率,实盘可能明显偏离。
核心思路是把 OHLC 缓存进 double 矩阵,再用 ArrayMaximum / ArrayMinimum 定位波段,避免每根 K 线都触发 OnTick。下面这段是原文给出的报价载入骨架,逐行看一遍就能在 MT5 里复现。
class="type">class="kw">double rates[][class="num">6]; class="type">int copied = CopyRates(Symbol(), PERIOD_M1, class="num">0, class="num">1000, rates); if(copied <= class="num">0) class="kw">return; class="type">class="kw">double open[], high[], low[], close[]; for(class="type">int i=class="num">0; i<copied; i++) { open[i] = rates[i][class="num">0]; high[i] = rates[i][class="num">1]; low[i] = rates[i][class="num">2]; close[i] = rates[i][class="num">3]; }
「原型策略与测试器的数据流转」
MQL5 智能系统的开发通常从一套可复用的原型开始:先按数学计算模式保存测试器的品种历史数据,再把压缩后的字节数组还原为 MqlRates 结构,供后续信号计算调用。基于移动均线的原型策略是最直接的验证入口,配合虚拟仓位类与交易模块类,可以在不接入真实账户的情况下跑通开平逻辑。 在优化器中做第一次测试时,建议用分帧机制保存自定义优化结果,并把仓位历史记录以字节表达分帧落盘,这样后续做策略分析器时能直接回放,而不必重跑整个历史。外汇与贵金属品种点差跳空频繁,这类回放验证只能给出概率层面的样本,不代表实盘表现。 实际动手时,先开 MT5 用默认 EURUSD 1H 跑通虚拟仓位类的空单逻辑,再逐步替换均线周期参数,观察优化器输出的分帧文件体积变化,通常 1 年数据在紧凑字节表达下可压到原 MqlRates 数组的 30% 左右。
◍ 用数学模式绕开真实撮合的干扰
MT5 策略测试器默认跑的是接近真实环境的模拟,含点差、滑点、佣金。但当你只想估算一个交易系统的纯净效率,这些摩擦项反而成了噪声。测试器里藏着一个很少被用的数学计算模式,它不撮合真实订单,只做数值运算,却仍保留云计算、遗传优化和自定义数据类型收集的能力。 这个有限模式适合两类人:一是要绝对速度、快速验证简单想法的人;二是做研究的,需要把策略逻辑和经纪商环境拆开看。比如你想知道某均线穿越规则在历史数据上理论命中率多少,不开数学模式,结果会被点差稀释得难以解读。 代价也直白:写一个自定义测试器是认真且耗时的活。本文目标很小——配合合适的函数库,证明这件事没初看那么难。若你常做策略原型验证,值得在 MT5 里切到数学计算模式跑一次基准对照。
有关数学计算模式的一般信息
在策略测试器窗口中启动数学计算模式。为此, 请在下拉列表中选择相同名称的菜单项: 图例 1. 在策略测试器中选择数学计算模式 此模式仅调用一组有限的功能, 且交易环境 (品种, 账户信息, 交易服务器属性) 不可用。OnTester() 成为主要的调用函数, 用户可以使用它来设置特殊的 自定义优化标准 。它将与其它标准优化条件一起使用, 并且可以显示在标准用户策略报告中。它在下面的屏幕截图中以红色勾勒: 图例 2. 自定义优化条件在 OnTester 函数中计算 由 OnTester 函数返回的值是可选的和可优化的。我们用一个简单的智能系统来展示这一点: 它的代码只包含输入参数 x 和 OnTester 函数, 它根据传递的参数计算正弦值。在此例中, x 。现在尝试优化此函数。为此, 请在策略测试器中选择 "慢速完整算法" 优化模式, 和之前的模拟模式: "数学计算"。 在优化参数中设置 x 的变化范围: 开始 — 0.01, 步长 — 0.01, 终止 — 10。一切准备就绪后, 运行策略测试器。它几乎会立即完成其工作。之后, 打开优化图形并在关联菜单中选择 "1D 图"。这将在图形表达中显示正弦函数: 图例 3. 正弦函数的图形表达 这种模式的一个显著特点是资源消耗最少。硬盘的读写操作最小化, 测试器代理无需下载所需品种的报价, 无需额外的计算, 所有计算都集中在 OnTester 函数中。 考虑到 OnTester 的高速度潜力, 可以创建一个能够执行简单模拟的自给自足计算模块。此为这个模块的元素: 测试品种的历史 虚拟仓位系统 用于管理虚拟仓位的交易系统 结果分析系统 模块的自给自足意即单一智能系统将包含测试所需的所有必要数据, 并使用测试系统本身。如果需要进行云优化, 该智能系统可以轻松传递到分布式计算网络。 我们继续介绍系统的第一部分, 即如何存储测试历史。 [CODE] <span class="comment">//+------------------------------------------------------------------+</span>
| <span class="comment">// | OnTesterCheck.mq5 | </span> |
|---|---|---|
| // | 版权所有 2017, MetaQuotes 软件公司 |
<span class="comment">//| &n
「把品种历史压进字节数组留作回测」
数学计算模式跑策略时,直接调 CopyRates(Symbol(),...) 没意义——这个模式下根本不连接交易品种。但回测又必须喂历史,办法是把报价先转成 uchar 字节流,再压成 Zip 存起来。 一根 MqlRates 柱线含开盘时间(datetime 8字节)、开高低收(double 各8字节)、逐笔量(long 8)、点差(int 4)、真实量(long 8),合计 60 字节。24小时外汇市场一天 1440 根分钟柱,一年约 391680 根,未压缩占 23 MB;若攒 10 年就是 230 MB,分发和加载都太重。 用 Zip 函数库压缩后,EURUSD 一年分钟历史从约 20 MB 降到 5 MB。转换靠 union:URateToByte 里放 MqlRates 和等长 uchar 数组,按另一种类型读出字节即可。 压缩完的字节数组通过 CResourceCreator 写成 mqh 头文件,作为资源随 ex5 编译。注意 MetaEditor 别手滑打开 rates.mqh——文本态比 5 MB 大得多,编辑器可能卡死;编译后实际只增 5 MB 字节。这套思路不限于报价,任何信息都能这么塞进程序。
class="type">uchar symbol[class="num">128394] = {0x98,0x32,0xa6,0xf7,0x64,0xbc...}; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| SaveRates.mq5 | class=class="str">"cmt">//| 版权所有 class="num">2016, Vasiliy Sokolov。| class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "版权所有 class="num">2016, Vasiliy Sokolov。" class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#include <Zip\Zip.mqh> class="macro">#include <ResourceCreator.mqh> input ENUM_TIMEFRAMES MainPeriod; union URateToByte { class="type">MqlRates bar; class="type">uchar bar_array[class="kw">sizeof(class="type">MqlRates)]; }RateToByte; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 脚本程序开始函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class=class="str">"cmt">//-- 下载报价 class="type">MqlRates rates[]; class="type">int total = CopyRates(Symbol(), Period(), class="num">0, class="num">20000, rates); class="type">uchar symbol_array[]; class=class="str">"cmt">//-- 将它们转换为字节表示 ArrayResize(symbol_array, class="kw">sizeof(class="type">MqlRates)*total); for(class="type">int i = class="num">0, dst = class="num">0; i < total; i++, dst +=class="kw">sizeof(class="type">MqlRates)) {
◍ 把行情压进 mqh 资源文件
把逐根 K 线转成的字节流直接塞进 zip,再导出成 mqh 头文件,是离线打包历史行情的省事做法。下面这段逻辑先把 symbol_array 用 CZip 压缩,Zip.ToCharArray 拿到字节数组,最后交给 CCreator.ByteArrayToMqhArray 写成 rates.mqh,数组名定为 rates。
RateToByte.bar = rates[i]; ArrayCopy(symbol_array, RateToByte.bar_array, dst, class="num">0, WHOLE_ARRAY); } class=class="str">"cmt">//-- 将它们压缩成一个 zip 存档 CZip Zip; CZipFile* file = new CZipFile(Symbol(), symbol_array); Zip.AddFile(file); class="type">uchar zip_symbol[]; class=class="str">"cmt">//-- 获取压缩存档的字节表示 Zip.ToCharArray(zip_symbol); class=class="str">"cmt">//-- 把它写成一个 mqh 包含文件 CCreator creator; creator.ByteArrayToMqhArray(zip_symbol, "rates.mqh", "rates");
RateToByte.bar = rates[i]; ArrayCopy(symbol_array, RateToByte.bar_array, dst, class="num">0, WHOLE_ARRAY); } class=class="str">"cmt">//-- 将它们压缩成一个 zip 存档 CZip Zip; CZipFile* file = new CZipFile(Symbol(), symbol_array); Zip.AddFile(file); class="type">uchar zip_symbol[]; class=class="str">"cmt">//-- 获取压缩存档的字节表示 Zip.ToCharArray(zip_symbol); class=class="str">"cmt">//-- 把它写成一个 mqh 包含文件 CCreator creator; creator.ByteArrayToMqhArray(zip_symbol, "rates.mqh", "rates");