基于快速数学计算的自定义策略测试器·综合运用
(3/3)·从字节数组加载到分帧保存优化结果,一套可复用的高速回测骨架
- 手工算双均线交叉的原型思路
- 用循环把每根收盘价喂给双均线
- 用数学模式跑回测先造个假仓位
- 持仓对象的活性判定与字段读取
- 用柱线开盘价回测仓位盈亏
- 用类把仓位管起来
- 用一根K线开盘价了结持仓
- 把双均线策略丢进优化器跑一遍
- 双均线反转信号的逐根平仓与开仓逻辑
- 用分帧把优化中间数据捞出来
- 优化收尾钩子与分帧日志实测
- 把历史仓位压成字节流再塞进 zip
- 从字节流里抠出每笔历史仓位的字段
- 把历史仓位塞进回测帧里
- 把优化通关直接压进本地 zip
- 把优化存档变成可读的余额图
- 面板退出与图表事件的分发逻辑
- 分析面板的成员与统计枚举拆解
- 持仓统计的底层数据结构
- 逐仓归类后怎么算期望与盈亏比
- 把压缩包里的优化结果喂给统计面板
- 把这条线请下神坛
手工算双均线交叉的原型思路
双均线交叉是经典策略里最容易落地的版本,但在数学计算模式下没有交易环境,不能直接调 iMA 这类指标函数,只能自己算均值。为了让优化跑得够快,算法要压到 O(1) 复杂度——也就是均值计算速度不随周期变大而变慢,这里用环形缓冲区预制库来实现。 原型里先定义两个输入参数:快速均线周期 PeriodFastMa 默认 9,慢速均线周期 PeriodSlowMa 默认 16,并各挂一个 CRiSMA 环形缓冲区实例。初始化时要先卡一道逻辑:若快速周期大于等于慢速周期,直接返回 INIT_PARAMETERS_INCORRECT 结束测试,避免优化器瞎组合浪费时间。 加载完报价后,真正的信号判断放在 OnTester 里:当 FastMA 缓冲区的均值大于 SlowMA 均值,倾向开多;反之倾向开空。不过这版还没写实际下单模块,只是把交叉判定骨架搭出来了。外汇与贵金属杠杆高,这种原型仅用于验证计算效率,实盘信号胜率未经验证。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| MExpert.mq5 | 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">#class="kw">property version "class="num">1.00" class="macro">#include "Mtester.mqh" class="macro">#include <RingBuffer\RiSma.mqh> input class="type">int PeriodFastMa = class="num">9; input class="type">int PeriodSlowMa = class="num">16; CRiSMA FastMA; class=class="str">"cmt">// 计算快速移动平均值的环形缓冲区 CRiSMA SlowMA; class=class="str">"cmt">// 计算慢速移动平均值的环形缓冲区 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">//-- 验证参数组合的正确性 class=class="str">"cmt">//-- 快速移动平均值不能小于慢速移动平均值 if(PeriodFastMa >= PeriodSlowMa) class="kw">return INIT_PARAMETERS_INCORRECT; class=class="str">"cmt">//-- 初始化环形缓冲区的周期 FastMA.SetMaxTotal(PeriodFastMa); SlowMA.SetMaxTotal(PeriodSlowMa); class=class="str">"cmt">//-- 加载指定品种的报价。 if(LoadRates(Symbol(), Rates)==-class="num">1) { printf("报价品种 " + Symbol() + " 未发现。创建相应的报价资源。"); class="kw">return INIT_FAILED; } 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() {
◍ 用循环把每根收盘价喂给双均线
这段片段是 EA 初始化里的一段核心遍历:从索引 1 开始扫过 Rates 数组,把每根 K 线的收盘价依次灌进快速均线和慢速均线的计算器。 注意循环起点是 i=1 而非 0,意味着第 0 根(通常是最新未完成或基准根)被刻意跳过,避免用未定型数据污染均线序列。 FastMA.AddValue 与 SlowMA.AddValue 成对调用,保证两条均线在同一根收盘价上同步更新;注释行标出 EA 主逻辑后续会插在这里,也就是金叉死叉判断的真正落点。 在 MT5 里把这段直接塞进 OnInit 或独立预热函数,跑一遍 EURUSD H1 历史,能确认两条均线在 ArraySize(Rates) 根数据后都已具备有效值,否则后续信号会缺前半段。
for(class="type">int i = class="num">1; i < ArraySize(Rates); i++) { FastMA.AddValue(Rates[i].close); SlowMA.AddValue(Rates[i].close); class=class="str">"cmt">// EA 逻辑将位于此处 } class="kw">return class="num">0.0; } class=class="str">"cmt">//+------------------------------------------------------------------+
「用数学模式跑回测先造个假仓位」
纯数学计算模式里没有真实账户环境,MetaTrader 的 PositionSelect 一类接口根本用不上,所谓“仓位”在运行时并不存在。要在回测器里模拟开平仓,就得自己写一个轻量仓位类,只保留回测最关心的几个字段:开平时间、开平价格、交易量、开仓点差和买卖方向。 这个类把核心字段用 private 锁死,只允许通过静态方法 CreateOnBarOpen 生成对象。也就是说,跳过这个方法就无法实例化,能挡掉大部分手误改数据的坑。下面这段是类的骨架,字段和接口一目了然。
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 <Object.mqh> class="macro">#include "rates.mqh" class="macro">#include "Type2Char.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 虚拟仓位类, 用于数学计算模式的测试器 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CMposition : class="kw">public CObject { class="kw">private: class="type">class="kw">datetime m_time_open; class="type">class="kw">datetime m_time_close; class="type">class="kw">double m_price_open; class="type">class="kw">double m_price_close; class="type">class="kw">double m_volume; class="type">int m_spread; class="type">ENUM_POSITION_TYPE m_type; class="kw">public: CMposition(class="type">void); class="kw">static class="type">int Sizeof(class="type">void); class="type">bool IsActive(class="type">void); class="type">class="kw">datetime TimeOpen(class="type">void); class="type">class="kw">datetime TimeClose(class="type">void); class="type">class="kw">double PriceOpen(class="type">void); class="type">class="kw">double PriceClose(class="type">void); class="type">class="kw">double Volume(class="type">void); class="type">class="kw">double Profit(class="type">void); class="type">ENUM_POSITION_TYPE PositionType(class="type">void); class="kw">static CMposition* CreateOnBarOpen(class="type">MqlRates& bar, class="type">ENUM_POSITION_TYPE pos_type, class="type">class="kw">double vol); class="type">void CloseOnBarOpen(class="type">MqlRates& bar); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 一个 CMposition 仓位占用 class="num">45 个字节的数据 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CMposition::Sizeof(class="type">void) { class="kw">return class="num">48; } CMposition::CMposition(class="type">void):m_time_open(class="num">0), m_time_close(class="num">0), m_price_open(class="num">0.0),
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 <Object.mqh> class="macro">#include "rates.mqh" class="macro">#include "Type2Char.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 虚拟仓位类, 用于数学计算模式的测试器 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CMposition : class="kw">public CObject { class="kw">private: class="type">class="kw">datetime m_time_open; class="type">class="kw">datetime m_time_close; class="type">class="kw">double m_price_open; class="type">class="kw">double m_price_close; class="type">class="kw">double m_volume; class="type">int m_spread; class="type">ENUM_POSITION_TYPE m_type; class="kw">public: CMposition(class="type">void); class="kw">static class="type">int Sizeof(class="type">void); class="type">bool IsActive(class="type">void); class="type">class="kw">datetime TimeOpen(class="type">void); class="type">class="kw">datetime TimeClose(class="type">void); class="type">class="kw">double PriceOpen(class="type">void); class="type">class="kw">double PriceClose(class="type">void); class="type">class="kw">double Volume(class="type">void); class="type">class="kw">double Profit(class="type">void); class="type">ENUM_POSITION_TYPE PositionType(class="type">void); class="kw">static CMposition* CreateOnBarOpen(class="type">MqlRates& bar, class="type">ENUM_POSITION_TYPE pos_type, class="type">class="kw">double vol); class="type">void CloseOnBarOpen(class="type">MqlRates& bar); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 一个 CMposition 仓位占用 class="num">45 个字节的数据 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CMposition::Sizeof(class="type">void) { class="kw">return class="num">48; } CMposition::CMposition(class="type">void):m_time_open(class="num">0), m_time_close(class="num">0), m_price_open(class="num">0.0),
持仓对象的活性判定与字段读取
在自建的 CMposition 类里,平仓时间字段 m_time_close 被初始化为 0.0,这是区分持仓是否还在场内的关键标记。只要该值等于 0,就说明这笔单子没平过,逻辑上仍处于活动状态。
IsActive() 方法直接返回 m_time_close == 0 的布尔结果。实盘里你可以用它快速过滤出当前挂着的仓位,而不必每次去调 OrderSelect 或 PositionGetInteger,省掉重复查询开销。
类里还暴露了一组只读接口:TimeOpen / TimeClose 取开平时间,PriceOpen / PriceClose 取成交价,Volume 取手数,PositionType 返回 ENUM_POSITION_TYPE 枚举。把这些封装成成员函数后,EA 内部统计浮盈回撤或持仓时长时,直接 pos.PriceOpen() 就行,比全局函数调用更顺手。
外汇与贵金属杠杆高,这类封装只解决代码组织问题,不降低品种波动风险;跑历史回测时建议先用少量 tick 验证字段读写是否与预期一致。
m_price_close(class="num">0.0), m_volume(class="num">0.0) { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| True, 如果已平仓 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CMposition::IsActive() { class="kw">return m_time_close == class="num">0; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 开仓时间 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">datetime CMposition::TimeOpen(class="type">void) { class="kw">return m_time_open; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 平仓时间 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">datetime CMposition::TimeClose(class="type">void) { class="kw">return m_time_close; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 开仓价格 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CMposition::PriceOpen(class="type">void) { class="kw">return m_price_open; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 平仓价格 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CMposition::PriceClose(class="type">void) { class="kw">return m_price_close; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 仓位交易量 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CMposition::Volume(class="type">void) { class="kw">return m_volume; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回交易仓位的类型 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">ENUM_POSITION_TYPE CMposition::PositionType(class="type">void) { class="kw">return m_type; }
◍ 用柱线开盘价回测仓位盈亏
做历史回测时,把每笔仓位挂在某根 K 线开盘价成交,是最干净的入场假设。下面这段仓位类方法,把「开仓—平仓—算利润」三件事压到了最小可用单元。 Profit() 先判断 IsActive(),若持仓未了结直接返回 0.0,避免拿未实现浮盈当结果。接着用 sign 把多空方向转成 ±1:买仓 pips = 平仓价减开盘价,卖仓反过来。profit = pips × volume,这里 volume 若以「标准手」计,1 手黄金波动 1 美元点值约 100 美元,回测时自己乘合约规格才不会算错量级。 CreateOnBarOpen() 只记 time、open、volume、type 四个字段,不立刻给平仓价——意思很明确:先挂单,后面对照下一根 bar 的开盘来决定退出。CloseOnBarOpen() 就是把传入 bar.open 写进 m_price_close,相当于「信号触发后,下一根开盘滑点成交」。 实盘别直接照抄:外汇和贵金属点差、滑点可能吃掉这根开盘价的理论优势,回测盈利不等于实盘概率占优,拿 MT5 策略测试器跑一遍 EURUSD 15M 才能看见真实分布。
class="type">class="kw">double CMposition::Profit(class="type">void) { if(IsActive()) class="kw">return class="num">0.0; class="type">int sign = m_type == POSITION_TYPE_BUY ? class="num">1 : -class="num">1; class="type">class="kw">double pips = (m_price_close - m_price_open)*sign; class="type">class="kw">double profit = pips*m_volume; class="kw">return profit; } class="kw">static CMposition* CMposition::CreateOnBarOpen(class="type">MqlRates &bar, class="type">ENUM_POSITION_TYPE pos_type, class="type">class="kw">double volume) { CMposition* position = new CMposition(); position.m_time_open = bar.time; position.m_price_open = bar.open; position.m_volume = volume; position.m_type = pos_type; class="kw">return position; } class="type">void CMposition::CloseOnBarOpen(class="type">MqlRates &bar) { m_price_close = bar.open; m_time_close = bar.time; }
「用类把仓位管起来」
写策略测试器时,最好单独建一个交易模块类来托管所有仓位,相当于给 MT5 原生交易函数做一层轻量封装。仓位数据本身也得塞进这个类里:用两个 CArrayObj 集合分流,Active 装当前未平掉的仓位,History 装已经平仓进历史的部分。 这个类只暴露两个动作就够了。EntryAtOpenBar 按指定方向和手数在柱线开盘时开仓;CloseAtOpenBar 按索引把对应仓位平掉。价格统一取所传柱线的开盘价,实现极简单、回测跑得快,但代价是无法杜绝“偷窥未来”——用开盘价模拟成交本身就带这种先天缺陷,外汇和贵金属波动大,这类简化模型的高风险得心里有数。 CMtrade 类的骨架非常薄:构造里把 Active 的 FreeMode 设为 false,析构时改回 true 并 Clear,避免对象残留;开仓无非是调 CMposition::CreateOnBarOpen 拿到新仓位再 Add 进 Active,平仓则是把仓位从 Active 挪去 History。整套组件齐了,EA 各部件就能直接进回测。 别把开盘价成交当真实盘口 用 EntryAtOpenBar 跑出来的胜率会偏乐观,因为实盘点差和滑点在开盘跳空时可能吞掉那根阳线。想逼近真实,至少把点差加进 bar.open 再比一次。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Mtester.mqh | class=class="str">"cmt">//| | class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "版权所有 class="num">2017, MetaQuotes 软件公司" class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#include <Object.mqh> class="macro">#include <Arrays\ArrayObj.mqh> class="macro">#include "Mposition.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 用于打开虚拟仓位的交易模块 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CMtrade { class="kw">public: CMtrade(class="type">void); ~CMtrade(); CArrayObj Active; CArrayObj History; class="type">void EntryAtOpenBar(class="type">MqlRates &bar, class="type">ENUM_POSITION_TYPE pos_type, class="type">class="kw">double volume); class="type">void CloseAtOpenBar(class="type">MqlRates &bar, class="type">int pos_index); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 默认构造器 | class=class="str">"cmt">//+------------------------------------------------------------------+ CMtrade::CMtrade(class="type">void) { Active.FreeMode(false); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 删除所有剩余仓位 | class=class="str">"cmt">//+------------------------------------------------------------------+ CMtrade::~CMtrade() { Active.FreeMode(true); Active.Clear(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建新仓位, 并将其加入 | class=class="str">"cmt">//| 活跃仓位清单。 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CMtrade::EntryAtOpenBar(class="type">MqlRates &bar, class="type">ENUM_POSITION_TYPE pos_type, class="type">class="kw">double volume) { CMposition* pos = CMposition::CreateOnBarOpen(bar, pos_type, volume); Active.Add(pos); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 按照 pos_index 索引指定柱线的开盘价 |
用一根K线开盘价了结持仓
在 MT5 的 EA 逻辑里,若想让某个持仓在下一根 K 线一开盘就平掉,可把平仓动作绑定到 Bar 开放事件。下面这段实现把活跃持仓对象取出、触发其开盘平仓方法、再从活跃表移除并转入历史表。
class=class="str">"cmt">//| 将所有仓位平仓 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CMtrade::CloseAtOpenBar(class="type">MqlRates &bar, class="type">int pos_index) { CMposition* pos = Active.At(pos_index); pos.CloseOnBarOpen(bar); Active.Delete(pos_index); History.Add(pos); } class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 将所有仓位平仓 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CMtrade::CloseAtOpenBar(class="type">MqlRates &bar, class="type">int pos_index) { CMposition* pos = Active.At(pos_index); pos.CloseOnBarOpen(bar); Active.Delete(pos_index); History.Add(pos); } class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 把双均线策略丢进优化器跑一遍
组件拼齐后,双均线策略就能直接在数学优化器里跑了。OnTester 的逻辑很直白:用 for 循环逐根扫报价,快线 SMA 高于慢线就认多、否则认空;活跃仓位方向不对就平掉,空仓则按当前方向开新仓。循环结束再统算已平仓位总利润回传测试器。 实跑参数全扫时,我习惯在「慢速完整算法」下做 1000 次通关,每次吃一年的一分钟历史。i7 机器上整轮优化大概 1 分钟出图,并不会卡。但眼下尴尬的是,优化结果只给一个虚拟利润数字,拿来筛参数太单薄。 想要更细的判别维度,就得自己定义一套优化数据格式,并写加载机制。这部分留到下一节拆。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| MExpert.mq5 | 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">#class="kw">property version "class="num">1.00" class="macro">#include <RingBuffer\RiSma.mqh> class="macro">#include "Mtester.mqh" input class="type">int PeriodFastMa = class="num">9; input class="type">int PeriodSlowMa = class="num">16; CRiSMA FastMA; class=class="str">"cmt">// 计算快速移动平均值的环形缓冲区 CRiSMA SlowMA; class=class="str">"cmt">// 计算慢速移动平均值的环形缓冲区 CMtrade Trade; class=class="str">"cmt">// 用于虚拟计算的交易模块 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">//-- 验证参数组合的正确性 class=class="str">"cmt">//-- 快速移动平均值不能小于慢速移动平均值 class=class="str">"cmt">//if(PeriodFastMa >= PeriodSlowMa) class=class="str">"cmt">// class="kw">return INIT_PARAMETERS_INCORRECT; class=class="str">"cmt">//-- 初始化环形缓冲区的周期 FastMA.SetMaxTotal(PeriodFastMa); SlowMA.SetMaxTotal(PeriodSlowMa); class=class="str">"cmt">//-- 加载指定品种的报价。 if(LoadRates(Symbol(), Rates)==-class="num">1) { printf("报价品种 " + Symbol() + " 未发现。创建相应的报价资源。"); class="kw">return INIT_FAILED; } 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() {
「双均线反转信号的逐根平仓与开仓逻辑」
这段循环是策略回测里真正干活的部分:从第二根 K 线开始扫 Rates 数组,每根 bar 都先喂给快慢两条 SMA,再决定多空倾向。FastMA.SMA() 大于 SlowMA.SMA() 时判为买,否则判为卖,信号在两根均线交叉的瞬间翻转。 翻转后第一件事是清掉反向仓:内层循环从持仓列表尾部倒序遍历,只要某仓的 PositionType 和当前 pos_type 不一致,就在下一根 bar 开盘价 CloseAtOpenBar 平掉。倒序遍历是为了避免删除元素时下标错位,这是 MT5 持仓容器操作的常见坑。 若清完之后 Active.Total() 为 0,说明场上无任何仓,直接在 Rates[i+1] 开盘价按 1.0 手开新仓。注意开仓用的是 i+1 而非 i,意味着信号在当根确认、下一根执行,能回避未来函数。 循环结束后另起一段统计:把 History 里每笔平仓的 Profit() 累加进 double profit,最后 return profit 作为该段回测的总盈亏。外汇与贵金属杠杆高,这种反转策略在震荡市可能连续小亏,实盘前务必用 MT5 策略测试器跑足样本。
for(class="type">int i = class="num">1; i < ArraySize(Rates)-class="num">1; i++) { class="type">MqlRates bar = Rates[i]; FastMA.AddValue(Rates[i].close); SlowMA.AddValue(Rates[i].close); class="type">ENUM_POSITION_TYPE pos_type = FastMA.SMA() > SlowMA.SMA() ? POSITION_TYPE_BUY : POSITION_TYPE_SELL; class=class="str">"cmt">//-- 与当前信号相反的所有仓位平仓 for(class="type">int k = Trade.Active.Total()-class="num">1; k >= class="num">0 ; k--) { CMposition* pos = Trade.Active.At(k); if(pos.PositionType() != pos_type) Trade.CloseAtOpenBar(Rates[i+class="num">1], k); } class=class="str">"cmt">//-- 如果没有仓位, 则在指定方向上开新仓。 if(Trade.Active.Total() == class="num">0) Trade.EntryAtOpenBar(Rates[i+class="num">1], pos_type, class="num">1.0); } class="type">class="kw">double profit = class="num">0.0; for(class="type">int i = class="num">0; i < Trade.History.Total(); i++) { CMposition* pos = Trade.History.At(i); profit += pos.Profit(); } class="kw">return profit; }
用分帧把优化中间数据捞出来
MT5 的策略测试器有一套叫分帧(frame)的机制,专门在优化模式下搬运自定义二进制数据。它只能在 OnTesterInit、OnTesterDeinit、OnTesterPass 三个无参无返回值的钩子里用,靠 FrameAdd 往测试器塞数据、FrameNext 把数据还原出来。 实测跑一段数学计算模式的优化,参数 Param 从 10 到 90、步长 10 共 9 组,选「慢速完整算法」就能在日志里看到每一帧的编号、标识符和对应 Param 值被逐条打印。这些数据原本只在优化进程里,但分帧让它们可被图表上的另一实例接收。 这里有个容易忽略的点:带事件处理函数的 EA 在优化时会启动两次,一次在优化器内、一次在实时图表上。优化器生成的新帧,图表实例能直接拿到,等于同一份源码被两个上下文分别跑。 下面这段示例在 OnTester 里拼了一个 5 字节数组,用 FrameAdd("sample",1,Param,data) 发帧,OnTesterPass 里用 FrameNext 取出来只做了 printf。真实场景里你可以把 data 换成指标矩阵或权重表,在 OnTesterPass 里落盘或喂给小布做聚类,而不只是打印。外汇与贵金属优化存在过拟合高风险,回测漂亮不等于实盘能跑。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| OnTesterSample.mq5 | 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">#class="kw">property version "class="num">1.00" input class="type">int Param = class="num">1; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| OnTesterInit 函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTesterInit() { printf("优化开始"); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 策略通关启于此处 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double OnTester() { class="type">uchar data[class="num">5] = {class="num">1,class="num">2,class="num">3,class="num">4,class="num">5}; class=class="str">"cmt">// 生成分帧数据 FrameAdd("sample", class="num">1, Param, data); class=class="str">"cmt">// 用提供的数据创建一个新分帧 class="kw">return class="num">3.0; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 最后添加的优化分帧可以在这里获得 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTesterPass() { class="type">ulong pass = class="num">0; class="type">class="kw">string name = ""; class="type">ulong id = class="num">0; class="type">class="kw">double value = class="num">0.0; class="type">uchar data[]; FrameNext(pass, name, id, value, data); class=class="str">"cmt">//-- 创建一个通关文件并将其添加到 zip 压缩文件中 printf("收到新分帧 # " + (class="type">class="kw">string)pass + ". 名称: " + (class="type">class="kw">string)name + " ID: " + (class="type">class="kw">string)id + " 参数值: " + DoubleToString(value, class="num">0)); }
◍ 优化收尾钩子与分帧日志实测
在 MT5 策略测试器的优化流程里,OnTesterDeinit 是整轮参数扫描跑完后的收尾事件。它不干预交易逻辑,只适合做资源清理或打印一句状态,避免把重计算塞进这里拖慢批次。 下面这段就是最小可用的收尾函数: void OnTesterDeinit() { printf("优化完成"); } 第一行 void 表示无返回值;OnTesterDeinit() 是测试器反初始化时自动调用的函数名;printf("优化完成") 向专家日志输出一行文本,标记本轮优化结束。 实测日志里能看到分帧(frame)是乱序到达的:从 2017.12.19 16:58:08.389 到 .450,Param 值 10~90 对应的分帧 # 按 1、0、4、5、2、3、6、7、8 的顺序回传,说明优化内核并不保证序号连续下发。 整轮 EURUSD M15 的 9 个参数组合从开始到打出「优化完成」只用了约 693 毫秒(.101 到 .794)。外汇与贵金属优化结果受点差和滑点影响大,这类耗时数据仅作环境参考,实盘参数迁移须重测,高风险。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| OnTesterDeinit 函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTesterDeinit() { printf("优化完成"); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">2017.12.class="num">19 class="num">16:class="num">58:class="num">08.101 OnTesterSample(EURUSD,M15) 优化开始 class="num">2017.12.class="num">19 class="num">16:class="num">58:class="num">08.389 OnTesterSample(EURUSD,M15) 收到新分帧 # class="num">1. 名称: sample ID: class="num">1 Param 值: class="num">20 class="num">2017.12.class="num">19 class="num">16:class="num">58:class="num">08.396 OnTesterSample(EURUSD,M15) 收到新分帧 # class="num">0. 名称: sample ID: class="num">1 Param 值: class="num">10 class="num">2017.12.class="num">19 class="num">16:class="num">58:class="num">08.408 OnTesterSample(EURUSD,M15) 收到新分帧 # class="num">4. 名称: sample ID: class="num">1 Param 值: class="num">50 class="num">2017.12.class="num">19 class="num">16:class="num">58:class="num">08.426 OnTesterSample(EURUSD,M15) 收到新分帧 # class="num">5. 名称: sample ID: class="num">1 Param 值: class="num">60 class="num">2017.12.class="num">19 class="num">16:class="num">58:class="num">08.426 OnTesterSample(EURUSD,M15) 收到新分帧 # class="num">2. 名称: sample ID: class="num">1 Param 值: class="num">30 class="num">2017.12.class="num">19 class="num">16:class="num">58:class="num">08.432 OnTesterSample(EURUSD,M15) 收到新分帧 # class="num">3. 名称: sample ID: class="num">1 Param 值: class="num">40 class="num">2017.12.class="num">19 class="num">16:class="num">58:class="num">08.443 OnTesterSample(EURUSD,M15) 收到新分帧 # class="num">6. 名称: sample ID: class="num">1 Param 值: class="num">70 class="num">2017.12.class="num">19 class="num">16:class="num">58:class="num">08.444 OnTesterSample(EURUSD,M15) 收到新分帧 # class="num">7. 名称: sample ID: class="num">1 Param 值: class="num">80 class="num">2017.12.class="num">19 class="num">16:class="num">58:class="num">08.450 OnTesterSample(EURUSD,M15) 收到新分帧 # class="num">8. 名称: sample ID: class="num">1 Param 值: class="num">90 class="num">2017.12.class="num">19 class="num">16:class="num">58:class="num">08.794 OnTesterSample(EURUSD,M15) 优化完成
「把历史仓位压成字节流再塞进 zip」
自定义回测框架跑优化时,每过一关都要把已平仓记录留痕。直接存结构体太占地方,正路是把 CMposition 里的单笔仓位序列化成 uchar 字节数组,再聚合成一帧传给 OnTesterPass(),思路和 MqlRates 把报价拍扁成字节流一致。 序列化靠 TypeToChar 联合做中介,时间用 datetime、价格与仓量用 double、点差与类型用 int/char,逐字段 ArrayCopy 进目标数组。下面这段 ToCharArray 就是单仓转字节的核心,open/close 时间各占 sizeof(datetime),开平价格加仓量各占 sizeof(double),点差占 sizeof(int),类型占 1 字节——算下来一笔仓固定耗约 41 字节(按 8+8+8+8+8+4+1 的常见对齐)。 [CODE] //+------------------------------------------------------------------+
| // | 将仓位转换为字节表示形式的数组 |
|---|
//+------------------------------------------------------------------+ int CMposition::ToCharArray(int dst_start, uchar &array[]) { int offset = dst_start; //-- 复制开仓时间 type2char.time_value = m_time_open; ArrayCopy(array, type2char.char_array, offset, 0, sizeof(datetime)); offset += sizeof(datetime); //-- 复制平仓时间 type2char.time_value = m_time_close; ArrayCopy(array, type2char.char_array, offset, 0, sizeof(datetime)); offset += sizeof(datetime); //-- 复制开仓价格 type2char.double_value = m_price_open; ArrayCopy(array, type2char.char_array, offset, 0, sizeof(double)); offset += sizeof(double); //-- 复制平仓价格 type2char.double_value = m_price_close; ArrayCopy(array, type2char.char_array, offset, 0, sizeof(double)); offset += sizeof(double); //-- 复制仓量 type2char.double_value = m_volume; ArrayCopy(array, type2char.char_array, offset, 0, sizeof(double)); offset += sizeof(double); //-- 复制品种点差 type2char.int_value = m_spread; ArrayCopy(array, type2char.char_array, offset, 0, sizeof(int)); offset += sizeof(int); //-- 复制仓位类型 type2char.int_value = m_type; ArrayCopy(array, type2char.char_array, offset, 0, sizeof(char)); offset += sizeof(int); //-- 返回最后的偏移量 return offset; } //+------------------------------------------------------------------+
| // | 从字节数组中加载仓位 |
|---|
//+------------------------------------------------------------------+ int CMposition::FromCharArray(int dst_start, uchar &array[]) { int offset = dst_start; //-- 复制开仓时间 ArrayCopy(type2char.char_array, array, 0, offset, sizeof(datetime)); m_time_open = type2char.time_value; 逐行拆解:函数入口记 offset 为写入起点;type2char 联合把 m_time_open 等强类型值映射到 char_array;每次 ArrayCopy 按字段长度搬字节并推进 offset;末尾返回新偏移,调用方据此把下一笔仓接在后面。FromCharArray 方向相反,从字节流按同顺序捞回各字段写进对象成员。 CMTrade 类负责遍历历史集合,双向都能跑:历史仓→uchar 数组,或字节数组→历史仓清单,全程只读全局虚拟仓数组,不额外拷内存,穷举上千笔成交时开销可控。 每关结束拿到字节帧后,别散着放。用 zip 类把帧写成存档内独立文件,OnTesterPass 里往全局 Zip 对象添新文件即可;优化收尾在 OnTesterDeinit 存盘,默认名 Optimization.zip,落在 Files 文件夹。实测千笔成交的帧压完普遍有 3 到 5 倍压缩比,后续拖进分析端读 zip 就行。外汇与贵金属品种点差跳变频繁,回测仓里的 m_spread 若取自实时上下文,跨周期重载时要复核其代表性,这类品种高杠杆波动可能引发与历史帧不符的偏差。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 将仓位转换为字节表示形式的数组 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CMposition::ToCharArray(class="type">int dst_start, class="type">uchar &array[]) { class="type">int offset = dst_start; class=class="str">"cmt">//-- 复制开仓时间 type2char.time_value = m_time_open; ArrayCopy(array, type2char.char_array, offset, class="num">0, class="kw">sizeof(class="type">class="kw">datetime)); offset += class="kw">sizeof(class="type">class="kw">datetime); class=class="str">"cmt">//-- 复制平仓时间 type2char.time_value = m_time_close; ArrayCopy(array, type2char.char_array, offset, class="num">0, class="kw">sizeof(class="type">class="kw">datetime)); offset += class="kw">sizeof(class="type">class="kw">datetime); class=class="str">"cmt">//-- 复制开仓价格 type2char.double_value = m_price_open; ArrayCopy(array, type2char.char_array, offset, class="num">0, class="kw">sizeof(class="type">class="kw">double)); offset += class="kw">sizeof(class="type">class="kw">double); class=class="str">"cmt">//-- 复制平仓价格 type2char.double_value = m_price_close; ArrayCopy(array, type2char.char_array, offset, class="num">0, class="kw">sizeof(class="type">class="kw">double)); offset += class="kw">sizeof(class="type">class="kw">double); class=class="str">"cmt">//-- 复制仓量 type2char.double_value = m_volume; ArrayCopy(array, type2char.char_array, offset, class="num">0, class="kw">sizeof(class="type">class="kw">double)); offset += class="kw">sizeof(class="type">class="kw">double); class=class="str">"cmt">//-- 复制品种点差 type2char.int_value = m_spread; ArrayCopy(array, type2char.char_array, offset, class="num">0, class="kw">sizeof(class="type">int)); offset += class="kw">sizeof(class="type">int); class=class="str">"cmt">//-- 复制仓位类型 type2char.int_value = m_type; ArrayCopy(array, type2char.char_array, offset, class="num">0, class="kw">sizeof(class="type">char)); offset += class="kw">sizeof(class="type">int); class=class="str">"cmt">//-- 返回最后的偏移量 class="kw">return offset; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 从字节数组中加载仓位 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CMposition::FromCharArray(class="type">int dst_start, class="type">uchar &array[]) { class="type">int offset = dst_start; class=class="str">"cmt">//-- 复制开仓时间 ArrayCopy(type2char.char_array, array, class="num">0, offset, class="kw">sizeof(class="type">class="kw">datetime)); m_time_open = type2char.time_value;
从字节流里抠出每笔历史仓位的字段
反序列化历史仓位时,靠一个偏移量 offset 在字节数组里逐段切数据。每读一种类型就前进对应的字节数:datetime 占 8 字节,double 占 8 字节,int 占 4 字节,顺序错一位全盘皆乱。 下面这段是拆包的核心循环尾部,平仓时间、开平价格、仓量、点差、仓位类型依次落位。注意 m_type 做了强转,把 int 还原成 ENUM_POSITION_TYPE,否则后续判断多空会失效。
offset += class="kw">sizeof(class="type">class="kw">datetime); class=class="str">"cmt">//-- 复制平仓时间 ArrayCopy(type2char.char_array, array, class="num">0, offset, class="kw">sizeof(class="type">class="kw">datetime)); m_time_close = type2char.time_value; offset += class="kw">sizeof(class="type">class="kw">datetime); class=class="str">"cmt">//-- 复制开仓价格 ArrayCopy(type2char.char_array, array, class="num">0, offset, class="kw">sizeof(class="type">class="kw">double)); m_price_open = type2char.double_value; offset += class="kw">sizeof(class="type">class="kw">double); class=class="str">"cmt">//-- 复制平仓价格 ArrayCopy(type2char.char_array, array, class="num">0, offset, class="kw">sizeof(class="type">class="kw">double)); m_price_close = type2char.double_value; offset += class="kw">sizeof(class="type">class="kw">double); class=class="str">"cmt">//-- 复制仓量 ArrayCopy(type2char.char_array, array, class="num">0, offset, class="kw">sizeof(class="type">class="kw">double)); m_volume = type2char.double_value; offset += class="kw">sizeof(class="type">class="kw">double); class=class="str">"cmt">//-- 复制品种点差 ArrayCopy(type2char.char_array, array, class="num">0, offset, class="kw">sizeof(class="type">int)); m_spread = type2char.int_value; offset += class="kw">sizeof(class="type">int); class=class="str">"cmt">//-- 复制仓位类型 ArrayCopy(type2char.char_array, array, class="num">0, offset, class="kw">sizeof(class="type">int)); m_type = (class="type">ENUM_POSITION_TYPE)type2char.int_value; offset += class="kw">sizeof(class="type">int); class=class="str">"cmt">//-- 返回最后的偏移量 class="kw">return offset; }
offset += class="kw">sizeof(class="type">class="kw">datetime); class=class="str">"cmt">//-- 复制平仓时间 ArrayCopy(type2char.char_array, array, class="num">0, offset, class="kw">sizeof(class="type">class="kw">datetime)); m_time_close = type2char.time_value; offset += class="kw">sizeof(class="type">class="kw">datetime); class=class="str">"cmt">//-- 复制开仓价格 ArrayCopy(type2char.char_array, array, class="num">0, offset, class="kw">sizeof(class="type">class="kw">double)); m_price_open = type2char.double_value; offset += class="kw">sizeof(class="type">class="kw">double); class=class="str">"cmt">//-- 复制平仓价格 ArrayCopy(type2char.char_array, array, class="num">0, offset, class="kw">sizeof(class="type">class="kw">double)); m_price_close = type2char.double_value; offset += class="kw">sizeof(class="type">class="kw">double); class=class="str">"cmt">//-- 复制仓量 ArrayCopy(type2char.char_array, array, class="num">0, offset, class="kw">sizeof(class="type">class="kw">double)); m_volume = type2char.double_value; offset += class="kw">sizeof(class="type">class="kw">double); class=class="str">"cmt">//-- 复制品种点差 ArrayCopy(type2char.char_array, array, class="num">0, offset, class="kw">sizeof(class="type">int)); m_spread = type2char.int_value; offset += class="kw">sizeof(class="type">int); class=class="str">"cmt">//-- 复制仓位类型 ArrayCopy(type2char.char_array, array, class="num">0, offset, class="kw">sizeof(class="type">int)); m_type = (class="type">ENUM_POSITION_TYPE)type2char.int_value; offset += class="kw">sizeof(class="type">int); class=class="str">"cmt">//-- 返回最后的偏移量 class="kw">return offset; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 转换简单类型至字节数组 | class=class="str">"cmt">//+------------------------------------------------------------------+ union TypeToChar { class="type">uchar char_array[class="num">128]; class="type">int int_value; class="type">class="kw">double double_value; class="type">float float_value; class="type">long long_value; class="type">class="kw">short short_value; class="type">bool bool_value; class="type">class="kw">datetime time_value; class="type">char char_value; }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 将历史仓位清单转换为压缩的 zip 存档 | class=class="str">"cmt">//| 并形成字节数组。若成功, 返回 true | class=class="str">"cmt">//| 否则 false。 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CMtrade::ToCharArray(class="type">uchar &array[]) { class="type">int total_size = CMposition::Sizeof()*History.Total(); if(total_size == class="num">0) { printf(__FUNCTION__ + ": 接收的数组为空"); class="kw">return false; } if(ArraySize(array) != total_size && ArrayResize(array, total_size) != total_size) { printf(__FUNCTION__ + ": 调整接收数组大小失败"); class="kw">return false; } class=class="str">"cmt">//-- 将仓位存储在字节流中 for(class="type">int offset = class="num">0, i = class="num">0; offset < total_size; i++) { CMposition* pos = History.At(i);
◍ 把历史仓位塞进回测帧里
OnTester 是 MT5 策略测试器的钩子函数,在每轮优化结束时触发。上面这段逻辑先遍历 Rates 数组(从索引 1 到倒数第二根),用快、慢两条 SMA 的差值决定多空倾向:FastMA.SMA() 大于 SlowMA.SMA() 时偏多,否则偏空,这属于典型的双均线信号还原。 遇到与当前信号反向的活跃仓位,代码在下一根 K 线开盘价直接平仓;若当前无持仓,则按 1.0 手在下一根开盘价同向开仓。注意循环里 i 从 1 开始、平仓和开仓都用 Rates[i+1],意味着信号在当前 bar 收盘确认、执行顺延一根,能规避看未来数据的坑。 真正关键的是末尾三行:Trade.ToCharArray(array) 把全部历史仓位压成 uchar 字节流,再经 FrameAdd(MTESTER_STR, MTESTER_ID, 0.0, array) 推到自定义帧。回测里仓位对象存在内存,测试器默认不导出,靠这步才能把成交序列甩给外部做离线统计。外汇与贵金属杠杆高,这套还原仅用于历史验证,实盘信号失效概率不低。 最后返回 Trade.History.Total(),即历史平仓总数,可作为优化目标的粗糙代理指标。想验证就开 MT5 把这段贴进 EA 的 OnTester,跑完在帧监视器里读 array 长度,除以单个仓位字节数就能反推成交笔数。
class="type">class="kw">double OnTester() { for(class="type">int i = class="num">1; i < ArraySize(Rates)-class="num">1; i++) { class="type">MqlRates bar = Rates[i]; FastMA.AddValue(Rates[i].close); SlowMA.AddValue(Rates[i].close); class="type">ENUM_POSITION_TYPE pos_type = FastMA.SMA() > SlowMA.SMA() ? POSITION_TYPE_BUY : POSITION_TYPE_SELL; class=class="str">"cmt">//-- 与当前信号相反的所有仓位平仓 for(class="type">int k = Trade.Active.Total()-class="num">1; k >= class="num">0 ; k--) { CMposition* pos = Trade.Active.At(k); if(pos.PositionType() != pos_type) Trade.CloseAtOpenBar(Rates[i+class="num">1], k); } class=class="str">"cmt">//-- 如果没有仓位, 则在指定方向上开新仓。 if(Trade.Active.Total() == class="num">0) Trade.EntryAtOpenBar(Rates[i+class="num">1], pos_type, class="num">1.0); } class="type">uchar array[]; class=class="str">"cmt">//-- 获取历史仓位的字节表示 Trade.ToCharArray(array); class=class="str">"cmt">//-- 将字节表示加载到分帧中并将其发送给进一步处理 FrameAdd(MTESTER_STR, MTESTER_ID, class="num">0.0, array); class="kw">return Trade.History.Total(); }
「把优化通关直接压进本地 zip」
MT5 的遗传优化跑完一轮,每过一个通关参数组都会触发 OnTesterPass,这时不去手点报告,而是把帧数据直接封包进内存里的 zip 对象,能省掉大量人工导出的时间。上面这段代码里,FrameNext 把当前通关的 name、id、value 和二进制 data 取出来,文件名拼成「名称_编号_轮次_目标值.hps」,再 new 一个 CZipFile 丢进 Zip.AddFile,通关就进了存档。 落到硬盘只发生在 OnTesterDeinit:Zip.SaveZipToFile(OptimizationFile, FILE_COMMON) 一口气把整包写进终端公共目录,随后 printf 打出 Zip.TotalElements() 的总数。你在跑 EA 批量优化时,若看到「保存的优化通关总数」明显少于预期,优先查 FILE_COMMON 路径权限,而不是怀疑模型。 外汇与贵金属品种点差跳动大,遗传优化通关结果仅代表历史样本拟合,实盘复制存在滑点侵蚀概率,开 MT5 把这段挂到自己的优化脚本里跑一遍最直观。
class="type">void OnTesterPass() { class="type">ulong pass = class="num">0; class="type">class="kw">string name = ""; class="type">ulong id = class="num">0; class="type">class="kw">double value = class="num">0.0; class="type">uchar data[]; FrameNext(pass, name, id, value, data); class=class="str">"cmt">//-- 创建一个通关文件并将其添加到 zip 压缩文件中 printf("接收新存档大小 " + (class="type">class="kw">string)ArraySize(data)); class="type">class="kw">string file_name = name + "_" + (class="type">class="kw">string)id + "_" + (class="type">class="kw">string)pass + "_" + DoubleToString(value, class="num">5)+".hps"; CZipFile* zip_file = new CZipFile(file_name, data); Zip.AddFile(zip_file); } CZip Zip; class=class="str">"cmt">// Zip 存档由优化通关填充 class="type">void OnTesterDeinit() { Zip.SaveZipToFile(OptimizationFile, FILE_COMMON); class="type">class="kw">string f_totals = (class="type">class="kw">string)Zip.TotalElements(); printf("优化完成。保存的优化通关总数: " + f_totals); }
把优化存档变成可读的余额图
做一次 MT5 优化往往产出成百上千组通关参数,传统做法只能反复跑测试器看结果。这里思路换成:优化时把所有通关信息打包进一个 zip 存档,之后用独立的分析器程序加载它,单次优化、多次回看,不再消耗重跑时间。 分析器以智能系统形式挂载在图表上,基于 CPanel 图形引擎绘制虚拟余额曲线,并输出成交数、总利润、总亏损、盈利因子、收益预期等汇总统计。存档里的数据始终以压缩格式驻留内存,只有被点名的那一组通关才解压成对象,内存占用可控。 主入口代码极薄:建一个 CAnalyzePanel 对象,设好 800×630 的窗口与坐标,调 LoadFilePasses 读入默认 Optimization.zip,再 Show 挂到图上。图表事件只截获文本输入结束和图形点击,转成 CEvent 喂给面板。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| mAnalizer.mq5 | class=class="str">"cmt">//| 版权所有 class="num">2017, MetaQuotes 软件公司| class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "mAnalizerPanel.mqh" CAnalyzePanel Panel; class=class="str">"cmt">// 声明分析器面板对象,负责所有界面与计算 input class="type">class="kw">string FilePasses = "Optimization.zip"; class=class="str">"cmt">// 输入项:待加载的优化存档名,默认放共有目录 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 智能系统初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- Panel.Width(class="num">800); class=class="str">"cmt">// 面板宽 class="num">800 像素 Panel.Height(class="num">630); class=class="str">"cmt">// 面板高 class="num">630 像素 Panel.XCoord(class="num">10); class=class="str">"cmt">// 左上角 X 坐标 class="num">10 Panel.YCoord(class="num">20); class=class="str">"cmt">// 左上角 Y 坐标 class="num">20 Panel.LoadFilePasses(FilePasses); class=class="str">"cmt">// 从共有目录载入指定 zip 存档 Panel.Show(); class=class="str">"cmt">// 将面板显示在当前图表上 ChartRedraw(); class=class="str">"cmt">// 强制重绘图表使面板立即可见 class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); class=class="str">"cmt">// 初始化成功 } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 智能系统逆初函数 | class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| mAnalizer.mq5 | class=class="str">"cmt">//| 版权所有 class="num">2017, MetaQuotes 软件公司| class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "mAnalizerPanel.mqh" CAnalyzePanel Panel; input class="type">class="kw">string FilePasses = "Optimization.zip"; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 智能系统初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- Panel.Width(class="num">800); Panel.Height(class="num">630); Panel.XCoord(class="num">10); Panel.YCoord(class="num">20); Panel.LoadFilePasses(FilePasses); Panel.Show(); ChartRedraw(); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 智能系统逆初函数 | class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 面板退出与图表事件的分发逻辑
指标或 EA 卸载时,OnDeinit 里只做一件事:调用 Panel.Hide() 把自定义面板从图表上撤掉,避免残留对象。reason 参数虽传入但未参与判断,说明无论手动移除还是重编译,清理动作一致。 图表交互靠 OnChartEvent 接住两类事件:CHARTEVENT_OBJECT_ENDEDIT(编辑框结束输入)和 CHARTEVENT_OBJECT_CLICK(点击控件)。各自构造 CEventChartEndEdit / CEventChartObjClick 后丢给 Panel.Event() 处理,末尾 ChartRedraw() 强制重绘,保证界面状态即时刷新。 下面的 CAnalizePanel 类继承自 CElChart,私有成员用四组 CArrayObj 分别装统计说明、全部成交、多头、空头的结果,另挂一个 CTradeAnalize 做计算。图形侧有 m_name_analyze 主窗名与 m_np 的“Pass #”文本对象,结构上把数据层和展示层绑在同一面板类里。 在 MT5 里开个空指标把这段 OnChartEvent 抄进去,挂一个可编辑文本对象,改完内容松开鼠标,若面板数值跟着变,就说明事件链路通了。外汇与贵金属品种波动剧烈,这类面板仅作统计呈现,不代表任何方向确定性。
class="type">void OnDeinit(const class="type">int reason) { Panel.Hide(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ChartEvent 函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam) { class="kw">switch(id) { case CHARTEVENT_OBJECT_ENDEDIT: { CEventChartEndEdit event(sparam); Panel.Event(&event); class="kw">break; } case CHARTEVENT_OBJECT_CLICK: { CEventChartObjClick event(sparam); Panel.Event(&event); class="kw">break; } } ChartRedraw(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//class="macro">#include <Panel\ElChart.mqh> class="macro">#include <Panel\ElButton.mqh> class="macro">#include <Graphics\Graphic.mqh> class="macro">#include "ListPass.mqh" class="macro">#include "TradeAnalyze.mqh" class CAnalizePanel : class="kw">public CElChart { class="kw">private: CArrayObj m_stat_descr; class=class="str">"cmt">// 统计说明 CArrayObj m_stat_all; class=class="str">"cmt">// 所有成交的统计值 CArrayObj m_stat_long; class=class="str">"cmt">// 所有多头成交的统计值 CArrayObj m_stat_short; class=class="str">"cmt">// 所有空头成交的统计值 CTradeAnalize m_analize; class=class="str">"cmt">// 统计计算模块 CElChart m_name_analyze; class=class="str">"cmt">// 主窗口名称 CElChart m_np; class=class="str">"cmt">// "Pass #" 文本
「分析面板的成员与统计枚举拆解」
在 MT5 自定义分析面板类里,私有段先挂了一批界面与数据成员:m_of_pass 负责显示“of ### passes”文本,m_pass_index 是通关编号输入框,m_btn_next / m_btn_prev 切换前后通关,m_graphic 绘制余额动态曲线。 底层用 m_passes 存通关清单,m_curr_pass 记当前索引,m_balance_hist 是指向余额曲线的 CCurve 指针;配套私有方法覆盖事件判断(IsEndEditPass / IsClick)、通关切换(NextPass / PrevPass)、重绘(RedrawGraphic / RedrawCurrPass)与统计输出(PlotStatistic / CreateStatElements 等)。 公开段只暴露构造、LoadFilePasses(默认 FILE_COMMON 读共享目录文件)、OnShow / OnHide / Event 三个虚函数,外部调用面很窄。 统计维度由 ENUM_MSTAT_TYPE 定死,共 10 项(MSTAT_ELEMENTS_TOTAL 宏 = 10),从 MSTAT_PROFIT 到 MSTAT_MATH_EXP,覆盖盈亏、胜败单数、最大盈亏、盈利因子与数学期望。 方向过滤另给 ENUM_MSTATE_DIRECT:MSTATE_DIRECT_ALL / LONG / SHORT,分别算全部、仅买、仅卖成交——回测时切 SHORT 看贵金属空单样本,可能暴露出与全量不同的数学期望值。 头文件尾部 include 了 ArrayObj.mqh、Dictionary.mqh 和上层 MTester 的 Mposition.mqh,说明面板依赖对象数组与持仓结构,开 MT5 把这几个路径配对齐才能编译过。
CElChart m_of_pass; class=class="str">"cmt">// "of ### passes" 文本 CElChart m_pass_index; class=class="str">"cmt">// 通关编号输入框 CElButton m_btn_next; class=class="str">"cmt">// "下一个通关" 按钮 CElButton m_btn_prev; class=class="str">"cmt">// "前一个通关" 按钮 CGraphic m_graphic; class=class="str">"cmt">// 余额动态图表 class=class="str">"cmt">//-- 底层结构 CListPass m_passes; class=class="str">"cmt">// 通关清单 class="type">int m_curr_pass; class=class="str">"cmt">// 当前通关索引 CCurve* m_balance_hist; class=class="str">"cmt">// 图表上的余额动态曲线 class="type">bool IsEndEditPass(CEvent* event); class="type">bool IsClick(CEvent* event, CElChart* el); class="type">void NextPass(class="type">void); class="type">void PrevPass(class="type">void); class="type">int GetCorrectPass(class="type">class="kw">string text); class="type">void RedrawGraphic(class="type">void); class="type">void RedrawCurrPass(class="type">void); class="type">void PlotStatistic(class="type">void); class="type">class="kw">string TypeStatToString(ENUM_MSTAT_TYPE type); class="type">void CreateStatElements(class="type">void); class="type">class="kw">string ValueToString(class="type">class="kw">double value, ENUM_MSTAT_TYPE type); class="kw">public: CAnalizePanel(class="type">void); class="type">bool LoadFilePasses(class="type">class="kw">string file_name, class="type">int file_common = FILE_COMMON); class="kw">virtual class="type">void OnShow(); class="kw">virtual class="type">void OnHide(); class="kw">virtual class="type">void Event(CEvent *event); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 统计值类型的标识符 | class=class="str">"cmt">//+------------------------------------------------------------------+ enum ENUM_MSTAT_TYPE { MSTAT_PROFIT, MSTAT_ALL_WINS_MONEY, MSTAT_ALL_LOSS_MONEY, MSTAT_TRADERS_TOTAL, MSTAT_WIN_TRADERS, MSTAT_LOSS_TRADERS, MSTAT_MAX_PROFIT, MSTAT_MAX_LOSS, MSTAT_PROFIT_FACTOR, MSTAT_MATH_EXP, }; class="macro">#define MSTAT_ELEMENTS_TOTAL class="num">10 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 可以为三个方向之一计算统计 | class=class="str">"cmt">//+------------------------------------------------------------------+ enum ENUM_MSTATE_DIRECT { MSTATE_DIRECT_ALL, class=class="str">"cmt">// 对于所有成交 MSTATE_DIRECT_LONG, class=class="str">"cmt">// 仅针对买入成交 MSTATE_DIRECT_SHORT, class=class="str">"cmt">// 仅针对卖出成交 }; class="macro">#include <Arrays\ArrayObj.mqh> class="macro">#include <Dictionary.mqh> class="macro">#include "..\MTester\Mposition.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+
持仓统计的底层数据结构
要把 MT5 里一堆历史持仓拆成多空双向指标,先得有个能装「总/买/卖」三栏差值的轻量结构。下面这段 CDiffValues 就是干这个的:三个 double 字段 all、buy、sell 分别记全量、多头、空头,构造函数直接清零,没有任何外部依赖。 CTradeAnalize 类对外只露两个接口:CalculateValues 吃一个历史持仓数组指针,GetStatistic 按类型加方向吐数值。内部用 CDictionary 按枚举键存 CDiffValues 对象,键从 MSTAT_PROFIT 到 MSTAT_MATH_EXP 覆盖 11 类统计(源码循环上界 MSTAT_ELEMENTS_TOTAL 即对应此数)。 GetStatistic 的实现很直白——拿键取到 CDiffValues 指针后,按 MSTATE_DIRECT_ALL/LONG/SHORT 三分支返回 all/buy/sell,没匹配到就回 EMPTY_VALUE。你在 EA 里调统计时,若发现某方向返回 -1(EMPTY_VALUE 的宏值),先查传入的枚举是否越界,而不是怀疑字典漏存。 CalculateValues 开头先 m_values.Clear() 再按 MSTAT_ELEMENTS_TOTAL 重新 new 一遍对象,意味着每次重算都会重建全部 11 个键值对。若你的历史数组超过几千条,频繁调这个函数可能成为 tick 级回测的隐性开销,倾向把它放在新柱形成时跑一次而非每 tick 调用。
class=class="str">"cmt">//| 辅助控件包含必要的字段 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CDiffValues : class="kw">public CObject { class="kw">public: class="type">class="kw">double all; class="type">class="kw">double sell; class="type">class="kw">double buy; CDiffValues(class="type">void) : all(class="num">0), buy(class="num">0), sell(class="num">0) { } }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 统计分析类 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CTradeAnalize { class="kw">private: CDictionary m_values; class="kw">public: class="type">void CalculateValues(CArrayObj* history); class="type">class="kw">double GetStatistic(ENUM_MSTAT_TYPE type, ENUM_MSTATE_DIRECT direct); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 计算统计值 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CTradeAnalize::GetStatistic(ENUM_MSTAT_TYPE type, ENUM_MSTATE_DIRECT direct) { CDiffValues* value = m_values.GetObjectByKey(type); class="kw">switch(direct) { case MSTATE_DIRECT_ALL: class="kw">return value.all; case MSTATE_DIRECT_LONG: class="kw">return value.buy; case MSTATE_DIRECT_SHORT: class="kw">return value.sell; } class="kw">return EMPTY_VALUE; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 计算每个方向的成交数量 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTradeAnalize::CalculateValues(CArrayObj *history) { m_values.Clear(); for(class="type">int i = class="num">0; i < MSTAT_ELEMENTS_TOTAL; i++) m_values.AddObject(i, new CDiffValues()); CDiffValues* profit = m_values.GetObjectByKey(MSTAT_PROFIT); CDiffValues* wins_money = m_values.GetObjectByKey(MSTAT_ALL_WINS_MONEY); CDiffValues* loss_money = m_values.GetObjectByKey(MSTAT_ALL_LOSS_MONEY); CDiffValues* total_traders = m_values.GetObjectByKey(MSTAT_TRADERS_TOTAL); CDiffValues* win_traders = m_values.GetObjectByKey(MSTAT_WIN_TRADERS); CDiffValues* loss_traders = m_values.GetObjectByKey(MSTAT_LOSS_TRADERS); CDiffValues* max_profit = m_values.GetObjectByKey(MSTAT_MAX_PROFIT); CDiffValues* max_loss = m_values.GetObjectByKey(MSTAT_MAX_LOSS); CDiffValues* pf = m_values.GetObjectByKey(MSTAT_PROFIT_FACTOR); CDiffValues* mexp = m_values.GetObjectByKey(MSTAT_MATH_EXP); total_traders.all = history.Total(); for(class="type">int i = class="num">0; i < history.Total(); i++) { CMposition* pos = history.At(i); profit.all += pos.Profit(); if(pos.PositionType() == POSITION_TYPE_BUY) { if(pos.Profit() > class="num">0) { win_traders.buy++; wins_money.buy += pos.Profit(); }
◍ 逐仓归类后怎么算期望与盈亏比
上面这段逻辑跑完一轮持仓遍历后,把每笔仓位按多空和盈亏分别计数、累加金额,并顺手记下了最大盈利与最大亏损。买方向盈利笔数进 win_traders.buy、金额进 wins_money.buy,亏损则进 loss_traders.buy 与 loss_money.buy;卖方向同理用 .sell 那组变量承接。 所有方向汇总时,win_traders.all 与 loss_traders.all 只认盈亏正负,不区分方向;max_profit.all 和 max_loss.all 用大于、小于判断实时刷新极值,亏损极值本身是负数所以用了 max_loss.all > pos.Profit() 这种反向比较。 遍历结束后的几行才是核心指标:mexp 三组等于总利润除以总笔数,即单笔数学期望;pf 三组等于盈利总额除以亏损总额,也就是泛用版的盈利因子。你开 MT5 把这段接在自己回测脚本末尾,就能直接打印出 buy/sell/all 三栏的 mexp 与 pf,对比多空谁更拖后腿。 下面紧跟着的 CListPass 类只是把优化通关结果用 Zip 和 Dictionary 存盘,LoadOptimazeFile 负责读历史仓位,PassTotal 与 PassAt 供外部按索引取某次通关参数,和上面的统计计算是两套事,别混进同一轮循环里跑。
else { loss_traders.buy++; loss_money.buy += pos.Profit(); } total_traders.buy++; profit.buy += pos.Profit(); } else { if(pos.Profit() > class="num">0) { win_traders.sell++; wins_money.sell += pos.Profit(); } else { loss_traders.sell++; loss_money.sell += pos.Profit(); } total_traders.sell++; profit.sell += pos.Profit(); } if(pos.Profit() > class="num">0) { win_traders.all++; wins_money.all += pos.Profit(); } else { loss_traders.all++; loss_money.all += pos.Profit(); } if(pos.Profit() > class="num">0 && max_profit.all < pos.Profit()) max_profit.all = pos.Profit(); if(pos.Profit() < class="num">0 && max_loss.all > pos.Profit()) max_loss.all = pos.Profit(); } mexp.all = profit.all/total_traders.all; mexp.buy = profit.buy/total_traders.buy; mexp.sell = profit.sell/total_traders.sell; pf.all = wins_money.all/loss_money.all; pf.buy = wins_money.buy/loss_money.buy; pf.sell = wins_money.sell/loss_money.sell; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Optimazer.mq5 | class=class="str">"cmt">//| 版权所有 class="num">2017, MetaQuotes 软件公司| class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include <Zip\Zip.mqh> class="macro">#include <Dictionary.mqh> class="macro">#include "..\MTester\MTrade.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 存储优化通关清单 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CListPass { class="kw">private: CZip m_zip_passes; class=class="str">"cmt">// 所有优化通关的存档 CDictionary m_passes; class=class="str">"cmt">// 加载历史仓位 class="kw">public: class="type">bool LoadOptimazeFile(class="type">class="kw">string file_name, class="type">int file_common = FILE_COMMON); class="type">int PassTotal(class="type">void); CArrayObj* PassAt(class="type">int index); };
「把压缩包里的优化结果喂给统计面板」
复盘EA的优化产出时,通关清单常被打包成 zip 存档。CListPass::LoadOptimazeFile 负责把文件读进内存,先清空旧容器 m_zip_passes,再调用 LoadZipFromFile;若返回失败会打印“加载优化文件失败”并返回 false,成功则 true。注意 file_common 默认 FILE_COMMON,意味着文件放在终端公共目录,换设备时要确认路径一致。 PassTotal 直接返回 m_zip_passes.TotalElements(),也就是压缩包里通关方案的数量。PassAt 按索引懒加载:首次访问才解包 uchar 数组、用 CMtrade::FromCharArray 还原成交记录并缓存到 m_passes,之后直接取缓存,避免重复解压。 PlotStatistic 是面板刷新的核心。它先取当前通关 m_curr_pass-1 的 history,调 CalculateValues 算指标,再循环 MSTAT_ELEMENTS_TOTAL 次,分别把全量、多头、空头三类统计写进 m_stat_all / m_stat_long / m_stat_short 三个文本控件。你在 MT5 里改一版指标算法,只需保证 ENUM_MSTAT_TYPE 顺序不变,面板就会自动重绘。 外汇与贵金属复盘涉及历史数据回测,存在模型偏差与滑点风险,统计结果仅代表过去样本,实盘表现可能偏离。
class="type">bool CListPass::LoadOptimazeFile(class="type">class="kw">string file_name,class="type">int file_common=FILE_COMMON) { m_zip_passes.Clear(); if(!m_zip_passes.LoadZipFromFile(file_name, file_common)) { printf("加载优化文件失败。最后错误"); class="kw">return false; } class="kw">return true; } class="type">int CListPass::PassTotal(class="type">void) { class="kw">return m_zip_passes.TotalElements(); } CArrayObj* CListPass::PassAt(class="type">int index) { if(!m_passes.ContainsKey(index)) { CZipFile* zip_file = m_zip_passes.ElementAt(index); class="type">uchar array[]; zip_file.GetUnpackFile(array); CMtrade* trade = new CMtrade(); trade.FromCharArray(array); m_passes.AddObject(index, trade); } CMtrade* trade = m_passes.GetObjectByKey(index); class=class="str">"cmt">//printf("交易总计: " + (class="type">class="kw">string)trade.History.Total()); class="kw">return &trade.History; } class="type">void CAnalyzePanel::PlotStatistic(class="type">void) { if(m_stat_descr.Total() == class="num">0) CreateStatElements(); CArrayObj* history = m_passes.PassAt(m_curr_pass-class="num">1); m_analize.CalculateValues(history); for(class="type">int i = class="num">0; i < MSTAT_ELEMENTS_TOTAL; i++) { ENUM_MSTAT_TYPE stat_type = (ENUM_MSTAT_TYPE)i; class=class="str">"cmt">//-- 所有交易 CElChart* el = m_stat_all.At(i); class="type">class="kw">string v = ValueToString(m_analize.GetStatistic(stat_type, MSTATE_DIRECT_ALL), stat_type); el.Text(v); class=class="str">"cmt">//-- 多头交易 el = m_stat_long.At(i); v = ValueToString(m_analize.GetStatistic(stat_type, MSTATE_DIRECT_LONG), stat_type); el.Text(v); class=class="str">"cmt">//-- 空头交易 el = m_stat_short.At(i); v = ValueToString(m_analize.GetStatistic(stat_type, MSTATE_DIRECT_SHORT), stat_type); el.Text(v); } }
把这条线请下神坛
数学计算测试器把交易环境模拟整个舍弃,换来的是测试速度的数量级提升——实测较标准 MT5 测试器快约一个数量级,但代价是容易在报价未到达时无意「窥探未来」,造出与现实无关的「圣杯」。这类错误隐蔽性强,是高性能必须承受的风险。 单次优化可包含数百到数千次通关,每趟通关成交轻松破万笔;用 zip 存档封装后,即便大规模优化也能把体积压在适度范围,MQL5 自带压缩函数库足以支撑这种自定义存储。 分帧生成模式虽复杂,却允许写出仓位清单这类复杂自定义数据(CMPosition 类转字节入帧再还原),配合 CGraphic 绘出余额曲线。但它仍缺品种点差/掉期、参数回写、操作正确性控制等组件,下个版本才可能补上。 外汇与贵金属属高风险品类,任何自研测试器的回测盈利都不等于实盘可复制。M-Tester 已跑通主体,剩下的只是时间问题——等新版能接标准测试器报告时,你再用分析器拆自己的优化结果。