在 MetaTrader 5 中交易策略优化的可视化·综合运用
(3/3)·单一优化图看不出策略强弱?本篇把图形界面、帧数据处理与结果同屏一次性串起来
在优化回传里实时画出余额曲线
做 MT5 批量优化时,默认报告只给一张最终散点图,过程中每个 pass 的余额序列其实是被丢弃的。下面这段类方法把每一帧的余额数组抓出来,直接塞进图表对象的曲线缓冲里,让你在优化跑完前就能肉眼看哪组参数更平滑。 核心入口是 OnTesterPassEvent,它靠 FrameNext 拉取当前帧,再用 FrameInputs 把该 pass 的 EA 输入参数存进 m_param_data。拿到数据后调 UpdateBalanceGraph 刷新图形,m_frames_counter 自增标记已处理帧数。 UpdateBalanceGraph 里有个细节容易踩坑:曲线索引 m_last_serie_index 是循环复用的。一旦 m_last_serie_index 达到 m_curves_total 就归零,所以图表上永远只保留最近 N 条序列,早前的会被覆盖。 水平轴范围按所有序列里最长那条定,x_axis.Max(x_max) 中 x_max 来自 m_curve_max 数组的 fmax 比较,DefaultStep 取 x_max/8.0 向下整。外汇与贵金属优化涉及高杠杆与滑点,回测曲线好看不代表实盘概率占优,请用小资金验证。
class=class="str">"cmt">//| 设置要在图表上显示的序列数量 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CFrameGenerator::SetCurvesTotal(class="kw">const class="type">uint total) { m_curves_total=total; ::ArrayResize(m_curve_max,total); ::ArrayInitialize(m_curve_max,class="num">0); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 更新余额图 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CFrameGenerator::UpdateBalanceGraph(class="type">void) { class=class="str">"cmt">//--- 接收当前帧的余额值的数组 class="type">class="kw">double serie[]; ::ArrayCopy(serie,m_data,class="num">0,STAT_TOTAL,::ArraySize(m_data)-STAT_TOTAL); class=class="str">"cmt">//--- 发送数组以便在余额图上显示 CCurve *curve=m_graph_balance.CurveGetByIndex(m_last_serie_index); curve.Name((class="type">class="kw">string)m_frames_counter); curve.Color((m_data[class="num">0]>=class="num">0)? ::ColorToARGB(clrLimeGreen) : ::ColorToARGB(clrRed)); curve.Update(serie); class=class="str">"cmt">//--- 获取序列大小 class="type">int serie_size=::ArraySize(serie); m_curve_max[m_last_serie_index]=serie_size; class=class="str">"cmt">//--- 定义具有最大元素数两的序列 class="type">class="kw">double x_max=class="num">0; for(class="type">uint i=class="num">0; i<m_curves_total; i++) x_max=::fmax(x_max,m_curve_max[i]); class=class="str">"cmt">//--- 水平轴属性 CAxis *x_axis=m_graph_balance.XAxis(); x_axis.Min(class="num">0); x_axis.Max(x_max); x_axis.DefaultStep((class="type">int)(x_max/class="num">8.0)); class=class="str">"cmt">//--- 更新结果图 m_graph_balance.CalculateMaxMinValues(); m_graph_balance.CurvePlotAll(); m_graph_balance.Update(); class=class="str">"cmt">//--- 增加序列计数器 m_last_serie_index++; class=class="str">"cmt">//--- 如果达到极限,请将序列计数器设置为零 if(m_last_serie_index>=m_curves_total) m_last_serie_index=class="num">0; } class CFrameGenerator { class="kw">private: class=class="str">"cmt">//--- EA 参数 class="type">class="kw">string m_param_data[]; class="type">uint m_par_count; }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 在优化过程中接收数据帧并显示图形 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CFrameGenerator::OnTesterPassEvent(class="type">void) { class=class="str">"cmt">//--- 获取新帧后,尝试从中提取数据 if(::FrameNext(m_pass,m_name,m_id,m_value,m_data)) { class=class="str">"cmt">//--- 获取所形成帧的 EA 输入参数 ::FrameInputs(m_pass,m_param_data,m_par_count); class=class="str">"cmt">//--- 将结果统计参数保存到数组中 SaveStatData(); class=class="str">"cmt">//--- 更新结果和余额图 UpdateResultsGraph(); UpdateBalanceGraph(); class=class="str">"cmt">//--- 增加处理帧的计数器 m_frames_counter++; class="kw">return(true); } class=class="str">"cmt">//---
「帧数据回收与终算的落地写法」
在 MT5 优化器跑完一整轮参数后,CFrameGenerator 必须先把手里四块动态数组清空,否则上一轮残留的盈亏坐标会混进新图。ArraysFree 直接调全局 ArrayFree 把 m_loss_y、m_loss_x、m_profit_y、m_profit_x 全部释放,这一步放在 FinalRecalculateFrames 开头,保证重算前画布是干净的。 FinalRecalculateFrames 的逻辑很直白:FrameFirst 把帧指针拨回开头,ArraysFree 清场,m_frames_counter 归零,然后 while(FrameNext(...)) 逐帧读 m_data[0],负数进 AddLoss、非负数进 AddProfit,同时帧计数器自增。循环结束时就拿到了本轮全部样本的分布。 图形更新那段给了可验证的锚点:X 轴 Max 设为 m_frames_counter,DefaultStep 用 (int)(m_frames_counter/8.0) 算,也就是把横轴大致切成 8 格。你在 MT5 策略测试器里跑完优化,打开结果图应该看到横轴刻度约 8 段,曲线名带 "P: " 和 "L: " 前缀显示总盈总亏。 OnTesterDeinitEvent 是挂接点,必须在 OnTesterDeinit() 里调用,它跑完 FinalRecalculateFrames 后把 m_frames_counter 存进 m_frames_total 并清零,顺手把 m_last_serie_index 置 0。外汇与贵金属品种测试受点差滑点影响大,这类重算结果仅反映历史样本分布,实盘存在较高风险,参数过拟合概率不低。
class="kw">return(class="kw">false); } class CFrameGenerator { class="kw">private: class=class="str">"cmt">//--- 释放数组 class="type">void ArraysFree(class="type">void); class=class="str">"cmt">//--- 优化后所有帧的最终数据重新计算 class="type">void FinalRecalculateFrames(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 释放数组 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CFrameGenerator::ArraysFree(class="type">void) { ::ArrayFree(m_loss_y); ::ArrayFree(m_loss_x); ::ArrayFree(m_profit_y); ::ArrayFree(m_profit_x); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 优化后所有帧的最终数据重新计算 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CFrameGenerator::FinalRecalculateFrames(class="type">void) { class=class="str">"cmt">//--- 将帧指针设置到开始 ::FrameFirst(); class=class="str">"cmt">//--- 重置计数器和数组 ArraysFree(); m_frames_counter=class="num">0; class=class="str">"cmt">//--- 启动循环遍历帧 class="kw">while(::FrameNext(m_pass,m_name,m_id,m_value,m_data)) { class=class="str">"cmt">//--- 负数结果 if(m_data[class="num">0]<class="num">0) AddLoss(m_data[class="num">0]); class=class="str">"cmt">//--- 正数结果 else AddProfit(m_data[class="num">0]); class=class="str">"cmt">//--- 增加处理帧的计数器 m_frames_counter++; } class=class="str">"cmt">//--- 在图形上更新序列 CCurve *curve=m_graph_results.CurveGetByIndex(class="num">0); curve.Name("P: "+(class="type">class="kw">string)ProfitsTotal()); curve.Update(m_profit_x,m_profit_y); class=class="str">"cmt">//--- curve=m_graph_results.CurveGetByIndex(class="num">1); curve.Name("L: "+(class="type">class="kw">string)LossesTotal()); curve.Update(m_loss_x,m_loss_y); class=class="str">"cmt">//--- 水平轴属性 CAxis *x_axis=m_graph_results.XAxis(); x_axis.Min(class="num">0); x_axis.Max(m_frames_counter); x_axis.DefaultStep((class="type">int)(m_frames_counter/class="num">8.0)); class=class="str">"cmt">//--- 更新结果图 m_graph_results.CalculateMaxMinValues(); m_graph_results.CurvePlotAll(); m_graph_results.Update(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 应在 OnTesterDeinit() 处理器中调用 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CFrameGenerator::OnTesterDeinitEvent(class="type">void) { class=class="str">"cmt">//--- 优化后最终重新计算所有帧的数据 FinalRecalculateFrames(); class=class="str">"cmt">//--- 记住总帧数并将计数器设置为零 m_frames_total =m_frames_counter; m_frames_counter =class="num">0; m_last_serie_index =class="num">0; }
◍ 把优化数据锁进界面与帧生成器
图形界面在测试初始化阶段由 CProgram::OnTesterInitEvent() 建好,随后必须让界面处于不可操作状态,否则优化过程中的用户输入会污染结果。具体靠 IsAvailableGUI() 与 IsLockedGUI() 两个私有方法控制窗口、休眠毫秒框、曲线数量框和重演按钮的可用与锁定。 帧生成器初始化时接收两个图形指针(m_graph1、m_graph2),用来把每帧优化结果画出来。统计表和参数表共用一套更新逻辑,以 '=' 分隔参数名与值,循环拆成两元素数组后写进单元格,方法分别是 UpdateStatTable() 和 UpdateParamTable()。 OnTesterPassEvent() 里收到帧生成器返回正数时才刷表;优化结束的 CalculateProfitsAndLosses() 会算正负结果占比并推到状态栏。注意当前 MT5 版本在优化完成时无法开启定时器,所以光标跟踪和计时器初始化虽写了,暂停滚动仍只能靠 Sleep 输入字段手动调。 用户改输入字段或点重演帧按钮,会触发 UpdateBalanceGraph() 重设序列数并刷新图;CFrameGenerator::ReplayFrames() 从零指针重放帧,返回 true 表示有帧、false 表示放完。外汇与贵金属优化空间大、过拟合概率高,建议先在历史数据跑小样本验证界面逻辑再上实盘参数。
class CProgram : class="kw">public CWndEvents { class="kw">private: class=class="str">"cmt">//--- 界面可用性 class="type">void IsAvailableGUI(class="kw">const class="type">bool state); class="type">void IsLockedGUI(class="kw">const class="type">bool state); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 优化开始事件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CProgram::OnTesterInitEvent(class="type">void) { class=class="str">"cmt">//--- 创建图形界面 if(!CreateFrameModeGUI()) { ::Print(__FUNCTION__," > 不能创建图形界面!"); class="kw">return; } class=class="str">"cmt">//--- 令界面无法访问 IsLockedGUI(class="kw">false); class=class="str">"cmt">//--- 初始化帧生成器 m_frame_gen.OnTesterInitEvent(m_graph1.GetGraphicPointer(),m_graph2.GetGraphicPointer()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 界面可用性 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CProgram::IsAvailableGUI(class="kw">const class="type">bool state) { m_window1.IsAvailable(state); m_sleep_ms.IsAvailable(state); m_curves_total.IsAvailable(state); m_reply_frames.IsAvailable(state); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 阻塞界面 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CProgram::IsLockedGUI(class="kw">const class="type">bool state) { m_window1.IsAvailable(state); m_sleep_ms.IsLocked(!state); m_curves_total.IsLocked(!state); m_reply_frames.IsLocked(!state); } class CProgram : class="kw">public CWndEvents { class="kw">private: class=class="str">"cmt">//--- 更新统计表 class="type">void UpdateStatTable(class="type">void); class=class="str">"cmt">//--- 更新参数表 class="type">void UpdateParamTable(class="type">void); } class=class="str">"cmt">//+------------------------------------------------------------------+
把优化帧塞进统计表与盈亏比率
回测通关事件 OnTesterPassEvent 里只做一件事:让帧生成器先消化测试结果,成功就调 UpdateStatTable 和 UpdateParamTable。这两行是 GUI 实时刷新的触发点,漏掉任一个,面板数据就会停在上一帧。 UpdateStatTable 先通过 CopyStatData 拿到字符串数组,总长度由返回值 total 决定。每条记录用 '=' 切分,StringSplit 返回 2 才认为合法;CurrentFrame 大于 1 时只写第二列(数值),否则连标题带数值一起写第 0、1 列。 盈亏比率在 CalculateProfitsAndLosses 里算:losses 和 profits 分别取自帧生成器的 LossesTotal、ProfitsTotal,各自除以 FramesTotal 再乘 100,保留两位小数。状态栏会显示类似「Profits: 63 (31.50%) / Losses: 137 (68.50%)」——外汇与贵金属回测中这类胜率分布偏向亏损侧是常见现象,仅反映样本内概率,实盘高风险依旧。 若帧总数为 0 直接 return,避免除零。开 MT5 把这段挂到自己的 CWndEvents 派生类里,跑一次优化就能在状态栏看到真实盈亏占比。
class="type">void CProgram::UpdateStatTable(class="type">void) { class=class="str">"cmt">//--- 获取统计表的数据数组 class="type">class="kw">string stat_data[]; class="type">int total=m_frame_gen.CopyStatData(stat_data); for(class="type">int i=class="num">0; i<total; i++) { class=class="str">"cmt">//--- 分成两行并输入到表格 class="type">class="kw">string array[]; if(::StringSplit(stat_data[i],&class="macro">#x27;=&class="macro">#x27;,array)==class="num">2) { if(m_frame_gen.CurrentFrame()>class="num">1) m_table_stat.SetValue(class="num">1,i,array[class="num">1],class="num">0,true); else { m_table_stat.SetValue(class="num">0,i,array[class="num">0],class="num">0,true); m_table_stat.SetValue(class="num">1,i,array[class="num">1],class="num">0,true); } } } class=class="str">"cmt">//--- 更新表格 m_table_stat.Update(); } class="type">void CProgram::OnTesterPassEvent(class="type">void) { class=class="str">"cmt">//--- 处理获得的测试结果并显示图形 if(m_frame_gen.OnTesterPassEvent()) { UpdateStatTable(); UpdateParamTable(); } } class CProgram : class="kw">public CWndEvents { class="kw">private: class=class="str">"cmt">//--- 计算正数和负数结果的比率 class="type">void CalculateProfitsAndLosses(class="type">void); } class="type">void CProgram::CalculateProfitsAndLosses(class="type">void) { class=class="str">"cmt">//--- 如果没有帧,退出 if(m_frame_gen.FramesTotal()<class="num">1) class="kw">return; class=class="str">"cmt">//--- 负数和正数结果的数量 class="type">int losses =m_frame_gen.LossesTotal(); class="type">int profits =m_frame_gen.ProfitsTotal(); class=class="str">"cmt">//--- 百分比 class="type">class="kw">string pl =::DoubleToString(((class="type">class="kw">double)losses/(class="type">class="kw">double)m_frame_gen.FramesTotal())*class="num">100,class="num">2); class="type">class="kw">string pp =::DoubleToString(((class="type">class="kw">double)profits/(class="type">class="kw">double)m_frame_gen.FramesTotal())*class="num">100,class="num">2);; class=class="str">"cmt">//--- 显示在状态栏中 m_status_bar.SetValue(class="num">1,"Profits: "+(class="type">class="kw">string)profits+" ("+pp+"%)"+" / Losses: "+(class="type">class="kw">string)losses+" ("+pl+"%)");
「优化收尾时把净值曲线重绘出来」
策略优化跑完,MT5 会触发 OnTesterDeinitEvent。这里先让框架做收尾(m_frame_gen.OnTesterDeinitEvent),再把 GUI 锁打开(IsLockedGUI(true)),随后算盈利与亏损次数的比值(CalculateProfitsAndLosses),最后用 CWndEvents::InitializeCore() 把界面核心重新拉起,否则优化后面板可能点不动。 净值图不是优化完就自动刷新的。UpdateBalanceGraph 先读 m_curves_total 的数值决定画几条曲线,再把旧曲线按索引从后往前全删掉(graph.CurveRemoveByIndex),接着用空 data 数组补回对应数量的 CURVE_LINES,最后 CurvePlotAll + Update 才出图。 界面交互上,自定义按钮点击(CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)和输入框结束编辑(CHARTEVENT_CUSTOM+ON_END_EDIT)都会判断 lparam 是否等于 m_curves_total.Id(),是就重绘曲线。也就是说你在面板上改“曲线数量”那个框,松手即触发重画,不用重启 EA。 外汇与贵金属品种做这类优化回测波动剧烈、滑点随机,重画出的曲线仅反映历史参数表现,实盘复制可能显著偏离。
m_status_bar.GetItemPointer(class="num">1).Update(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 优化完成事件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CProgram::OnTesterDeinitEvent(class="type">void) { class=class="str">"cmt">//--- 优化完成 m_frame_gen.OnTesterDeinitEvent(); class=class="str">"cmt">//--- 令界面可访问 IsLockedGUI(true); class=class="str">"cmt">//--- 计算正数和负数结果的比率 CalculateProfitsAndLosses(); class=class="str">"cmt">//--- 初始化图形界面核心 CWndEvents::InitializeCore(); } class CProgram : class="kw">public CWndEvents { class="kw">private: class=class="str">"cmt">//--- 更新结果图 class="type">void UpdateBalanceGraph(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 更新图形 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CProgram::UpdateBalanceGraph(class="type">void) { class=class="str">"cmt">//--- 设置工作序列的数量 class="type">int curves_total=(class="type">int)m_curves_total.GetValue(); m_frame_gen.SetCurvesTotal(curves_total); class=class="str">"cmt">//--- 删除序列 CGraphic *graph=m_graph1.GetGraphicPointer(); class="type">int total=graph.CurvesTotal(); for(class="type">int i=total-class="num">1; i>=class="num">0; i--) graph.CurveRemoveByIndex(i); class=class="str">"cmt">//--- 添加序列 class="type">class="kw">double data[]; for(class="type">int i=class="num">0; i<curves_total; i++) graph.CurveAdd(data,CURVE_LINES,""); class=class="str">"cmt">//--- 更新结果图 graph.CurvePlotAll(); graph.Update(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 事件处理器 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CProgram::OnEvent(class="kw">const class="type">int id,class="kw">const class="type">long &lparam,class="kw">const class="type">class="kw">double &dparam,class="kw">const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- 按钮触发事件 if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON) { class=class="str">"cmt">//--- 更改图表上的序列数量 if(lparam==m_curves_total.Id()) { UpdateBalanceGraph(); class="kw">return; } class="kw">return; } class=class="str">"cmt">//--- 在输入字段中输入数值的事件 if(id==CHARTEVENT_CUSTOM+ON_END_EDIT) { class=class="str">"cmt">//--- 更改图表上的序列数量 if(lparam==m_curves_total.Id()) { UpdateBalanceGraph(); class="kw">return; } class="kw">return; } } class CFrameGenerator { class="kw">public: class=class="str">"cmt">//--- 循环遍历帧
◍ 优化后逐帧重演怎么落地
MT5 策略测试器跑完遗传优化,结果不是一次性吐出,而是按帧(frame)流式回放。CFrameGenerator::ReplayFrames() 就是这套重演的引擎:每次调用推进一帧,返回 true 表示还有后续,返回 false 表示全部帧走完。 帧计数器小于 1 时先释放旧数组并调用 ::FrameFirst() 回到开头;随后 ::FrameNext() 取出当前帧的 pass、name、id、value、data,再用 ::FrameInputs() 拿该帧对应的 EA 输入参数,SaveStatData() 落统计,UpdateResultsGraph() 和 UpdateBalanceGraph() 刷两张图。m_frames_counter 每帧加 1。 外层 ViewOptimizationResults() 用 while(m_frame_gen.ReplayFrames() && !::IsStopped()) 把帧一帧帧播完。暂停时长由 m_sleep_ms 控件值决定,转成 int 后传给 ::Sleep(pause),默认节奏由你调滑块控制。进度条实时显示 Replay frames: 当前/总数,例如跑 5000 帧优化时就是 1/5000 到 5000/5000。 重演结束清零 m_frames_counter 与 m_last_serie_index,调用 CalculateProfitsAndLosses() 算盈亏帧比,最后把 GUI 解禁。外汇与贵金属品种优化重演受点差滑点影响大,同一帧在不同经纪商环境可能倾向不同结果,建议本地多券商数据各跑一遍验证。
class="type">bool CFrameGenerator::ReplayFrames(class="type">void) { class=class="str">"cmt">//--- 将帧指针设置为开头 if(m_frames_counter<class="num">1) { ArraysFree(); ::FrameFirst(); } class=class="str">"cmt">//--- 启动循环遍历帧 if(::FrameNext(m_pass,m_name,m_id,m_value,m_data)) { class=class="str">"cmt">//--- 获取 EA 输入,哪个帧已经形成 ::FrameInputs(m_pass,m_param_data,m_par_count); class=class="str">"cmt">//--- 将统计结果参数保存到数组中 SaveStatData(); class=class="str">"cmt">//--- 更新结果和余额图 UpdateResultsGraph(); UpdateBalanceGraph(); class=class="str">"cmt">//--- 增加处理帧的计数器 m_frames_counter++; class="kw">return(true); } class=class="str">"cmt">//--- 循环完成 m_frames_counter =class="num">0; m_last_serie_index =class="num">0; class="kw">return(class="kw">false); } class="type">void CProgram::ViewOptimizationResults(class="type">void) { class=class="str">"cmt">//--- 令界面不可访问 IsAvailableGUI(class="kw">false); class=class="str">"cmt">//--- 暂停 class="type">int pause=(class="type">int)m_sleep_ms.GetValue(); class=class="str">"cmt">//--- 播放帧 class="kw">while(m_frame_gen.ReplayFrames() && !::IsStopped()) { class=class="str">"cmt">//--- 更新表格 UpdateStatTable(); UpdateParamTable(); class=class="str">"cmt">//--- 更新进度条 m_progress_bar.Show(); m_progress_bar.LabelText("Replay frames: "+class="type">class="kw">string(m_frame_gen.CurrentFrame())+"/"+class="type">class="kw">string(m_frame_gen.FramesTotal())); m_progress_bar.Update((class="type">int)m_frame_gen.CurrentFrame(),(class="type">int)m_frame_gen.FramesTotal()); class=class="str">"cmt">//--- 暂停 ::Sleep(pause); } class=class="str">"cmt">//--- 计算正数和负数结果的比率 CalculateProfitsAndLosses(); class=class="str">"cmt">//--- 隐藏进度条 m_progress_bar.Hide(); class=class="str">"cmt">//--- 令界面可用 IsAvailableGUI(true); m_reply_frames.MouseFocus(class="kw">false); m_reply_frames.Update(true); }
在自定义事件里捞回优化结果
EA 跑完框架优化后,结果不会自己跳出来,得靠图表自定义事件把数据端喂回来。上面这段把按钮点击映射成 CHARTEVENT_CUSTOM+ON_CLICK_BUTTON,一旦 lparam 对上 m_reply_frames.Id(),就调 ViewOptimizationResults() 把缓存的优化帧刷到面板上。 这种写法把「算完」和「看结果」拆成两个时刻,实盘里你能边盯价边等回测帧,不必阻塞主线程。MT5 的事件 id 从 CHARTEVENT_CUSTOM(1000) 起算,自定义偏移量撞车会静默丢失事件,调试时建议先 Print(id) 确认区间。 外汇与贵金属杠杆高、滑点跳变频繁,回测帧里的参数在活盘可能只具备概率优势,别直接当下单依据。
class="type">void CProgram::OnEvent(class="kw">const class="type">int id,class="kw">const class="type">long &lparam,class="kw">const class="type">class="kw">double &dparam,class="kw">const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- 按下按钮的事件 if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON) { class=class="str">"cmt">//--- 查看优化结果 if(lparam==m_reply_frames.Id()) { ViewOptimizationResults(); class="kw">return; } class=class="str">"cmt">//--- ... class="kw">return; } }
「原版EA跑出来到底什么样」
用 MT5 标准库里自带的 Moving Average EA 做基线验证,代码按原样加载,不调参不改装。策略类写在 Program.mqh,FormatString.mqh 仅作行格式化辅助函数,不属于任何类,在依赖图里以黑箭头标记。 测试器直接挂 EURUSD H1,区间 2017.01.01–2018.01.01。跑完优化可见结果信息量够用,但 95.23% 的优化组合是负收益;把样本区间拉长,亏损比例还会继续恶化。做策略开发时,多数参数组合应为正才是可用前提,否则这个算法大概率只会持续失血,不该上实盘。 换 MACD Sample.mq5 再测,它本来就是类封装,小改后直接挂进同一套应用框架。品种周期不变,时间范围扩到 2010.01.01–2018.01.01,优化结果反转:正面组合占到 90.89%。 外汇/贵金属品种跳空与滑点风险高,回测顺滑不等于实盘能复现。参数优化在大数据量下可能耗时很久,不必盯屏;跑完点「重演帧」用加速模式回看,序列上限设 25 条就能快速扫完结果。
◍ 画得少,看得清
基于 EasyAndFast 函数库搭的图形界面,把优化帧的接收和分析做成了可视化的现成环境,省掉了自己写面板的麻烦。 目前这套方案有两个绕不开的坑:帧处理模式跑完优化后计时器起不来,限制了同界面的后续用法;从图表撤掉 EA 时 OnDeinit() 里的逆初始化不触发,事件链容易乱。这两点大概率要等 MT5 后续版本才补。 外汇和贵金属品种波动大、杠杆高,用这类工具做优化验证时先在小周期样本里跑通事件逻辑,比直接上实盘稳妥。