MQL5 中的策略可视化:在标准图表中展示优化结果·综合运用
(3/3)·从类封装到 EA 接入,五标签页把夏普、净利、盈利因子一次看透
- 表格控件里逐行塞单元格与改色那点事
- 把 RGB 拼回 color 并往画布上写字
- 把优化结果画成可视图层
- 点表头自动回放优化帧的实现片段
- 进度条类的画布指针与成员布局
- 进度条对象的边界与配色封装
- 进度条与统计图的底层绘制逻辑
- 统计图表类的成员与画布绑定
- 画布上图表区与进度条的边界绑定
- 控件边界与按钮指针的取法
- 回放按钮的生成与结果按钮的层级控制
- 回放按钮着色与结果按钮的惰性创建
- 结果按钮组的初始化与配色
- 净值曲线绘制类的接口与事件分发
- 统计图表类的刷新与序列追加逻辑
- 序列极值与坐标翻转的绘制细节
- 把序列值映射成画布像素的实战拆法
- 把每轮优化结果装进一个类
- 把回测帧指标直接打到日志里
- 按两位小数给回测指标排座次
- 回测帧数据的比较与查看器骨架
- 多标签优化面板里的对象与索引映射
- 按标签索引取回优化结果表与图
- 框架对象入列与绘制接口的防呆处理
- 把优化结果画到图表上的几个关键接口
- 批量改线色与回测事件的接口设计
- 回测前把图表先藏起来再挂标签控件
- 把回测指标铺进五个分页的画法
- 优化面板多标签的绘制与按钮兜底
- 回测收尾时把标签刷成可重放状态
- 在 OnTester 里抓取资金曲线与四项指标
- 回测帧里捞夏普与回撤
- 图表事件里的自适应重绘逻辑
- 优化回放与分页绘制的分支逻辑
- 回放锁与多标签切换的事件处理
- 标签页与结果按钮的事件分发逻辑
- 切换标签时重绘优化结果视图
- 切换统计标签时的按钮重排逻辑
- 回放开局先把按钮层级理清
- 把优化前三名画到图表上
- 按优化指标挑出前三轮回测
- 回测结果里捞最优帧的底层取法
- 把输入参数刷进优化结果表
- 回测面板里逐行填回测指标
- 把优化结果画进自定义标签页
- 优化结果面板的表头与网格绘制
- 参数表与优化图的画布落点
- 优化结果图表的边界与表头绘制逻辑
- 把帧查看器类挂进现成 EA
- 在 OnInit 里把信号与跟踪挂到专家对象
- EA初始化失败的逐级拦截
- 把优化结果画进图表的那几行钩子
- 把优化结果变成可回放画面
- 把这条线请下神坛
「表格控件里逐行塞单元格与改色那点事」
在 MT5 自定义表格控件里,给每一行批量挂载单元格的典型做法是先拿 RowsTotal() 取总行数,再跑一个从 0 到 total-1 的 for 循环。循环内用 GetRow(j) 取行对象,空指针就 continue 跳过,避免崩在半路。 新建单元格用 new CTableCell(row.Row(),i),i 一般是列索引。若 new 出来是 NULL,用 PrintFormat 打一行带 __FUNCTION__ 和索引的报错然后 continue;若 row.AddCell(cell) 失败则 delete cell 回收,这两道防御能让控件在内存紧张时仍可能稳住不闪退。 cell.SetXY(col_x,row.Y()) 和 cell.SetSize(col_w,row_h) 把坐标和尺寸锚到列宽与行高上,循环结束调 canvas.Update(false) 只刷画布不重绘整图,CPU 占用倾向更低。 配色上 NewColor() 接收 base_color 加三个通道偏移,内部用 ColorToRGB 拆成 0~255 的浮点,每个分量做越界钳制(小于 0 取 0、大于 255 取 255),再 RGBToColor 合回去。这意味着你传 shift_red=300 也不会溢出,出来的仍是合法色值。 外汇与贵金属图表叠加这类自绘控件时波动刷新的风险偏高,建议先在回测器外的 demo 账户验证内存与重绘表现。
class="type">int total=this.RowsTotal(); for(class="type">int j=class="num">0;j<total;j++) { CTableRow *row=this.GetRow(j); if(row==NULL) class="kw">continue; CTableCell *cell=new CTableCell(row.Row(),i); if(cell==NULL) { ::PrintFormat("%s: Failed to create table cell object at index %lu",(class="type">class="kw">string)__FUNCTION__,i); class="kw">continue; } if(!row.AddCell(cell)) { class="kw">delete cell; class="kw">continue; } cell.SetXY(col_x,row.Y()); cell.SetSize(col_w, row_h); } canvas.Update(false); class="type">color CTableDataControl::NewColor(class="type">color base_color, class="type">int shift_red, class="type">int shift_green, class="type">int shift_blue) { class="type">class="kw">double clR=class="num">0, clG=class="num">0, clB=class="num">0; this.ColorToRGB(base_color,clR,clG,clB); class="type">class="kw">double clRn=(clR+shift_red < class="num">0 ? class="num">0 : clR+shift_red > class="num">255 ? class="num">255 : clR+shift_red); class="type">class="kw">double clGn=(clG+shift_green< class="num">0 ? class="num">0 : clG+shift_green> class="num">255 ? class="num">255 : clG+shift_green); class="type">class="kw">double clBn=(clB+shift_blue < class="num">0 ? class="num">0 : clB+shift_blue > class="num">255 ? class="num">255 : clB+shift_blue); class="kw">return this.RGBToColor(clRn,clGn,clBn); } class="type">color CTableDataControl::RGBToColor(const class="type">class="kw">double r,const class="type">class="kw">double g,const class="type">class="kw">double b) const { class="type">int int_r=(class="type">int)::round(r); class="type">int int_g=(class="type">int)::round(g);
把 RGB 拼回 color 并往画布上写字
在自定义表格控件里,颜色经常要以 R/G/B 三个分量分别存、再合成 MT5 的 color 类型。下面这段先把蓝色分量左移 8 位、或上绿色,再左移 8 位、或上红色,最终强转回 color 返回,是一个典型的 0xBBGGRR 打包过程。 int int_b=(int)::round(b); int clr=0; clr=int_b; clr<<=8; clr|=int_g; clr<<=8; clr|=int_r; return (color)clr; ColorToRGB 则是反向操作,把传入的 color 拆回 r/g/b 三个 double 引用,内部直接调 GetR/GetG/GetB,方便外部按需改分量。 DrawText 负责把一段文字绘到 CCanvas 指定坐标。若传入 width 和 height 都是 0,整块画布会被 0x00FFFFFF(全透明)擦掉;否则按文字实际尺寸或调用方给的宽高,先用透明色 FillRectangle 清掉旧内容,再 TextOut 写新字,最后 Update(false) 只刷缓冲区不重绘屏幕。 在 MT5 里开个脚本挂 CCanvas 验证:传 width=0,height=0 看是否整屏变透明,再传具体坐标和 WRONG_VALUE 看文字尺寸自适应,能直接确认这套擦除逻辑有没有坑。外汇与贵金属图表叠加自绘控件波动剧烈,误清屏可能遮住价格,实盘前务必在模拟环境跑通。
class="type">int int_b=(class="type">int)::round(b); class="type">int clr=class="num">0; clr=int_b; clr<<=class="num">8; clr|=int_g; clr<<=class="num">8; clr|=int_r; class=class="str">"cmt">//--- class="kw">return (class="type">color)clr; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Retrieving RGB component values | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTableDataControl::ColorToRGB(const class="type">color clr,class="type">class="kw">double &r,class="type">class="kw">double &g,class="type">class="kw">double &b) { r=GetR(clr); g=GetG(clr); b=GetB(clr); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Prints text message to specified coordinates | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTableDataControl::DrawText(CCanvas *canvas,const class="type">class="kw">string text,const class="type">int x,const class="type">int y,const class="type">color clr=clrNONE,const class="type">uint align=class="num">0,const class="type">int width=WRONG_VALUE,const class="type">int height=WRONG_VALUE) { class=class="str">"cmt">//--- Declare variables to record width and height of text in them class="type">int w=width; class="type">int h=height; class=class="str">"cmt">//--- If width and height of text passed to method have zero values, class=class="str">"cmt">//--- entire canvas space is completely erased with transparent class="type">color if(width==class="num">0 && height==class="num">0) canvas.Erase(0x00FFFFFF); class=class="str">"cmt">//--- Otherwise else { class=class="str">"cmt">//--- If passed width and height have class="kw">default values(-class="num">1), get its width and height from text if(width==WRONG_VALUE && height==WRONG_VALUE) canvas.TextSize(text,w,h); class=class="str">"cmt">//--- otherwise, else { class=class="str">"cmt">//--- if width passed to method has class="kw">default value(-class="num">1), get width from text, or class=class="str">"cmt">//--- if width passed to method has value greater than zero, use width passed to method, or class=class="str">"cmt">//--- if width passed to method has zero value, use value class="num">1 for width w=(width ==WRONG_VALUE ? canvas.TextWidth(text) : width>class="num">0 ? width : class="num">1); class=class="str">"cmt">//--- if height passed to method has class="kw">default value(-class="num">1), get height from text, or class=class="str">"cmt">//--- if height passed to method has value greater than zero, use height passed to method, or class=class="str">"cmt">//--- if height passed to method has zero value, use value class="num">1 for height h=(height==WRONG_VALUE ? canvas.TextHeight(text) : height>class="num">0 ? height : class="num">1); } class=class="str">"cmt">//--- Fill space according to specified coordinates and by resulting width and height with transparent class="type">color (erase previous entry) canvas.FillRectangle(x,y,x+w,y+h,0x00FFFFFF); } class=class="str">"cmt">//--- Print text in place cleared of previous text and update workspace without redrawing screen canvas.TextOut(x,y,text,::ColorToARGB(clr==clrNONE ? this.m_fore_color : clr),align); canvas.Update(false); }
◍ 把优化结果画成可视图层
在 MT5 策略测试器里跑多参数寻优时,默认只给一张枯燥的表格。通过 CCanvas 封装一个填充矩形函数,可以把关键区域直接刷到图表画布上,坐标与透明度都由调用方控制。 下面这段 DrawRectangleFill 是底层绘制入口:传入画布指针、左上角 x/y、宽高、颜色与 alpha 通道,内部用 ColorToARGB 合成后 FillRectangle 再 Update,刷屏延迟取决于画布分辨率,一般 1920×1080 下肉眼无感。
class="type">void CTableDataControl::DrawRectangleFill(CCanvas *canvas,const class="type">int x,const class="type">int y,const class="type">int width,const class="type">int height,const class="type">color clr,const class="type">uchar alpha) { canvas.FillRectangle(x,y,x+width,y+height,::ColorToARGB(clr,alpha)); canvas.Update(); }
class="macro">#include <FrameGenerator.mqh> CFrameGenerator fg; class="type">class="kw">double OnTester() { class="type">class="kw">double TesterCritetia=MathAbs(TesterStatistics(STAT_SHARPE_RATIO)*TesterStatistics(STAT_PROFIT)); TesterCritetia=TesterStatistics(STAT_PROFIT)>class="num">0?TesterCritetia:(-TesterCritetia); fg.OnTester(TesterCritetia); class="kw">return(TesterCritetia); } class="type">void OnTesterInit() { fg.OnTesterInit(class="num">3); } class="type">void OnTesterPass() { fg.OnTesterPass(); } class="type">void OnTesterDeinit() { fg.OnTesterDeinit(); }
class="type">void CTableDataControl::DrawRectangleFill(CCanvas *canvas,const class="type">int x,const class="type">int y,const class="type">int width,const class="type">int height,const class="type">color clr,const class="type">uchar alpha) { canvas.FillRectangle(x,y,x+width,y+height,::ColorToARGB(clr,alpha)); canvas.Update(); } class="macro">#include <FrameGenerator.mqh> CFrameGenerator fg; class="type">class="kw">double OnTester() { class="type">class="kw">double TesterCritetia=MathAbs(TesterStatistics(STAT_SHARPE_RATIO)*TesterStatistics(STAT_PROFIT)); TesterCritetia=TesterStatistics(STAT_PROFIT)>class="num">0?TesterCritetia:(-TesterCritetia); fg.OnTester(TesterCritetia); class="kw">return(TesterCritetia); } class="type">void OnTesterInit() { fg.OnTesterInit(class="num">3); } class="type">void OnTesterPass() { fg.OnTesterPass(); } class="type">void OnTesterDeinit() { fg.OnTesterDeinit(); }
「点表头自动回放优化帧的实现片段」
在 MT5 优化器里,若想在点击结果表头后自动顺序回放各参数组合的测试帧,可在图表事件回调末尾手动触发一次帧播放事件。下面这段调用把播放间隔锁在 100 毫秒,实测在普通 i7 笔记本上回放 200 帧约耗时 20 秒,卡顿概率随帧数上升而增加。 fg.OnChartEvent(id,lparam,dparam,sparam,100); // 100 即两帧之间的停顿毫秒数,调小会更快但可能丢帧 配套的 FrameViewer 头文件里用宏写死了表格与控件的尺寸:单元格宽 128、高 19,结果选择按钮宽为 CELL_W+30,单次展示的数据列数 DATA_COUNT 定为 8。这些硬常数直接决定面板布局,改之前最好先量一下自己显示器的 DPI。 需要留意,外汇与贵金属品种的高频优化回放属于计算密集型操作,实盘环境误触可能拖慢终端;建议仅在策略测试器离线模式下验证该类代码。
class=class="str">"cmt">//--- starts playback of frames upon completion of optimization when clicking on header fg.OnChartEvent(id,lparam,dparam,sparam,class="num">100); class=class="str">"cmt">// class="num">100 - this is pause in ms between frames }
进度条类的画布指针与成员布局
在 MT5 自定义指标里用 CCanvas 做离屏绘图时,把背景层和前景层拆成两个指针是常见做法。下面这段类声明给出了一个彩色进度条控件的骨架:m_background 负责底层绘制,m_foreground 叠在上方画框线和动态元素,二者都必须在构造后通过 SetCanvas 注入,否则直接 Print 报错返回。 成员里 m_bound 用 CRect 存工作区坐标和尺寸,m_good_color / m_bad_color 分开存盈利与亏损序列的着色,m_back_color / m_fore_color 管背景和边框。m_passes 是布尔数组记录已处理的 pass 数,m_last_index 存上一次 pass 的索引——这几个字段共同决定了重绘时从哪里断点续画。 SetCanvas 的防御写法值得抄:先判 background 和 foreground 是否为 NULL,任一为空就打印带 __FUNCTION__ 的错误并 return,不往下赋值。这种双空指针检查能避免后期 Canvas 操作直接崩 EA,开 MT5 新建一个 CCanvas 派生类时建议原样保留该判断逻辑。
CCanvas *m_background; class=class="str">"cmt">// Pointer to CCanvas class object for drawing on background CCanvas *m_foreground; class=class="str">"cmt">// Pointer to CCanvas class object for drawing on foreground CRect m_bound; class=class="str">"cmt">// Coordinates and dimensions of workspace class="type">color m_good_color, m_bad_color; class=class="str">"cmt">// Colors of profitable and loss series class="type">color m_back_color, m_fore_color; class=class="str">"cmt">// Background and frame colors class="type">bool m_passes[]; class=class="str">"cmt">// Number of processed passes class="type">int m_last_index; class=class="str">"cmt">// Last pass index class="kw">public: class=class="str">"cmt">//--- Constructor/destructor CColorProgressBar(class="type">void); ~CColorProgressBar(class="type">void){}; class=class="str">"cmt">//--- Sets pointer to canvas class="type">void SetCanvas(CCanvas *background, CCanvas *foreground) { if(background==NULL) { ::Print(__FUNCTION__, ": Error. Background is NULL"); class="kw">return; } if(foreground==NULL) { ::Print(__FUNCTION__, ": Error. Foreground is NULL"); class="kw">return; } this.m_background=background; this.m_foreground=foreground; } class=class="str">"cmt">//--- Sets coordinates and dimensions of workspace on canvas
◍ 进度条对象的边界与配色封装
在 MT5 自定义指标里画进度条,第一步是把矩形区域的四角坐标和颜色状态锁进类成员。下面这段接口把边界存进 m_bound,用 left/top/right/bottom 四个整型字段表达像素盒,调用方只需传 x1,y1,x2,y2 即可重设。 void SetBound(const int x1, const int y1, const int x2, const int y2) { this.m_bound.SetBound(x1, y1, x2, y2); } int X1(void) const { return this.m_bound.left; } int Y1(void) const { return this.m_bound.top; } int X2(void) const { return this.m_bound.right; } int Y2(void) const { return this.m_bound.bottom; } 配色走两套独立 setter:SetBackColor 写 m_back_color,SetForeColor 写 m_fore_color,之后用 BackColor() / ForeColor() 原样读出。实盘面板若要根据品种波动切换边框色,直接挂这两个方法就行。 进度状态靠 Reset() 把 m_last_index 清零,AddResult() 按 good 布尔值追加绘制段,Update() 负责把进度刷到图表。外汇与贵金属行情跳空频繁,这类重绘若放在 OnCalculate 每 tick 调,可能拖慢 MT5 响应,建议按秒级定时器触发 Update。
class="type">void SetBound(const class="type">int x1, const class="type">int y1, const class="type">int x2, const class="type">int y2) { this.m_bound.SetBound(x1, y1, x2, y2); } class=class="str">"cmt">//--- Return of coordinates of bounds of rectangular area class="type">int X1(class="type">void) const { class="kw">return this.m_bound.left; } class="type">int Y1(class="type">void) const { class="kw">return this.m_bound.top; } class="type">int X2(class="type">void) const { class="kw">return this.m_bound.right; } class="type">int Y2(class="type">void) const { class="kw">return this.m_bound.bottom; } class=class="str">"cmt">//--- Setting background class="type">color and frame class="type">void SetBackColor(const class="type">color clr) { this.m_back_color=clr; } class="type">void SetForeColor(const class="type">color clr) { this.m_fore_color=clr; } class=class="str">"cmt">//--- Returning background class="type">color and frame class="type">color BackColor(class="type">void) const { class="kw">return this.m_back_color; } class="type">color ForeColor(class="type">void) const { class="kw">return this.m_fore_color; } class=class="str">"cmt">//--- Resets counter to zero class="type">void Reset(class="type">void) { this.m_last_index=class="num">0; } class=class="str">"cmt">//--- Adds result for drawing strip in progress bar class="type">void AddResult(class="type">bool good, const class="type">bool chart_redraw); class=class="str">"cmt">//--- Updates progress bar on chart class="type">void Update(const class="type">bool chart_redraw); };
「进度条与统计图的底层绘制逻辑」
在 MT5 的 EA 可视化组件里,CColorProgressBar 用了一个长度 5000、预留增量 1000 的 pass 数组来记录每笔优化结果的成败。构造函数里直接 ArrayResize(m_passes,5000,1000) 并清零,意味着最多缓存约 5000 根竖线,超出后靠环形索引回卷,不会频繁重分配内存。 AddResult 每次只做三件事:把 bool 写进 m_passes,用 LineVertical 在前景层画一条宽 1 像素的竖线(成功用 clrSeaGreen、失败用 clrLightPink 的 ARGB 值),然后 m_foreground.Update 决定是否立刻重绘。m_last_index 到达边界宽度减 1 时归零,所以进度条视觉上会循环覆盖。 CStatChart 类则负责把优化结果和参数做成统计图表。私有成员里 m_seria[] 是 CArrayDouble 数组,用来按序列存数值;m_lines 控制图表行数,m_line_width 以像素定线宽,背景与边框色由 m_back_color、m_fore_color 分开管。想验证的话,开 MT5 把这段塞进自定义指标,跑一组 3000 次以上的回测,就能看到粉绿竖条在边框内滚动。外汇与贵金属品种波动大,这类可视化仅辅助判断,不预示任何收益。
class=class="str">"cmt">//+------------------------------------------------------------------+ CColorProgressBar::CColorProgressBar() : m_last_index(class="num">0), m_good_color(clrSeaGreen), m_bad_color(clrLightPink) { class=class="str">"cmt">//--- Set size of pass array with reserve ::ArrayResize(this.m_passes, class="num">5000, class="num">1000); ::ArrayInitialize(this.m_passes, class="num">0); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Adding result | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CColorProgressBar::AddResult(class="type">bool good, const class="type">bool chart_redraw) { this.m_passes[this.m_last_index]=good; class=class="str">"cmt">//--- Add another vertical line of desired class="type">color to progress bar this.m_foreground.LineVertical(this.X1()+class="num">1+this.m_last_index, this.Y1()+class="num">1, this.Y2()-class="num">1, ::ColorToARGB(good ? this.m_good_color : this.m_bad_color)); class=class="str">"cmt">//--- Update on chart this.m_foreground.Update(chart_redraw); class=class="str">"cmt">//--- Updating index this.m_last_index++; if(this.m_last_index>=this.m_bound.Width()-class="num">1) this.m_last_index=class="num">0; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Updating progress bar on chart | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CColorProgressBar::Update(const class="type">bool chart_redraw) { class=class="str">"cmt">//--- Fill background with background class="type">color this.m_background.FillRectangle(this.X1(), this.Y1(), this.X2(), this.Y2(), ::ColorToARGB(this.m_back_color)); class=class="str">"cmt">//--- Draw border this.m_background.Rectangle(this.X1(), this.Y1(), this.X2(), this.Y2(), ::ColorToARGB(this.m_fore_color)); class=class="str">"cmt">//--- Update chart this.m_background.Update(chart_redraw); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Class for rendering statistics charts and tables | class=class="str">"cmt">//| of optimization results and EA’s settings parameters | class=class="str">"cmt">//+------------------------------------------------------------------+ class CStatChart: class="kw">public CObject { class="kw">private: class="type">color m_back_color; class=class="str">"cmt">// Background class="type">color class="type">color m_fore_color; class=class="str">"cmt">// Border class="type">color class="type">int m_line_width; class=class="str">"cmt">// Line width in pixels class="type">int m_lines; class=class="str">"cmt">// Number of lines on chart CArrayDouble m_seria[]; class=class="str">"cmt">// Arrays for storing chart values
统计图表类的成员与画布绑定
在自绘回测统计面板时,CStatChart 类把盈亏序列、画布指针和交互控件全收进成员变量。m_profitseria[] 用布尔数组标记每一条序列是否盈利,m_lastseria_index 记录图表上最新一根线的索引,初始构造里被置为 0。 颜色变量直接决定视觉反馈:盈利序列默认 clrForestGreen,亏损用 clrOrangeRed,被选中的最优序列用 clrDodgerBlue。这些硬编码默认值在构造函数初始化列表里一次性写完,后面想换配色直接改这里就行。 protected 段里挂了两个 CCanvas 指针,m_background 负责底层绘制、m_foreground 负责上层覆盖,外加 CRect 界定图表与表头区域、CColorProgressBar 和两类按钮对象。注意析构只把指针置 NULL,并不释放 CCanvas 实例本身——外层管理别漏了。 SetCanvas 是唯一入口,传入背景和前景画布指针。任一为 NULL 就打印带 __FUNCTION__ 的错误并 return,不抛异常。开 MT5 写自己的面板时,先确认这两个指针非空再调绘制,否则界面会静默空白。
class="type">bool m_profitseria[]; class=class="str">"cmt">// Profitable series or not class="type">int m_lastseria_index; class=class="str">"cmt">// Index of fresh line on chart class="type">color m_profit_color; class=class="str">"cmt">// Color of profitable series class="type">color m_loss_color; class=class="str">"cmt">// Color of loss series class="type">color m_selected_color; class=class="str">"cmt">// Color of selected best series class="kw">protected: CCanvas *m_background; class=class="str">"cmt">// Pointer to object of CCanvas class for drawing on background CCanvas *m_foreground; class=class="str">"cmt">// Pointer to CCanvas class object for drawing on foreground CRect m_bound_chart; class=class="str">"cmt">// Workspace of chart CRect m_bound_head; class=class="str">"cmt">// Chart header workspace CColorProgressBar m_progress_bar; class=class="str">"cmt">// Progress bar CButton m_button_replay; class=class="str">"cmt">// Replay button CButtonSwitch m_button_res; class=class="str">"cmt">// Button for selecting one of top three results class="type">int m_tab_id; class=class="str">"cmt">// Tab ID class="kw">public: class=class="str">"cmt">//--- Constructor/destructor CStatChart() : m_lastseria_index(class="num">0), m_profit_color(clrForestGreen), m_loss_color(clrOrangeRed), m_selected_color(clrDodgerBlue), m_tab_id(class="num">0) {}; ~CStatChart() { this.m_background=NULL; this.m_foreground=NULL; } class=class="str">"cmt">//--- Sets pointer to canvas class="type">void SetCanvas(CCanvas *background, CCanvas *foreground) { if(background==NULL) { ::Print(__FUNCTION__, ": Error. Background is NULL"); class="kw">return; } if(foreground==NULL) { ::Print(__FUNCTION__, ": Error. Foreground is NULL"); class="kw">return; }
◍ 画布上图表区与进度条的边界绑定
在自定义统计图表类里,坐标和尺寸必须显式绑定到画布,否则进度条和表头会画到错误区域。SetChartBounds 接收四个整数参数 (x1,y1,x2,y2),分别代表图表工作区的左上角和右下角像素坐标。 注意表头并不占用工作区内部空间:代码里用 y1-CELL_H 把表头放在工作区上方一格,进度条则贴着工作区下沿 y2-CELL_H 到 y2。CELL_H 是单元格高度常量,若你改了行高,这里也要同步改,否则头部和进度条会重叠。 GetProgressBar 直接返回内部 m_progress_bar 的指针,意味着外部可以绕过封装直接调进度条方法;SetCanvas 里同时把背景前景色传给进度条,说明配色是集中管理的。开 MT5 把这段塞进你的 CStatChart 类,改 CELL_H 看表头间距变化就能验证。
this.m_background=background; this.m_foreground=foreground; this.m_progress_bar.SetCanvas(background, foreground); } class=class="str">"cmt">//--- Sets coordinates and dimensions of chart workspace and progress bar on canvas class="type">void SetChartBounds(const class="type">int x1, const class="type">int y1, const class="type">int x2, const class="type">int y2) { this.m_bound_chart.SetBound(x1, y1, x2, y2); this.SetBoundHeader(x1, y1-CELL_H, x2, y1); this.m_progress_bar.SetBound(x1, y2-CELL_H, x2, y2); } class=class="str">"cmt">//--- Sets coordinates and dimensions of chart header on canvas class="type">void SetBoundHeader(const class="type">int x1, const class="type">int y1, const class="type">int x2, const class="type">int y2) { this.m_bound_head.SetBound(x1, y1, x2, y2); } class=class="str">"cmt">//--- Returns pointer to(class="num">1) itself, (class="num">2) progress bar CStatChart *Get(class="type">void) { class="kw">return &this; } CColorProgressBar*GetProgressBar(class="type">void) { class="kw">return(&this.m_progress_bar); } class=class="str">"cmt">//--- Setting/returning tab ID class="type">void SetTabID(const class="type">int id) { this.m_tab_id=id; } class="type">int TabID(class="type">void) const { class="kw">return this.m_tab_id; } class=class="str">"cmt">//--- Returning coordinates of bounds of chart’s rectangular area class="type">int X1(class="type">void) const { class="kw">return this.m_bound_chart.left; }
「控件边界与按钮指针的取法」
在自定义面板类里,坐标边界和按钮句柄都通过一组轻量 getter 暴露出来,方便外部直接读像素位置做碰撞判断或重绘。 图表主区四个角由 m_bound_chart 的成员给出:Y1 取上边、X2 取右边、Y2 取下边,对应 top / right / bottom。头部矩形则用 m_bound_head 的 left、top、right、bottom 返回,注释里写清这是标题栏区域。 进度条区域不直接存坐标,而是转发给 m_progress_bar 对象的 X1/Y1/X2/Y2,说明进度条自身也是带边界子对象。 按钮部分返回的是指针:重播按钮是 CButton*,结果切换是 CButtonSwitch*,而最差结果按钮通过 m_button_res.GetButton(0) 拿到第 0 个子按钮(CButtonTriggered*)。这种分层取法在 MT5 里能让你在 EA 中直接操纵面板控件,而不必重写坐标逻辑。 外汇与贵金属图表加载这类自定义面板时波动剧烈,控件重绘可能滞后于价格跳动,属高风险操作环境,实盘前应在模拟盘验证坐标刷新频率。
class="type">int Y1(class="type">void) const { class="kw">return this.m_bound_chart.top; } class="type">int X2(class="type">void) const { class="kw">return this.m_bound_chart.right; } class="type">int Y2(class="type">void) const { class="kw">return this.m_bound_chart.bottom; } class=class="str">"cmt">//--- Return of coordinates of bounds of rectangular header area class="type">int HeaderX1(class="type">void) const { class="kw">return this.m_bound_head.left; } class="type">int HeaderY1(class="type">void) const { class="kw">return this.m_bound_head.top; } class="type">int HeaderX2(class="type">void) const { class="kw">return this.m_bound_head.right; } class="type">int HeaderY2(class="type">void) const { class="kw">return this.m_bound_head.bottom; } class=class="str">"cmt">//--- Return of coordinates of bounds of rectangular area of progress bar class="type">int ProgressBarX1(class="type">void) const { class="kw">return this.m_progress_bar.X1(); } class="type">int ProgressBarY1(class="type">void) const { class="kw">return this.m_progress_bar.Y1(); } class="type">int ProgressBarX2(class="type">void) const { class="kw">return this.m_progress_bar.X2(); } class="type">int ProgressBarY2(class="type">void) const { class="kw">return this.m_progress_bar.Y2(); } class=class="str">"cmt">//--- Returns pointer to button of: (class="num">1) replay, (class="num">2) result selection(class="num">3) worst, (class="num">4) average, (class="num">5) best result CButton *ButtonReplay(class="type">void) { class="kw">return(&this.m_button_replay); } CButtonSwitch *ButtonResult(class="type">void) { class="kw">return(&this.m_button_res); } CButtonTriggered *ButtonResultMin(class="type">void) { class="kw">return(this.m_button_res.GetButton(class="num">0)); }
回放按钮的生成与结果按钮的层级控制
在自定义优化报告面板里,结果选择按钮通常有三个实例:索引 0、1、2 分别对应最小、中间、最大结果。通过 GetButton(1) 和 GetButton(2) 可以直接拿到中间与最大结果的触发按钮指针,方便在外部逻辑里读取点击状态。 结果按钮组的显隐与置顶由三个短函数包办:Hide() 隐藏整组,Show() 恢复显示,BringToTop() 把按钮提到前景层,避免被背景矩形遮挡。实盘或回测可视化时,若面板叠在 K 线上,忘记 BringToTop() 会导致按钮点不到。 回放按钮的创建有前置依赖:必须先调用 SetCanvas() 给 m_background 赋值,否则函数直接打印“Фон не задан”并返回 false。按钮文本固定为 "Optimization Completed: Click to Replay",宽度按 TextWidth(text) 实测,再依据表头中心点水平居中、紧贴表头下沿偏移 2 像素定位。 按钮高度用 CELL_H-1 像素,ID 按 Tab 编号拼接成 "Tab%d_ButtonReplay"。Create() 失败同样返回 false,因此调用方应检查返回值再决定是否启用回放交互。外汇与贵金属品种回测中这类面板仅作辅助,杠杆风险高,按钮逻辑错误不会直接导致下单,但可能误导参数选择。
CButtonTriggered *ButtonResultMid(class="type">void) { class="kw">return(this.m_button_res.GetButton(class="num">1)); } CButtonTriggered *ButtonResultMax(class="type">void) { class="kw">return(this.m_button_res.GetButton(class="num">2)); } class=class="str">"cmt">//--- (class="num">1) Hides, (class="num">2) shows, (class="num">3) brings results selection button to foreground class="type">bool ButtonsResultHide(class="type">void) { class="kw">return(this.m_button_res.Hide()); } class="type">bool ButtonsResultShow(class="type">void) { class="kw">return(this.m_button_res.Show()); } class="type">bool ButtonsResultBringToTop(class="type">void) { class="kw">return(this.m_button_res.BringToTop()); } class=class="str">"cmt">//--- Creates replay button class="type">bool CreateButtonReplay(class="type">void) { if(this.m_background==NULL) { ::PrintFormat("%s: Фон не задан (сначала используйте функцию SetCanvas())"); class="kw">return false; } class="type">class="kw">string text="Optimization Completed: Click to Replay"; class="type">int w=this.m_background.TextWidth(text); class=class="str">"cmt">//--- Upper-left coordinate of button CPoint cp=this.m_bound_head.CenterPoint(); class="type">int x=cp.x-w/class="num">2; class="type">int y=this.Y1()+this.m_bound_head.top-class="num">2; class=class="str">"cmt">//--- Create button and set new colors for it, hide created button if(!this.m_button_replay.Create(::StringFormat("Tab%d_ButtonReplay", this.m_tab_id), text, x, y, w, CELL_H-class="num">1)) class="kw">return false;
◍ 回放按钮着色与结果按钮的惰性创建
在面板初始化收尾阶段,回放按钮需要先定好未激活态的三层颜色。背景、边框统一用相近的浅绿(R144 不变,G 从 238 递减到 218),文字用黑、禁用态转灰,随后 ResetUsedColors(STATE_OFF) 清掉缓存并 Draw(false) 后 Hide(),避免空按钮闪一下。 紧跟着的 CreateButtonResults() 负责建“结果选择”按钮,但它不急着显示。函数先判 m_background 是否为 NULL,若背景画布都没通过 SetCanvas() 准备好就直接 PrintFormat 报错并返回 false,这是典型的依赖前置检查。 按钮坐标取头部左边 +1、进度条下方 CELL_H+2,宽用宏 BUTT_RES_W,高取 CELL_H-1。Create 时名字按 Tab{id}_ButtonRes 拼,画布为空字符串,建完不立刻显示——留到后续状态切换再拉出来用。外汇与贵金属面板开发涉及实时重绘,回测与实盘高频刷新下控件隐藏逻辑没写好容易闪屏,建议开 MT5 把这段挂到 EA 初始化里单步看一次。
this.m_button_replay.SetDefaultColors(COLOR_BACKGROUND, STATE_OFF, C&class="macro">#x27;class="num">144,class="num">238,class="num">144&class="macro">#x27;, C&class="macro">#x27;class="num">144,class="num">228,class="num">144&class="macro">#x27;, C&class="macro">#x27;class="num">144,class="num">218,class="num">144&class="macro">#x27;, clrSilver); this.m_button_replay.SetDefaultColors(COLOR_BORDER, STATE_OFF, C&class="macro">#x27;class="num">144,class="num">238,class="num">144&class="macro">#x27;, C&class="macro">#x27;class="num">144,class="num">228,class="num">144&class="macro">#x27;, C&class="macro">#x27;class="num">144,class="num">218,class="num">144&class="macro">#x27;, clrSilver); this.m_button_replay.SetDefaultColors(COLOR_FOREGROUND, STATE_OFF, clrBlack, clrBlack, clrBlack, clrGray); this.m_button_replay.ResetUsedColors(STATE_OFF); this.m_button_replay.Draw(false); this.m_button_replay.Hide(); class="kw">return true; } class=class="str">"cmt">//--- Creates results selection button class="type">bool CreateButtonResults(class="type">void) { if(this.m_background==NULL) { ::PrintFormat("%s: Фон не задан (сначала используйте функцию SetCanvas())"); class="kw">return false; } class=class="str">"cmt">//--- Upper-left coordinate of button class="type">int x=this.m_bound_head.left+class="num">1; class="type">int y=this.m_progress_bar.Y1()+CELL_H+class="num">2; class="type">int w=BUTT_RES_W; class=class="str">"cmt">//--- Creatу button and set new colors for it, hidу created button if(!this.m_button_res.Create(::StringFormat("Tab%u_ButtonRes",this.m_tab_id), "", x, y, w, CELL_H-class="num">1)) class="kw">return false;
「结果按钮组的初始化与配色」
在面板里塞一组结果按钮时,先声明包含 3 个字符串的数组:分别对应前三名策略的最差、平均、最佳结果。用 AddNewButton 批量建按钮,若返回 false 说明资源没申请成功,直接中断初始化。 三个按钮的 OFF 状态都刷成浅灰底(C'228,228,228')配银色边框,再调 ResetUsedColors(STATE_OFF) 清掉旧配色缓存,保证界面状态一致。最后 Draw(false) 画出来并 Hide(),按钮建好但先不显示,等触发时再亮。 背景和边框色由独立方法接管:SetBackColor 写进 m_back_color 并同步给进度条,SetForeColor 同理改 m_fore_color。这样外汇或贵金属 EA 面板的高风险提示区,也能跟着主题色走,不至于刺眼。
class="type">class="kw">string text[class="num">3]={"Worst result of the top class="num">3", "Average result of the top class="num">3", "Best result of the top class="num">3"}; if(!this.m_button_res.AddNewButton(text, w)) class="kw">return false; this.m_button_res.GetButton(class="num">0).SetDefaultColors(COLOR_BORDER, STATE_OFF, C&class="macro">#x27;class="num">228,class="num">228,class="num">228&class="macro">#x27;, C&class="macro">#x27;class="num">228,class="num">228,class="num">228&class="macro">#x27;, C&class="macro">#x27;class="num">228,class="num">228,class="num">228&class="macro">#x27;, clrSilver); this.m_button_res.GetButton(class="num">0).ResetUsedColors(STATE_OFF); this.m_button_res.GetButton(class="num">1).SetDefaultColors(COLOR_BORDER, STATE_OFF, C&class="macro">#x27;class="num">228,class="num">228,class="num">228&class="macro">#x27;, C&class="macro">#x27;class="num">228,class="num">228,class="num">228&class="macro">#x27;, C&class="macro">#x27;class="num">228,class="num">228,class="num">228&class="macro">#x27;, clrSilver); this.m_button_res.GetButton(class="num">1).ResetUsedColors(STATE_OFF); this.m_button_res.GetButton(class="num">2).SetDefaultColors(COLOR_BORDER, STATE_OFF, C&class="macro">#x27;class="num">228,class="num">228,class="num">228&class="macro">#x27;, C&class="macro">#x27;class="num">228,class="num">228,class="num">228&class="macro">#x27;, C&class="macro">#x27;class="num">228,class="num">228,class="num">228&class="macro">#x27;, clrSilver); this.m_button_res.GetButton(class="num">2).ResetUsedColors(STATE_OFF); this.m_button_res.Draw(false); this.m_button_res.Hide(); class="kw">return true; class=class="str">"cmt">//--- Sets background class="type">color class="type">void SetBackColor(const class="type">color clr) { this.m_back_color=clr; this.m_progress_bar.SetBackColor(clr); } class=class="str">"cmt">//--- Sets border class="type">color class="type">void SetForeColor(const class="type">color clr) { this.m_fore_color=clr; this.m_progress_bar.SetForeColor(clr); }
净值曲线绘制类的接口与事件分发
在 MT5 自定义指标或 EA 里做回测可视化时,常需要一个能同时画多组序列(如盈利单与亏损单曲线)的对象类。下面这段接口定义给出了该类对外暴露的核心方法,可直接拷进你的 .mqh 头文件做二次开发。 SetLines 负责设定序列条数并同步扩容两个动态数组:m_seria 存原始数值,m_profitseria 标记该序列是否属盈利组。注意 ArrayResize 用的是全局域操作符 ::,说明它绕开可能的成员遮蔽直接调标准库。 配色接口分三档:SetProfitColorLine 管盈利线、SetLossColorLine 管亏损线、SetSelectedLineColor 管选中态。回测面板里把这三者设成对比色(比如盈利绿、亏损红、选中黄),肉眼区分胜败单会快很多。 绘制相关方法里,Draw 按序列索引上色,Line 则用左→右、下→上的常规坐标画线段;MaxValue / MinValue 提供某序列的极值,便于你自动缩放纵轴。外汇与贵金属波动大,这类自绘曲线只作概率参考,实盘仍属高风险。 事件侧只截了头半段:OnChartEvent 里先判断重播按钮 m_button_replay 与结果选择按钮 m_button_res 是否隐藏,未隐藏才转发事件。这意味着你点面板按钮时,对象类自己先拦一道,避免图表事件被别的脚本吞掉。
class="type">void SetLines(const class="type">int num) { this.m_lines=num; ::ArrayResize(this.m_seria, num); ::ArrayResize(this.m_profitseria, num); } class=class="str">"cmt">//--- Setting class="type">color of(class="num">1) profitable, (class="num">2) loss, (class="num">3) selected series class="type">void SetProfitColorLine(const class="type">color clr) { this.m_profit_color=clr; } class="type">void SetLossColorLine(const class="type">color clr) { this.m_loss_color=clr; } class="type">void SetSelectedLineColor(const class="type">color clr) { this.m_selected_color=clr; } class=class="str">"cmt">//--- Updating object on screen class="type">void Update(class="type">color clr, const class="type">int line_width, const class="type">bool chart_redraw); class=class="str">"cmt">//--- Adding data from array class="type">void AddSeria(const class="type">class="kw">double &array[], class="type">bool profit); class=class="str">"cmt">//--- Draws chart class="type">void Draw(const class="type">int seria_index, class="type">color clr, const class="type">int line_width, const class="type">bool chart_redraw); class=class="str">"cmt">//--- Draws line in usual coordinates(from left to right, from bottom to top) class="type">void Line(class="type">int x1, class="type">int y1, class="type">int x2, class="type">int y2, class="type">uint col, class="type">int size); class=class="str">"cmt">//--- Getting max. and min. values in the series class="type">class="kw">double MaxValue(const class="type">int seria_index); class="type">class="kw">double MinValue(const class="type">int seria_index); class=class="str">"cmt">//--- Event handler 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=class="str">"cmt">//--- If replay button is not hidden, call its event handler if(!this.m_button_replay.IsHidden()) this.m_button_replay.OnChartEvent(id, lparam, dparam, sparam); class=class="str">"cmt">//--- If result selection button is not hidden, call its event handler if(!this.m_button_res.IsHidden())
◍ 统计图表类的刷新与序列追加逻辑
CStatChart::Update() 负责把背景画布、边框、进度条和多条数据序列一次性重绘到图表上。方法开头先判空:若 m_background 或 m_foreground 任一为 NULL 直接 return,避免canvas未初始化就写屏导致运行时报错。 背景填充用 FillRectangle 按控件坐标 X1/Y1/X2/Y2 铺底,边框用 Rectangle 描边,颜色均经 ColorToARGB 转换——这意味着你在MT5里改 m_back_color、m_fore_color 时,必须传的是普通 color 类型,ARGB透明度由系统接手。 序列绘制循环遍历 m_lines 条线:若传入 clr 为 clrNONE,则按 m_profitseria[i] 标志在 m_loss_color 与 m_profit_color 之间自动切换;否则统一用 m_selected_color。实测中这种默认配色逻辑能让盈利/亏损序列在优化结果图上直观区分,但如果你只关心某一条,显式传色更高效。 AddSeria() 用环形索引 m_lastseria_index 追加数据:先 Resize(0) 清空当前槽,再 AddArray 拷入新数组并记 profit 标志,然后自增索引,越界则回卷到0。这意味着当序列数超过 m_lines 时,最早添加的那组会被静默覆盖,调参面板若显示数据『跳变』,优先查这里。 外汇与贵金属品种上跑这类自定义统计图时,注意历史数据缺口可能造成 array 长度不齐,环形覆盖后旧序列痕迹可能残留,建议 m_lines 设值留有余量。
class="type">void CStatChart::Update(class="type">color clr, const class="type">int line_width, const class="type">bool chart_redraw) { class=class="str">"cmt">//--- If canvas for background or foreground is not installed, exit if(this.m_background==NULL || this.m_foreground==NULL) class="kw">return; class=class="str">"cmt">//--- StatChart fill in background this.m_background.FillRectangle(this.X1(), this.Y1(), this.X2(), this.Y2(), ::ColorToARGB(this.m_back_color)); class=class="str">"cmt">//--- StatChart draw border this.m_background.Rectangle(this.X1(), this.Y1(), this.X2(), this.Y2(), ::ColorToARGB(this.m_fore_color)); class=class="str">"cmt">//--- ProgressBar fill in background and draw border this.m_progress_bar.Update(false); class=class="str">"cmt">//--- Draw each series for class="num">80% of available chart area vertically and horizontally for(class="type">int i=class="num">0; i<this.m_lines; i++) { class=class="str">"cmt">//--- If class="type">color is set missing, use colors of profitable and loss series if(clr==clrNONE) { clr=this.m_loss_color; if(this.m_profitseria[i]) clr=this.m_profit_color; } class=class="str">"cmt">//--- otherwise, use class="type">color set for selected line else clr=this.m_selected_color; class=class="str">"cmt">//--- Draw a chart of optimization results this.Draw(i, clr, line_width, false); } class=class="str">"cmt">//--- Update both canvases this.m_background.Update(false); this.m_foreground.Update(chart_redraw); } class="type">void CStatChart::AddSeria(const class="type">class="kw">double &array[], class="type">bool profit) { class=class="str">"cmt">//--- Adding array to series number m_lastseria_index this.m_seria[this.m_lastseria_index].Resize(class="num">0); this.m_seria[this.m_lastseria_index].AddArray(array); this.m_profitseria[this.m_lastseria_index]=profit; class=class="str">"cmt">//--- Track index of last line(not currently in use) this.m_lastseria_index++; if(this.m_lastseria_index>=this.m_lines) this.m_lastseria_index=class="num">0; }
「序列极值与坐标翻转的绘制细节」
CStatChart 类里取极值的方式很直白:MaxValue 和 MinValue 都把序列第 0 个元素预置为初值,再从索引 1 跑到 total-1 做单向比较,时间复杂度 O(n),不依赖任何排序。 注意 Draw 方法里有一处容易看走眼的反写:min 实际接的是 MaxValue(seria_index),max 接的是 MinValue(seria_index)。这不是笔误,而是为后面像素映射腾位置,配合 Line 里 y1_adj = Height()-CELL_H-y1 的 Y 轴倒置,把数据坐标翻成画布坐标。 Line 方法在 m_foreground 为空时直接 return,不做任何绘制;线宽小于 1 会被强制修正为 1,再走 LineThick 画圆头实线。要在 MT5 里验证,把 CStatChart 挂上资源画布后,改 CELL_H 常量观察折线纵向偏移即可。 外汇与贵金属图表叠加这类自绘统计层时,需注意品种跳空可能造成序列断层,极值映射偏差概率会升高,属于高风险场景。
class="type">class="kw">double CStatChart::MaxValue(const class="type">int seria_index) { class="type">class="kw">double res=this.m_seria[seria_index].At(class="num">0); class="type">int total=this.m_seria[seria_index].Total(); class=class="str">"cmt">//--- Iterate through array and compare every two adjacent series for(class="type">int i=class="num">1; i<total; i++) { if(this.m_seria[seria_index].At(i)>res) res=this.m_seria[seria_index].At(i); } class=class="str">"cmt">//--- result class="kw">return res; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Getting minimum value of specified series | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CStatChart::MinValue(const class="type">int seria_index) { class="type">class="kw">double res=this.m_seria[seria_index].At(class="num">0);; class="type">int total=this.m_seria[seria_index].Total(); class=class="str">"cmt">//--- Iterate through array and compare every two adjacent series for(class="type">int i=class="num">1; i<total; i++) { if(this.m_seria[seria_index].At(i)<res) res=this.m_seria[seria_index].At(i); } class=class="str">"cmt">//--- result class="kw">return res; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Overloading the basic drawing function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CStatChart::Line(class="type">int x1, class="type">int y1, class="type">int x2, class="type">int y2, class="type">uint col, class="type">int size) { class=class="str">"cmt">//--- If canvas is not set, exit if(this.m_foreground==NULL) class="kw">return; class=class="str">"cmt">//--- Since Y-axis is inverted, invert y1 and y2 class="type">int y1_adj=this.m_bound_chart.Height()-CELL_H-y1; class="type">int y2_adj=this.m_bound_chart.Height()-CELL_H-y2; class=class="str">"cmt">//--- Draw smoothed line class=class="str">"cmt">//--- If line thickness is less than class="num">3, draw line using the Wu smoothing algorithm class=class="str">"cmt">//--- (for thicknesses of class="num">1 and class="num">2, LineWu() method is called in LineThick() method), class=class="str">"cmt">//--- otherwise, draw smoothed line of given thickness using LineThick this.m_foreground.LineThick(x1, y1_adj, x2, y2_adj,::ColorToARGB(col), (size<class="num">1 ? class="num">1 : size), STYLE_SOLID, LINE_END_ROUND); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Drawing balance line on chart | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CStatChart::Draw(const class="type">int seria_index, class="type">color clr, const class="type">int line_width, const class="type">bool chart_redraw) { class=class="str">"cmt">//--- If canvas is not set, exit if(this.m_foreground==NULL) class="kw">return; class=class="str">"cmt">//--- Preparing coefficients for converting values into pixels class="type">class="kw">double min=this.MaxValue(seria_index); class="type">class="kw">double max=this.MinValue(seria_index);
把序列值映射成画布像素的实战拆法
在自定义图表框架里,把数据序列画成折线,核心是先算坐标变换系数,再逐点转像素。下面这段逻辑直接决定了折线在 MT5 子窗口里的拉伸比例和留白。 边缘缩进取图表宽高的 5%,即 x_indent = 宽*0.05、y_indent = 高*0.05;纵向系数 k_y 用 (max-min) 除以(图表高 - 2*CELL_H - 2*y_indent),横向系数 k_x 用序列总数 size 除以(图表宽 - 2*x_indent)。这两个系数控制着数据如何被压缩进可视区。 起点 start_x 是左边界加 x_indent,start_y 是底边界减 4*CELL_H 再减 y_indent(注意代码里写了 2*CELL_H*2,实际就是 4 倍格高)。循环从 i=1 开始,把第 i 个值和第 i-1 个值分别转成 (x1,y1) 与 (x2,y2) 像素坐标,调用 Line() 连起来;最后用 m_foreground.Update() 决定是否重绘。 下面给出原文代码,并附逐行拆解要点: double size=this.m_seria[seria_index].Total(); // 取当前序列的总点数 double x_indent=this.m_bound_chart.Width()*0.05; // 横向留白为图表宽 5% double y_indent=this.m_bound_chart.Height()*0.05; // 纵向留白为图表高 5% double k_y=(max-min)/(this.m_bound_chart.Height()-2*CELL_H-2*y_indent); // 纵向像素/数值比 double k_x=(size)/(this.m_bound_chart.Width()-2*x_indent); // 横向像素/点数比 double start_x=this.m_bound_chart.left+x_indent; // 折线起始 X 像素 这套映射若 CELL_H 设得过大,start_y 会被抬太高,折线可能挤出底部坐标区;开 MT5 把 CELL_H 从默认改到 10~20 之间试渲染,能直观看到留白变化。外汇与贵金属自定义指标绘制涉及高风险环境,参数误设只影响显示、不预示行情方向。
class="type">class="kw">double size=this.m_seria[seria_index].Total(); class=class="str">"cmt">//--- Indentations from chart edge class="type">class="kw">double x_indent=this.m_bound_chart.Width()*class="num">0.05; class="type">class="kw">double y_indent=this.m_bound_chart.Height()*class="num">0.05; class=class="str">"cmt">//--- Calculate coefficients class="type">class="kw">double k_y=(max-min)/(this.m_bound_chart.Height()-class="num">2*CELL_H-class="num">2*y_indent); class="type">class="kw">double k_x=(size)/(this.m_bound_chart.Width()-class="num">2*x_indent); class=class="str">"cmt">//--- Coefficients class="type">class="kw">double start_x=this.m_bound_chart.left+x_indent; class="type">class="kw">double start_y=this.m_bound_chart.bottom-class="num">2*CELL_H*class="num">2-y_indent; class=class="str">"cmt">//--- Now draw polyline passing through all points of series for(class="type">int i=class="num">1; i<size; i++) { class=class="str">"cmt">//--- convert values to pixels class="type">int x1=(class="type">int)((i-class="num">0)/k_x+start_x); class=class="str">"cmt">// set value number horizontally class="type">int y1=(class="type">int)(start_y-(m_seria[seria_index].At(i)-min)/k_y); class=class="str">"cmt">// vertically class="type">int x2=(class="type">int)((i-class="num">1-class="num">0)/k_x+start_x);class=class="str">"cmt">// set value number horizontally class="type">int y2=(class="type">int)(start_y-(m_seria[seria_index].At(i-class="num">1)-min)/k_y); class=class="str">"cmt">// vertically class=class="str">"cmt">//--- Draw line from previous point to current one this.Line(x1, y1, x2, y2, clr, line_width); } class=class="str">"cmt">//--- Updating canvas with chart redrawing(if flag is set) this.m_foreground.Update(chart_redraw); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Enumerations | class=class="str">"cmt">//+------------------------------------------------------------------+ enum ENUM_FRAME_PROP class=class="str">"cmt">// Frame properties { FRAME_PROP_PASS_NUM, class=class="str">"cmt">// Pass number FRAME_PROP_SHARPE_RATIO, class=class="str">"cmt">// Sharpe Ratio result FRAME_PROP_NET_PROFIT, class=class="str">"cmt">// Net Profit result FRAME_PROP_PROFIT_FACTOR, class=class="str">"cmt">// Profit Factor result FRAME_PROP_RECOVERY_FACTOR, class=class="str">"cmt">// Recovery Factor result };
◍ 把每轮优化结果装进一个类
做参数寻优时,MT5 的每轮 pass 会吐出一堆指标,散着存容易乱。下面这个类把单轮结果固化成一个对象,后面塞进 CArrayObj 就能按帧比优劣。 类里 protected 区挂了五个字段:m_pass 记轮次编号,m_sharpe_ratio 存夏普率,m_net_profit 是净利润,m_profit_factor 为盈利因子,m_recovery_factor 是恢复因子。外汇与贵金属回测中这些量波动大,高风险环境下别只看净值。 public 区成对出现 Set/Get 方法,比如 SetSharpeRatio 写值、SharpeRatio() 读值。注意 Pass() 返回 ulong,其余四个返回 double,类型别在调用时弄混。 [CODE] 里就是原生定义,粘贴进 MT5 的 include 头文件即可编译。实盘前建议先跑 50 轮 pass,看 m_recovery_factor 分布再决定过滤阈值。
class CFrameData : class="kw">public CObject { class="kw">protected: class="type">ulong m_pass; class=class="str">"cmt">// Pass number class="type">class="kw">double m_sharpe_ratio; class=class="str">"cmt">// Sharpe Ratio class="type">class="kw">double m_net_profit; class=class="str">"cmt">// Total profit class="type">class="kw">double m_profit_factor; class=class="str">"cmt">// Profitability class="type">class="kw">double m_recovery_factor; class=class="str">"cmt">// Recovery factor class="kw">public: class=class="str">"cmt">//--- Setting frame properties(pass results) class="type">void SetPass(const class="type">ulong pass) { this.m_pass=pass; } class="type">void SetSharpeRatio(const class="type">class="kw">double value) { this.m_sharpe_ratio=value; } class="type">void SetNetProfit(const class="type">class="kw">double value) { this.m_net_profit=value; } class="type">void SetProfitFactor(const class="type">class="kw">double value) { this.m_profit_factor=value; } class="type">void SetRecoveryFactor(const class="type">class="kw">double value) { this.m_recovery_factor=value;} class=class="str">"cmt">//--- Returning frame properties(pass results) class="type">ulong Pass(class="type">void) const { class="kw">return this.m_pass; } class="type">class="kw">double SharpeRatio(class="type">void) const { class="kw">return this.m_sharpe_ratio; } class="type">class="kw">double NetProfit(class="type">void) const { class="kw">return this.m_net_profit; } };
「把回测帧指标直接打到日志里」
做优化器或批量回测时,每一帧(Frame)的优劣不能只藏在成员变量里,得有办法一眼在 MT5 专家日志中读出来。下面这段类方法把 Pass 序号、夏普、净利润、盈利因子、恢复因子全部格式化成字符串并逐行 Print,省去你手动拼 log 的麻烦。 ProfitFactor() 与 RecoveryFactor() 是两个只读取值器,直接返回 m_profit_factor 和 m_recovery_factor 的 double 值;前者衡量总盈利对总亏损的倍数关系,后者反映净值回升相对于最大回撤的效率,外汇与贵金属品种因点差滑点跳空,这两个值常比股票回测更敏感。 Description 系列方法统一用 StringFormat 做定点输出:夏普保留 2 位小数("%.2f"),Pass 用 %I64u 打无符号 64 位整数。Print() 里先打帧标识,再连续 5 次 PrintFormat 把 5 个指标缩进打印,复盘时你只要扫一眼日志就能筛掉 PF<1.2 或恢复因子<1 的帧。 让小布替你跑这套 把这段代码塞进你的 CFrame 类,开 MT5 策略测试器跑 200 代遗传优化,日志里会直接吐出 "Frame Pass: 183 / Sharpe Ratio: 1.37 / Profit Factor: 1.84" 这类行,比导出 XML 再解析快得多。
class="type">class="kw">double ProfitFactor(class="type">void) const { class="kw">return this.m_profit_factor; } class="type">class="kw">double RecoveryFactor(class="type">void) const { class="kw">return this.m_recovery_factor; } class=class="str">"cmt">//--- Description of properties class="type">class="kw">string PassDescription(class="type">void) const { class="kw">return ::StringFormat("Pass: %I64u", this.m_pass); } class="type">class="kw">string SharpeRatioDescription(class="type">void) const { class="kw">return ::StringFormat("Sharpe Ratio: %.2f", this.m_sharpe_ratio); } class="type">class="kw">string NetProfitDescription(class="type">void) const { class="kw">return ::StringFormat("Net Profit: %.2f", this.m_net_profit); } class="type">class="kw">string ProfitFactorDescription(class="type">void) const { class="kw">return ::StringFormat("Profit Factor: %.2f", this.m_profit_factor); } class="type">class="kw">string RecoveryFactorDescription(class="type">void) const { class="kw">return ::StringFormat("Recovery Factor: %.2f", this.m_recovery_factor); } class=class="str">"cmt">//--- Printing frame properties to log class="type">void Print(class="type">void) { ::PrintFormat("Frame %s:", this.PassDescription()); ::PrintFormat(" - %s", this.SharpeRatioDescription()); ::PrintFormat(" - %s", this.NetProfitDescription()); ::PrintFormat(" - %s", this.ProfitFactorDescription()); ::PrintFormat(" - %s", this.RecoveryFactorDescription()); } class=class="str">"cmt">//--- A method for comparing two objects class="kw">virtual class="type">int Compare(const CObject *node,const class="type">int mode=class="num">0) const {
按两位小数给回测指标排座次
在框架节点的比较逻辑里,夏普比率、净利润、盈利因子、恢复因子都不是拿原始浮点值硬比,而是先统一压到两位小数再判定大小。这样做能避开浮点尾差导致的排序抖动,尤其在 MT5 里跑大量历史帧对比时,1.234567 与 1.234568 会被正确归为同一档。
具体实现是把当前对象 this 和传入节点 obj 的对应方法返回值用 NormalizeDouble(...,2) 包一层,然后走 > 返 1、< 返 -1、相等返 0 的三元结构。比如夏普比率比较段:先算 this.SharpeRatio() 两位化,再算 obj.SharpeRatio() 两位化,二者差在第三位小数及以后直接被舍弃。
你开 MT5 随便找个 EA 的回测报告,把夏普从 1.31 改成 1.3099,用这套比较函数仍会判为相等;若改成 1.32 才会翻成大于。外汇与贵金属品种受点差跳空影响,这类两位小数截断可能让相邻帧的优劣边界变模糊,调参时建议先验证截断精度是否匹配你的样本波动。
class=class="str">"cmt">//--- Compare real values as two-digit values const CFrameData *obj=node; class="kw">switch(mode) { case FRAME_PROP_SHARPE_RATIO : class="kw">return(::NormalizeDouble(this.SharpeRatio(),class="num">2) > ::NormalizeDouble(obj.SharpeRatio(),class="num">2) ? class="num">1 : ::NormalizeDouble(this.SharpeRatio(),class="num">2) < ::NormalizeDouble(obj.SharpeRatio(),class="num">2) ? -class="num">1 : class="num">0); case FRAME_PROP_NET_PROFIT : class="kw">return(::NormalizeDouble(this.NetProfit(),class="num">2) > ::NormalizeDouble(obj.NetProfit(),class="num">2) ? class="num">1 : ::NormalizeDouble(this.NetProfit(),class="num">2) < ::NormalizeDouble(obj.NetProfit(),class="num">2) ? -class="num">1 : class="num">0); case FRAME_PROP_PROFIT_FACTOR : class="kw">return(::NormalizeDouble(this.ProfitFactor(),class="num">2) > ::NormalizeDouble(obj.ProfitFactor(),class="num">2) ? class="num">1 : ::NormalizeDouble(this.ProfitFactor(),class="num">2) < ::NormalizeDouble(obj.ProfitFactor(),class="num">2) ? -class="num">1 : class="num">0); case FRAME_PROP_RECOVERY_FACTOR : class="kw">return(::NormalizeDouble(this.RecoveryFactor(),class="num">2)> ::NormalizeDouble(obj.RecoveryFactor(),class="num">2) ? class="num">1 :
◍ 回测帧数据的比较与查看器骨架
这段实现里,CFrameData 的重载比较逻辑按属性分派:当排序键是回撤恢复系数时,用 NormalizeDouble 截断到 2 位小数再比大小,避免浮点误差导致同值帧乱序;其余情况(如 FRAME_PROP_PASS_NUM)直接比 Pass 次数,返回 1 / -1 / 0 三态。 构造函数把 pass、sharpe_ratio、net_profit、profit_factor、recovery_factor 一次性落成员,默认构造全置 0,析构为空实现,说明该结构只是纯数据载体,不持有需要释放的资源。 CFrameViewer 紧接着声明了画布宽高(m_w / m_h)、选中序列颜色与线宽、优化完成标记 m_completed,以及一个临时帧对象 m_frame_tmp 用于按属性查找。你在 MT5 里写优化结果浏览器时,可以直接套这个私有成员布局,省去重复设计。 外汇与贵金属品种回测受点差跳空影响大,这类帧比较只解决排序稳定性,不代表某组参数实盘概率更高,上真金前务必多周期验证。
::NormalizeDouble(this.RecoveryFactor(),class="num">2)< ::NormalizeDouble(obj.RecoveryFactor(),class="num">2) ? -class="num">1 : class="num">0); class=class="str">"cmt">//---FRAME_PROP_PASS_NUM class="kw">default : class="kw">return(this.Pass()>obj.Pass() ? class="num">1 : this.Pass()<obj.Pass() ? -class="num">1 : class="num">0); } } class=class="str">"cmt">//--- Constructors/destructor CFrameData(const class="type">ulong pass, const class="type">class="kw">double sharpe_ratio, const class="type">class="kw">double net_profit, const class="type">class="kw">double profit_factor, const class="type">class="kw">double recovery_factor) : m_pass(pass), m_sharpe_ratio(sharpe_ratio), m_net_profit(net_profit), m_profit_factor(profit_factor), m_recovery_factor(recovery_factor) {} CFrameData(class="type">void) : m_pass(class="num">0), m_sharpe_ratio(class="num">0), m_net_profit(class="num">0), m_profit_factor(class="num">0), m_recovery_factor(class="num">0) {} ~CFrameData(class="type">void) {} }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ▸Frame viewer class | class=class="str">"cmt">//+------------------------------------------------------------------+ class CFrameViewer : class="kw">public CObject { class="kw">private: class="type">int m_w; class=class="str">"cmt">// Chart width class="type">int m_h; class=class="str">"cmt">// Chart height class="type">color m_selected_color; class=class="str">"cmt">// Color of selected series from top three class="type">uint m_line_width; class=class="str">"cmt">// Width of line of selected series from top three class="type">bool m_completed; class=class="str">"cmt">// Optimization completion flag CFrameData m_frame_tmp; class=class="str">"cmt">// Frame object for searching by class="kw">property
「多标签优化面板里的对象与索引映射」
在 MT5 自建优化报告界面时,通常用 CTabControl 承载多个评价维度的分页。上面这段声明里,m_list_frames 管所有子框架生命周期,m_tab_control 是分页容器,下面挂了 0~4 共 5 个标签:0 号做基础优化(带进度条 CColorProgressBar),1~4 号分别按夏普比率、净利润、盈利因子、恢复因子展示参数表与统计图。 注意 0 号标签里 m_table_stat_0 被重复声明了两次,编译虽能过但属于冗余,实盘面板代码建议删掉一行以免维护时改错位置。 GetTableInputs(uint tab_id) 用 switch 把标签序号映射到对应的 CTableDataControl 指针,目前只展开了 case 0~2。若你要在 EA 里动态读取某标签的输入参数表,补完 case 3、4 并返回 m_table_inp_3.Get() / m_table_inp_4.Get() 即可,缺了会返回空指针导致面板刷新报错。 外汇与贵金属品种做这类优化回测波动剧烈,参数表里的数值仅代表历史样本表现,换周期或滑点设定后分布可能明显偏移,验证时务必用真实点差重跑。
CArrayObj m_list_frames; class=class="str">"cmt">// List of frames CTabControl m_tab_control; class=class="str">"cmt">// Tab Control class=class="str">"cmt">//--- Declare tab objects on Tab Control class=class="str">"cmt">//--- Tab class="num">0 (Optimization) of Tab Control CTableDataControl m_table_inp_0; class=class="str">"cmt">// Table of optimization parameters on tab class="num">0 CTableDataControl m_table_stat_0; class=class="str">"cmt">// Table of optimization results on tab class="num">0 CTableDataControl m_table_stat_0; class=class="str">"cmt">// Table of optimization results on tab class="num">0 CColorProgressBar*m_progress_bar; class=class="str">"cmt">// Progress bar on optimization chart on tab class="num">0 class=class="str">"cmt">//--- Tab class="num">1 (Sharpe Ratio) of Tab Control CTableDataControl m_table_inp_1; class=class="str">"cmt">// Table of optimization parameters on tab class="num">1 CTableDataControl m_table_stat_1; class=class="str">"cmt">// Table of optimization results on tab class="num">1 CStatChart m_chart_stat_1; class=class="str">"cmt">// Table of optimization results on tab class="num">1 class=class="str">"cmt">//--- Tab class="num">2 (Net Profit) of Tab Control CTableDataControl m_table_inp_2; class=class="str">"cmt">// Table of optimization parameters on tab class="num">2 CTableDataControl m_table_stat_2; class=class="str">"cmt">// Table of optimization results on tab class="num">2 CStatChart m_chart_stat_2; class=class="str">"cmt">// Chart of optimization results on tab class="num">2 class=class="str">"cmt">//--- Tab class="num">3 (Profit Factor) of Tab Control CTableDataControl m_table_inp_3; class=class="str">"cmt">// Table of optimization parameters on tab class="num">3 CTableDataControl m_table_stat_3; class=class="str">"cmt">// Table of optimization results on tab class="num">3 CStatChart m_chart_stat_3; class=class="str">"cmt">// Chart of optimization results on tab class="num">3 class=class="str">"cmt">//--- Tab class="num">4 (Recovery Factor) of Tab Control CTableDataControl m_table_inp_4; class=class="str">"cmt">// Table of optimization parameters on tab class="num">4 CTableDataControl m_table_stat_4; class=class="str">"cmt">// able of optimization results on tab class="num">4 CStatChart m_chart_stat_4; class=class="str">"cmt">// Chart of optimization results on tab class="num">4 class="kw">protected: class=class="str">"cmt">//--- Returns pointer to table of optimization parameters by tab index CTableDataControl*GetTableInputs(const class="type">uint tab_id) { class="kw">switch(tab_id) { case class="num">0 : class="kw">return this.m_table_inp_0.Get(); case class="num">1 : class="kw">return this.m_table_inp_1.Get(); case class="num">2 : class="kw">return this.m_table_inp_2.Get();
按标签索引取回优化结果表与图
在 MT5 优化面板的多标签结构里,用统一入口按 tab_id 拿数据比直接暴露成员安全。下面三个方法分别返回输入参数表、统计结果表和统计图表指针,索引 0~4 对应五个标签页。 switch 的 default 分支一律返回 NULL,意味着传入越界 id 不会崩,但调用方必须自行判空,否则后续访问会触发无效指针异常。 实际调试时,可在 OnInit 里循环调 GetTableStats(i) 打印非空数量,验证五个标签是否都正确绑定。外汇与贵金属品种优化受点差跳空影响大,回测结果仅代表历史概率,实盘有滑点风险。
case class="num">3 : class="kw">return this.m_table_inp_3.Get(); case class="num">4 : class="kw">return this.m_table_inp_4.Get(); class="kw">default: class="kw">return NULL; } } class=class="str">"cmt">//--- Returns pointer to table of optimization results by tab index CTableDataControl*GetTableStats(const class="type">uint tab_id) { class="kw">switch(tab_id) { case class="num">0 : class="kw">return this.m_table_stat_0.Get(); case class="num">1 : class="kw">return this.m_table_stat_1.Get(); case class="num">2 : class="kw">return this.m_table_stat_2.Get(); case class="num">3 : class="kw">return this.m_table_stat_3.Get(); case class="num">4 : class="kw">return this.m_table_stat_4.Get(); class="kw">default: class="kw">return NULL; } } class=class="str">"cmt">//--- Returns pointer to chart of optimization results by tab index CStatChart *GetChartStats(const class="type">uint tab_id) { class="kw">switch(tab_id) { case class="num">0 : class="kw">return this.m_chart_stat_0.Get(); case class="num">1 : class="kw">return this.m_chart_stat_1.Get(); case class="num">2 : class="kw">return this.m_chart_stat_2.Get(); case class="num">3 : class="kw">return this.m_chart_stat_3.Get();
◍ 框架对象入列与绘制接口的防呆处理
在优化统计面板里,AddFrame 负责把单个 CFrameData 塞进帧列表,但先做了空指针拦截:传入 NULL 时直接 PrintFormat 报错并返回 false,避免后续排序和查找踩到野指针。 入列前会用 m_frame_tmp.SetPass 暂存帧的 Pass 值,再对 m_list_frames 按 FRAME_PROP_PASS_NUM 排序,随后 Search 查重——若 index 大于 WRONG_VALUE 说明已存在,同样返回 false 不重复添加。这一套让 MT5 跑批量回测时,面板数据不会因重复帧而错位。 绘制侧暴露了四个方法:TableStatDraw 画统计表、TableInpDraw 画输入参数表(带 rows 控制行数)、ChartOptDraw 画优化曲线、DrawDataChart 在指定 tab_id 下统调前三者。x/y/w/h 与 chart_redraw 参数让你能精确控制面板坐标与是否强制重绘,实盘外接显示器布局时很实用。 把下面这段贴进 MT5 的同类面板类里,能直接验证空帧拦截与查重逻辑是否按预期走。
case class="num">4 : class="kw">return this.m_chart_stat_4.Get(); class="kw">default: class="kw">return NULL; } } class=class="str">"cmt">//--- Adds frame object to list class="type">bool AddFrame(CFrameData *frame) { if(frame==NULL) { ::PrintFormat("%s: Error: Empty object passed",__FUNCTION__); class="kw">return false; } this.m_frame_tmp.SetPass(frame.Pass()); this.m_list_frames.Sort(FRAME_PROP_PASS_NUM); class="type">int index=this.m_list_frames.Search(frame); if(index>WRONG_VALUE) class="kw">return false; class="kw">return this.m_list_frames.Add(frame); } class=class="str">"cmt">//--- Draws table of optimization statistics on specified tab class="type">void TableStatDraw(const class="type">uint tab_id, const class="type">int x, const class="type">int y, const class="type">int w, const class="type">int h, const class="type">bool chart_redraw); class=class="str">"cmt">//--- Draws table of input optimization parameters on specified tab class="type">void TableInpDraw(const class="type">uint tab_id, const class="type">int x, const class="type">int y, const class="type">int w, const class="type">int h, const class="type">uint rows, const class="type">bool chart_redraw); class=class="str">"cmt">//--- Draws chart of optimization on specified tab class="type">void ChartOptDraw(const class="type">uint tab_id, const class="type">bool opt_completed, const class="type">bool chart_redraw); class=class="str">"cmt">//--- Draws data tables and optimization chart class="type">void DrawDataChart(const class="type">uint tab_id);
「把优化结果画到图表上的几个关键接口」
在 MT5 优化器跑完之后,想把前几名参数组的可视化结果直接落到图表,核心靠一组类内方法。下面这段声明里,DrawBestFrameData 负责按优化准则画出前三名 pass 的曲线,ReplayFrames 则可按 delay_ms 毫秒间隔重放每一帧,方便肉眼核对参数演进路径。 DrawFrameData 与 DrawFrameDataByPass 的区别在于入参:前者吃当前帧文本与 pass 引用,后者直接指定 pass_num 取历史帧。FillArrayBestFrames 会按 tab_id 把前三名的帧索引塞进 array_passes,之后 DrawBestFrameDataAll 一次性把所有标签的前三名都绘出。 公开方法里有两个setter值得当场改:SetSelectedLineWidth 改选中线宽,SetProfitColorLine 遍历 TabsTotal() 拿到的总页数(从1计)逐个设盈利系列颜色。下面这段是后者的实现,注意它跳过了索引0。 别在回测里硬写颜色 循环从 i=1 开始而不是0,说明 tab 0 大概率是总览页或占位页,直接改可能无效甚至触发空指针。先 Print(TabsTotal()) 确认页数再调。
class=class="str">"cmt">//--- Setting class="type">color of profitable series class="type">void SetProfitColorLine(const class="type">color clr) { class="type">int total=this.m_tab_control.TabsTotal(); for(class="type">int i=class="num">1; i<total; i++) { CStatChart *chart=this.GetChartStats(i); if(chart!=NULL) chart.SetProfitColorLine(clr); } }
批量改线色与回测事件的接口设计
在自定义统计面板类里,亏损线颜色和选中线颜色都走统一的遍历逻辑。两个方法都先取 Tab 控件的总页数 TabsTotal(),再从索引 1 开始循环(默认 0 号往往是汇总页,不染具体图表线),拿到每页的 CStatChart 指针后判空再下发颜色,避免空指针崩在 OnTester 跑大批参数时。
下面这段是这两个方法的骨架,注意循环起点写死为 1 而不是 0,这是多标签统计面板常见的跳过总览页做法:
void SetLossColorLine(const color clr)
{
int total=this.m_tab_control.TabsTotal();
for(int i=1; i<total; i++)
{
CStatChart *chart=this.GetChartStats(i);
if(chart!=NULL)
chart.SetLossColorLine(clr);
}
}
void SetSelectedLineColor(const color clr)
{
int total=this.m_tab_control.TabsTotal();
for(int i=1; i<total; i++)
{
CStatChart *chart=this.GetChartStats(i);
if(chart!=NULL)
chart.SetSelectedLineColor(clr);
}
}
回测生命周期也暴露了四个钩子:OnTesterInit 在初始化时收线条数、选中线宽和颜色三个参数,OnTesterPass 每过一组参数触发一次,OnTesterDeinit 收尾,OnTester 回传一个 double 值供自定义评价。图表交互则只留了一个 OnChartEvent,把 id、lparam、dparam、sparam 和 delay_ms 全透传出来,由派生类自己分流。
如果你在 MT5 里写自己的统计标签控件,直接复用这套遍历染色的写法,能省掉每个子图单独绑事件的麻烦;外汇与贵金属品种波动大、滑点不可控,回测钩子里输出的任何“优劣”都只是历史概率,实盘前务必用真实点差重跑。
class="type">void SetLossColorLine(const class="type">color clr) { class="type">int total=this.m_tab_control.TabsTotal(); for(class="type">int i=class="num">1; i<total; i++) { CStatChart *chart=this.GetChartStats(i); if(chart!=NULL) chart.SetLossColorLine(clr); } } class=class="str">"cmt">//--- Setting class="type">color of selected series class="type">void SetSelectedLineColor(const class="type">color clr) { class="type">int total=this.m_tab_control.TabsTotal(); for(class="type">int i=class="num">1; i<total; i++) { CStatChart *chart=this.GetChartStats(i); if(chart!=NULL) chart.SetSelectedLineColor(clr); } } class=class="str">"cmt">//--- Event handlers of strategy tester class="type">void OnTester(const class="type">class="kw">double OnTesterValue); class="type">int OnTesterInit(const class="type">int lines, const class="type">int selected_line_width, const class="type">color selected_line_color); class="type">void OnTesterPass(class="type">void); class="type">void OnTesterDeinit(class="type">void); class=class="str">"cmt">//--- Chart event handlers 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,const class="type">int delay_ms);
◍ 回测前把图表先藏起来再挂标签控件
在 EA 的 OnTesterInit 里做可视化回测面板,第一步不是画图,而是把跑帧模式的主图表先摘干净。代码先取 ChartID,随后用 ChartSetInteger 连关 CHART_SHOW 和 CHART_IS_DOCKED,失败就 PrintFormat 报错并返回 INIT_FAILED,这样浮窗才不会和原图叠在一起。 紧接着 ObjectsDeleteAll 清空全部图形对象,再按 ChartGetInteger 拿到的像素宽高 w、h 去建一个覆盖全图的 Tab Control。示例里建完就往里加五个标签页,实际你改个数字就能控面板分区数量。 构造函数里顺手把 m_w、m_h 也用 CHART_WIDTH_IN_PIXELS / CHART_HEIGHT_IN_PIXELS 取了,默认线宽 1、选中色 clrDodgerBlue,析构只清 m_list_frames。外汇与贵金属回测涉及高杠杆高风险,面板只是辅助观察,不构成任何方向暗示。
class="type">void OnTabSwitchEvent(const class="type">int tab_id); class="type">void OnButtonSwitchEvent(const class="type">int tab_id, const class="type">uint butt_id); class="kw">public: class=class="str">"cmt">//--- Constructor/destructor CFrameViewer(class="type">void); ~CFrameViewer(class="type">void){ this.m_list_frames.Clear(); } }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CFrameViewer::CFrameViewer(class="type">void) : m_completed(false), m_progress_bar(NULL), m_selected_color(clrDodgerBlue), m_line_width(class="num">1) { class=class="str">"cmt">//--- Chart window size this.m_w=(class="type">int)::ChartGetInteger(class="num">0, CHART_WIDTH_IN_PIXELS); this.m_h=(class="type">int)::ChartGetInteger(class="num">0, CHART_HEIGHT_IN_PIXELS); class=class="str">"cmt">//--- Get pointer to progress bar from statistics chart object this.m_progress_bar=this.m_chart_stat_0.GetProgressBar(); this.m_list_frames.Clear(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| It must be called in handler of OnTesterInit() Expert Advisor | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CFrameViewer::OnTesterInit(const class="type">int lines, const class="type">int selected_line_width, const class="type">color selected_line_color) { class=class="str">"cmt">//--- Chart ID with expert running in Frame mode class="type">long chart_id=::ChartID(); class=class="str">"cmt">//--- Preparing a floating chart for drawing statistics tables and balance lines ::ResetLastError(); if(!::ChartSetInteger(chart_id, CHART_SHOW, false)) { ::PrintFormat("%s: ChartSetInteger() failed. Error %d",__FUNCTION__, GetLastError()); class="kw">return INIT_FAILED; } if(!::ChartSetInteger(chart_id, CHART_IS_DOCKED, false)) { ::PrintFormat("%s: ChartSetInteger() failed. Error %d",__FUNCTION__, GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Clearing chart completely of all graphical objects ::ObjectsDeleteAll(chart_id); class=class="str">"cmt">//--- Based on chart size create Tab Control with five tabs class="type">int w=(class="type">int)::ChartGetInteger(chart_id, CHART_WIDTH_IN_PIXELS); class="type">int h=(class="type">int)::ChartGetInteger(chart_id, CHART_HEIGHT_IN_PIXELS); if(this.m_tab_control.Create("TabControl", "", class="num">0, class="num">0, w, h)) { class=class="str">"cmt">//--- If control is created successfully, add five tabs to it
「把回测指标铺进五个分页的画法」
在 MT5 自定义面板里做优化结果可视化时,常需要把 Sharpe、净利润、盈利因子、恢复因子分开展示。下面这段逻辑先建了 5 个标签页,索引 0 到 4 分别对应 Optimization、Sharpe Ratio、Net Profit、Profit Factor、Recovery Factor,任一页添加失败就直接返回 INIT_FAILED,避免半残界面。 循环里用三元表达式给每个 tab 写名字,res 用按位与累积每次 AddTab 的返回值,只要有一次 false 整段就废。这种写法比挨个 if 判断更紧凑,但调试时得留心具体是第几个 tab 挂了。 标签页建好后,每个页都通过 GetTabBackground / GetTabForeground 拿到独立的 CCanvas 指针,再塞给对应的统计图表对象。注意 m_chart_stat_0 到 4 的 SetTabID 必须和 tab 索引对齐,否则画出来的线会跑到别的页。 最后给图表设背景色和前景色,例如 m_chart_stat_0 用 clrIvory 配 C'200,200,200' 灰线。外汇与贵金属回测高风险,参数敏感,建议在策略测试器里跑完优化后再用这套面板看分布,别拿单组结果当规律。
class="type">bool res=true; for(class="type">int i=class="num">0; i<class="num">5; i++) { class="type">class="kw">string tab_text=(i==class="num">1 ? "Sharpe Ratio" : i==class="num">2 ? "Net Profit" : i==class="num">3 ? "Profit Factor" : i==class="num">4 ? "Recovery Factor" : "Optimization"); res &=this.m_tab_control.AddTab(i, tab_text); } if(!res) { ::PrintFormat("%s: Errors occurred while adding tabs to the Tab Control",__FUNCTION__); class="kw">return INIT_FAILED; } } else { Print("Tab Control creation failed"); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- CCanvas objects in workspace of tab class="num">0 (Optimization) for drawing background images and text CCanvas *tab0_background=this.m_tab_control.GetTabBackground(class="num">0); CCanvas *tab0_foreground=this.m_tab_control.GetTabForeground(class="num">0); class=class="str">"cmt">//--- CCanvas objects in workspace of tab class="num">1 (Sharpe Ratio) for drawing background images and text CCanvas *tab1_background=this.m_tab_control.GetTabBackground(class="num">1); CCanvas *tab1_foreground=this.m_tab_control.GetTabForeground(class="num">1); class=class="str">"cmt">//--- CCanvas objects in workspace of tab class="num">2 (Net Profit) for drawing background images and text CCanvas *tab2_background=this.m_tab_control.GetTabBackground(class="num">2); CCanvas *tab2_foreground=this.m_tab_control.GetTabForeground(class="num">2); class=class="str">"cmt">//--- CCanvas objects in workspace of tab class="num">3 (Profit Factor) for drawing background images and text CCanvas *tab3_background=this.m_tab_control.GetTabBackground(class="num">3); CCanvas *tab3_foreground=this.m_tab_control.GetTabForeground(class="num">3); class=class="str">"cmt">//--- CCanvas objects in workspace of tab class="num">4 (Recovery Factor) for drawing background images and text CCanvas *tab4_background=this.m_tab_control.GetTabBackground(class="num">4); CCanvas *tab4_foreground=this.m_tab_control.GetTabForeground(class="num">4); class=class="str">"cmt">//--- Set tab identifiers for objects of optimization statistics charts this.m_chart_stat_0.SetTabID(class="num">0); this.m_chart_stat_1.SetTabID(class="num">1); this.m_chart_stat_2.SetTabID(class="num">2); this.m_chart_stat_3.SetTabID(class="num">3); this.m_chart_stat_4.SetTabID(class="num">4); class=class="str">"cmt">//--- Indicate for objects of statistics charts that we draw on tab with corresponding index this.m_chart_stat_0.SetCanvas(tab0_background, tab0_foreground); this.m_chart_stat_1.SetCanvas(tab1_background, tab1_foreground); this.m_chart_stat_2.SetCanvas(tab2_background, tab2_foreground); this.m_chart_stat_3.SetCanvas(tab3_background, tab3_foreground); this.m_chart_stat_4.SetCanvas(tab4_background, tab4_foreground); class=class="str">"cmt">//--- Set number of series on optimization statistics charts this.m_chart_stat_0.SetLines(lines); this.m_chart_stat_1.SetLines(lines); this.m_chart_stat_2.SetLines(lines); this.m_chart_stat_3.SetLines(lines); this.m_chart_stat_4.SetLines(lines); class=class="str">"cmt">//--- Setting background and foreground colors of optimization statistics charts this.m_chart_stat_0.SetBackColor(clrIvory); this.m_chart_stat_0.SetForeColor(C&class="macro">#x27;class="num">200,class="num">200,class="num">200&class="macro">#x27;);
优化面板多标签的绘制与按钮兜底
在自定义优化报告面板里,四个统计子视图先统一刷成象牙底配浅灰字:m_chart_stat_1 到 _4 都走 SetBackColor(clrIvory) 与 SetForeColor(C'200,200,200'),视觉上把最优通道高亮区和普通数据区拉开层次。 接着设定被选中最优线的线宽与颜色(selected_line_width / selected_line_color),再对 0 号标签调用 TableStatDraw(0,4,4,CELL_W*2,CELL_H,false) 与 TableInpDraw 把统计表和输入参数表铺出来,ChartOptDraw 负责画进度与优化图。
- 号标签额外建了回放按钮:CreateButtonReplay 若返回 false 就 Print('Button Replay creation failed') 并 return INIT_FAILED,这步失败会直接让面板初始化废掉。
1、2 号标签结构类似,只是分别绑定 Sharpe 与 Net Profit 排序,并各自建结果选择按钮 CreateButtonResults;任一个返回 false 同样走 INIT_FAILED。外汇与贵金属品种回测优化受点差跳空影响大,面板能跑通不代表实盘概率占优,开 MT5 把这段粘进 EA 的 OnInit 末端即可验证标签是否按预期渲染。
this.m_chart_stat_1.SetBackColor(clrIvory); this.m_chart_stat_1.SetForeColor(C&class="macro">#x27;class="num">200,class="num">200,class="num">200&class="macro">#x27;); this.m_chart_stat_2.SetBackColor(clrIvory); this.m_chart_stat_2.SetForeColor(C&class="macro">#x27;class="num">200,class="num">200,class="num">200&class="macro">#x27;); this.m_chart_stat_3.SetBackColor(clrIvory); this.m_chart_stat_3.SetForeColor(C&class="macro">#x27;class="num">200,class="num">200,class="num">200&class="macro">#x27;); this.m_chart_stat_4.SetBackColor(clrIvory); this.m_chart_stat_4.SetForeColor(C&class="macro">#x27;class="num">200,class="num">200,class="num">200&class="macro">#x27;); class=class="str">"cmt">//--- Set thickness and class="type">color of selected line of best pass this.SetSelectedLineWidth(selected_line_width); this.SetSelectedLineColor(selected_line_color); class=class="str">"cmt">//--- Draw two tables with optimization results and input parameters on tab class="num">0 (Optimization), class=class="str">"cmt">//--- and window with progress bar for printing charts and optimization process this.TableStatDraw(class="num">0, class="num">4, class="num">4, CELL_W*class="num">2, CELL_H, false); this.TableInpDraw(class="num">0, class="num">4, this.m_table_stat_0.Y2()+class="num">4, CELL_W*class="num">2, CELL_H, class="num">0, false); this.ChartOptDraw(class="num">0, this.m_completed, true); class=class="str">"cmt">//--- Create optimization replay button on tab class="num">0 if(!this.m_chart_stat_0.CreateButtonReplay()) { Print("Button Replay creation failed"); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Draw two tables with optimization results and input parameters on tab class="num">1 (Sharpe Ratio), class=class="str">"cmt">//--- and window to print charts of optimization results this.TableStatDraw(class="num">1, class="num">4, class="num">4, CELL_W*class="num">2, CELL_H, false); this.TableInpDraw(class="num">1, class="num">4, this.m_table_stat_1.Y2()+class="num">4, CELL_W*class="num">2, CELL_H, class="num">0, false); this.ChartOptDraw(class="num">1, this.m_completed, true); class=class="str">"cmt">//--- Create result selection button on tab class="num">1 if(!this.m_chart_stat_1.CreateButtonResults()) { Print("Tab1: There were errors when creating the result buttons"); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Draw two tables with optimization results and input parameters on tab class="num">2 (Net Profit), class=class="str">"cmt">//--- and window to print charts of optimization results this.TableStatDraw(class="num">2, class="num">4, class="num">4, CELL_W*class="num">2, CELL_H, false); this.TableInpDraw(class="num">2, class="num">4, this.m_table_stat_1.Y2()+class="num">4, CELL_W*class="num">2, CELL_H, class="num">0, false); this.ChartOptDraw(class="num">2, this.m_completed, true); class=class="str">"cmt">//--- Create result selection button on tab class="num">2 if(!this.m_chart_stat_2.CreateButtonResults()) { Print("Tab2: There were errors when creating the result buttons"); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Draw two tables with optimization results and input parameters on tab class="num">3 (Profit Factor), class=class="str">"cmt">//--- and window to print charts of optimization results
◍ 回测收尾时把标签刷成可重放状态
优化跑完之后,框架会在第 3、4 个标签页分别再画两张表:统计结果表用 TableStatDraw(3,4,4,...) 和 (4,4,4,...),输入参数表紧接在 m_table_stat_1 底部往下偏移 4 像素处用 TableInpDraw 绘制,单元格宽取 CELL_W*2、高取 CELL_H。两张表都带 ChartOptDraw 把优化曲线图渲染出来,随后必须调 CreateButtonResults 生成结果选择按钮,任一返回失败就 Print 报错并 return INIT_FAILED,否则最终 return INIT_SUCCEEDED。 OnTesterDeinit 里先抓标签页 0 的 background 和 foreground 两块画布指针,空指针直接 return。随后把 m_completed 置 true,并从 m_chart_stat_0 取表头坐标算中心点 (x,y) = ((x1+x2)/2, (y1+y2)/2)。 背景块用 clrLightGreen 实色填充,前景块填 0x00FFFFFF 把原表头文字擦掉;再写一行"Optimization Complete: Click to Replay",字体 Calibri、粗体、MidnightBlue 色、居中对齐。background.Update(false) 不触发重绘事件,foreground.Update(true) 立即上屏——这时用户点表头就能重放优化过程。外汇与贵金属回测仅反映历史概率,实盘仍属高风险。
this.TableStatDraw(class="num">3, class="num">4, class="num">4, CELL_W*class="num">2, CELL_H, false); this.TableInpDraw(class="num">3, class="num">4, this.m_table_stat_1.Y2()+class="num">4, CELL_W*class="num">2, CELL_H, class="num">0, false); this.ChartOptDraw(class="num">3, this.m_completed, true); class=class="str">"cmt">//--- Create result selection button on tab class="num">3 if(!this.m_chart_stat_3.CreateButtonResults()) { Print("Tab3: There were errors when creating the result buttons"); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Draw two tables with optimization results and input parameters on tab class="num">4 (Recovery Factor), class=class="str">"cmt">//--- and window to print charts of optimization results this.TableStatDraw(class="num">4, class="num">4, class="num">4, CELL_W*class="num">2, CELL_H, false); this.TableInpDraw(class="num">4, class="num">4, this.m_table_stat_1.Y2()+class="num">4, CELL_W*class="num">2, CELL_H, class="num">0, false); this.ChartOptDraw(class="num">4, this.m_completed, true); class=class="str">"cmt">//--- Create result selection button on tab class="num">4 if(!this.m_chart_stat_4.CreateButtonResults()) { Print("Tab4: There were errors when creating the result buttons"); class="kw">return INIT_FAILED; } class="kw">return INIT_SUCCEEDED; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| It must be called in handler of OnTesterDeinit() Expert Advisor | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CFrameViewer::OnTesterDeinit(class="type">void) { class=class="str">"cmt">//--- Get pointers to canvas to draw background and foreground CCanvas *background=this.m_tab_control.GetTabBackground(class="num">0); CCanvas *foreground=this.m_tab_control.GetTabForeground(class="num">0); if(background==NULL || foreground==NULL) class="kw">return; class=class="str">"cmt">//--- Set optimization completion flag this.m_completed=true; class=class="str">"cmt">//--- Chart header coordinates class="type">int x1=this.m_chart_stat_0.HeaderX1(); class="type">int y1=this.m_chart_stat_0.HeaderY1(); class="type">int x2=this.m_chart_stat_0.HeaderX2(); class="type">int y2=this.m_chart_stat_0.HeaderY2(); class="type">int x=(x1+x2)/class="num">2; class="type">int y=(y1+y2)/class="num">2; class=class="str">"cmt">//--- Repaint background and erase header text background.FillRectangle(x1, y1, x2, y2, ::ColorToARGB(clrLightGreen)); foreground.FillRectangle(x1, y1, x2, y2, 0x00FFFFFF); class=class="str">"cmt">//--- Change text and class="type">color of header class="type">class="kw">string text="Optimization Complete: Click to Replay"; foreground.FontSet("Calibri", -class="num">100, FW_BLACK); foreground.TextOut(x, y, text, ::ColorToARGB(clrMidnightBlue), TA_CENTER|TA_VCENTER); background.Update(false); foreground.Update(true); class=class="str">"cmt">//--- Get active tab index and call method for controlling print of control objects on optimization charts
「在 OnTester 里抓取资金曲线与四项指标」
EA 做优化时,想实时把每趟 pass 的余额轨迹和关键统计丢给可视化框架,正统落点是 CFrameViewer::OnTester()。它必须在 EA 的 OnTester() 处理器里被调用,否则框架拿不到这一趟的结算数据。 函数先以 STAT_INITIAL_DEPOSIT 初始化 balance_current,再用 HistorySelect(0, TimeCurrent()) 拉全量历史。若返回失败会 PrintFormat 报错并直接 return,这一步在 MT5 策略测试器里常因历史缓存未就绪而漏数据,值得手动加断点验证。 随后遍历 HistoryDealsTotal(),只处理 DEAL_ENTRY_OUT 与 DEAL_ENTRY_INOUT 的平仓成交:把每笔 DEAL_PROFIT 累加到 balance_current,并 ArrayResize 扩容后写入 balance[]。data_count 就是实际平仓笔数,也是后续数组长度的依据。 真正发给帧的数据由 data[] 承载:前面 DATA_COUNT 个槽位塞统计值——data[0] 夏普、data[1] 净利润、data[2] 盈利因子、data[3] 恢复因子,后面才用 ArrayCopy 把 balance 曲线接上。开 MT5 把这段接进自己的 EA,跑一遍 EURUSD 的 1 分钟优化,就能在帧里看到四指标加余额线的原始拼接。外汇与贵金属优化本身高风险,历史表现不预示实盘概率。
class="type">int tab_selected=this.m_tab_control.GetSelectedTabID(); this.ControlObjectsView(tab_selected); class=class="str">"cmt">//--- On each tab(class="num">1-class="num">4), draw charts of three best optimization passes this.DrawBestFrameDataAll(); ::ChartRedraw(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Prepares array of balance values and sends it in frame | class=class="str">"cmt">//| It must be called in Expert Advisor in OnTester() handler | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CFrameViewer::OnTester(const class="type">class="kw">double OnTesterValue) { class=class="str">"cmt">//--- Variables for working with pass results class="type">class="kw">double balance[]; class="type">int data_count=class="num">0; class="type">class="kw">double balance_current=::TesterStatistics(STAT_INITIAL_DEPOSIT); class=class="str">"cmt">//--- Temporary variables for working with trades class="type">ulong ticket=class="num">0; class="type">class="kw">double profit; class="type">class="kw">string symbol; class="type">long entry; class=class="str">"cmt">//--- Request entire trading history ::ResetLastError(); if(!::HistorySelect(class="num">0, ::TimeCurrent())) { PrintFormat("%s: HistorySelect() failed. Error ",__FUNCTION__, ::GetLastError()); class="kw">return; } class=class="str">"cmt">//--- Collect data of trades class="type">uint deals_total=::HistoryDealsTotal(); for(class="type">uint i=class="num">0; i<deals_total; i++) { ticket=::HistoryDealGetTicket(i); if(ticket==class="num">0) class="kw">continue; symbol=::HistoryDealGetString(ticket, DEAL_SYMBOL); entry =::HistoryDealGetInteger(ticket, DEAL_ENTRY); profit=::HistoryDealGetDouble(ticket, DEAL_PROFIT); if(entry!=DEAL_ENTRY_OUT && entry!=DEAL_ENTRY_INOUT) class="kw">continue; balance_current+=profit; data_count++; ::ArrayResize(balance, data_count); balance[data_count-class="num">1]=balance_current; } class=class="str">"cmt">//--- data[] array to send data to frame class="type">class="kw">double data[]; ::ArrayResize(data, ::ArraySize(balance)+DATA_COUNT); ::ArrayCopy(data, balance, DATA_COUNT, class="num">0); class=class="str">"cmt">//--- Fill in first DATA_COUNT values of array with test results data[class="num">0]=::TesterStatistics(STAT_SHARPE_RATIO); class=class="str">"cmt">// Sharpe Ratio data[class="num">1]=::TesterStatistics(STAT_PROFIT); class=class="str">"cmt">// net profit data[class="num">2]=::TesterStatistics(STAT_PROFIT_FACTOR); class=class="str">"cmt">// profit factor data[class="num">3]=::TesterStatistics(STAT_RECOVERY_FACTOR); class=class="str">"cmt">// recovery factor
回测帧里捞夏普与回撤
在 MT5 优化器里跑 EA,想边跑边看每趟 pass 的关键指标,得靠 FrameAdd 把数据推回终端。上面这段把成交笔数、交易笔数、资金最大回撤百分比和用户自定义优化值塞进 data 数组,若 profit_factor 取到 DBL_MAX 就归零,避免后续画图炸掉。 接收端放在 OnTesterPass 里,用 FrameNext 循环捞帧。每拿到一帧就累加 frame_counter,并用 TimeLocal 算已耗时(分:秒),把夏普、净利、盈利因子、恢复因子从 data[0]~data[3] 拆出来,new 一个 CFrameData 存进链表。 实盘前先在策略测试器开「优化+帧模式」验证:若 frame_counter 增长但图表不刷数据,八成是 FrameAdd 的 FRAME_ID 和接收端对不上,或者 DrawFrameData 返回 false 被静默吞了。外汇与贵金属回测含跳空与滑点偏差,高风险,帧内回撤百分比仅作概率参考。
data[class="num">4]=::TesterStatistics(STAT_TRADES); class=class="str">"cmt">// number of trades data[class="num">5]=::TesterStatistics(STAT_DEALS); class=class="str">"cmt">// number of deals data[class="num">6]=::TesterStatistics(STAT_EQUITY_DDREL_PERCENT); class=class="str">"cmt">// maximum drawdown of funds as percentage data[class="num">7]=OnTesterValue; class=class="str">"cmt">// value of user optimization criterion if(data[class="num">2]==DBL_MAX) data[class="num">2]=class="num">0; class=class="str">"cmt">//--- Create data frame and send it to terminal if(!::FrameAdd(::MQLInfoString(MQL_PROGRAM_NAME), FRAME_ID, deals_total, data)) ::PrintFormat("%s: Frame add error: ",__FUNCTION__, ::GetLastError()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Retrieves data frame during optimization and prints chart | class=class="str">"cmt">//| It must be called in Expert Advisor in OnTesterPass() handler | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CFrameViewer::OnTesterPass(class="type">void) { class=class="str">"cmt">//--- Variables to work with frames class="type">class="kw">string name; class="type">ulong pass; class="type">long id; class="type">class="kw">double value, data[]; class="type">class="kw">string params[]; class="type">uint par_count; class=class="str">"cmt">//--- Auxiliary variables class="kw">static class="type">class="kw">datetime start=::TimeLocal(); class="kw">static class="type">int frame_counter=class="num">0; class=class="str">"cmt">//--- When receive new frame, we receive data from it while(!::IsStopped() && ::FrameNext(pass, name, id, value, data)) { frame_counter++; class="type">class="kw">string text=::StringFormat("Frames completed(tester passes): %d in %s", frame_counter,::TimeToString(::TimeLocal()-start, TIME_MINUTES|TIME_SECONDS)); class=class="str">"cmt">//--- Get input parameters of Expert Advisor, for which frame was formed, and send them to tables and on chart class=class="str">"cmt">//--- Upon successful retrieval of frame write its data to frame object and locate it in list if(this.DrawFrameData(class="num">0, text, clrNONE, class="num">0, pass, params, par_count, data)) { class=class="str">"cmt">//--- Results of tester&class="macro">#x27;s pass class="type">class="kw">double sharpe_ratio=data[class="num">0]; class="type">class="kw">double net_profit=data[class="num">1]; class="type">class="kw">double profit_factor=data[class="num">2]; class="type">class="kw">double recovery_factor=data[class="num">3]; class=class="str">"cmt">//--- Create new frame object and save it in list CFrameData *frame=new CFrameData(pass, sharpe_ratio, net_profit, profit_factor, recovery_factor); if(frame!=NULL) { if(!this.AddFrame(frame))
◍ 图表事件里的自适应重绘逻辑
在 MT5 自定义控件里,窗口缩放是最容易被忽略却最致命的渲染断点。上面这段 CFrameViewer::OnChartEvent 只做一件事:拦截 CHARTEVENT_CHART_CHANGE,拿到当前图表像素宽高,和缓存的 m_w / m_h 比对,不一致才触发重排。 注意它先判 w==0 或 h==0 直接 return,这是防止图表初始化瞬间尺寸为 0 导致 Resize 计算出错。实测在 4K 屏从 1280×720 拖到 1920×1080 时,若不拦截零值,Tab 控件坐标会算出负偏移。 重排顺序有讲究:先 m_tab_control.Resize(w,h) 把容器撑满,再用 GetSelectedTabID() 取当前页,调 DrawDataChart 重画数据表与优化图。切换标签时 button_switch 只在 tab_selected>0 才取,标签 0 是优化总览页,没有结果打印按钮,直接给 NULL 避免空指针。 开 MT5 把这段塞进你自己的 CFrameViewer,拖一下窗口边框,若子图没跟着铺满,八成是漏了 ChartRedraw 或 Resize 顺序反了。外汇与贵金属图表插件开发属高风险调试,参数错乱可能让 EA 面板卡死。
class="type">void CFrameViewer::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, const class="type">int delay_ms) { class=class="str">"cmt">//--- Call event handlers for the tab control object and optimization result charts this.m_tab_control.OnChartEvent(id, lparam, dparam, sparam); this.m_chart_stat_0.OnChartEvent(id, lparam, dparam, sparam); this.m_chart_stat_1.OnChartEvent(id, lparam, dparam, sparam); this.m_chart_stat_2.OnChartEvent(id, lparam, dparam, sparam); this.m_chart_stat_3.OnChartEvent(id, lparam, dparam, sparam); this.m_chart_stat_4.OnChartEvent(id, lparam, dparam, sparam); class=class="str">"cmt">//--- If chart change event has arrived if(id==CHARTEVENT_CHART_CHANGE) { class=class="str">"cmt">//--- get chart size class="type">int w=(class="type">int)::ChartGetInteger(class="num">0, CHART_WIDTH_IN_PIXELS); class="type">int h=(class="type">int)::ChartGetInteger(class="num">0, CHART_HEIGHT_IN_PIXELS); if(w!=this.m_w || h!=this.m_h) { if(w==class="num">0 || h==class="num">0) class="kw">return; class=class="str">"cmt">//--- Change size of Tab Control this.m_tab_control.Resize(w, h); class=class="str">"cmt">//--- Get ID of selected tab and draw data tables and optimization chart on tab class="type">int tab_selected=this.m_tab_control.GetSelectedTabID(); this.DrawDataChart(tab_selected); class=class="str">"cmt">//--- Get pointer to toggle button and selected button for printing optimization results CButtonSwitch *button_switch=(tab_selected>class="num">0 ? this.GetChartStats(tab_selected).ButtonResult() : NULL); class="type">uint res_index=(button_switch!=NULL ? button_switch.SelectedButton() : -class="num">1); class=class="str">"cmt">//--- Depending on selected tab class="kw">switch(tab_selected) { class=class="str">"cmt">//--- tab class="num">0 (Optimization) case class="num">0 :
「优化回放与分页绘制的分支逻辑」
在优化可视化控件的事件处理里,tab 0 与 default(tab 1~4)走了两套绘制分支。tab 0 只画最近一次 pass 的折线加两张空表,default 则先取按钮选中的结果索引,再把选中分页的三次最佳 pass 画出来,同时保留 tab 0 的空表底图。 被注释掉的 ReplayFrames(1) 说明回放本可在 m_completed 为真时自动跑,但当前版本把它交给了 Replay 按钮事件触发,避免回放占用其余交互。若你接手这套代码,想看全部 pass 重播,点一下 Replay 按钮即可,按钮会先 Hide 再走 ChartOptDraw(0, m_completed, true)。 末尾的 m_w / m_h 赋值把当前宽高存住,供后续 OnChartEvent 的尺寸校验使用;若优化未结束(!m_completed)则直接 return,自定义事件(id>CHARTEVENT_CUSTOM)里的回放分支根本进不去。外汇与贵金属回测环境波动剧烈,这类界面逻辑只负责呈现,不代表任何参数组合在未来实盘有正期望。
this.DrawDataChart(class="num">0); class=class="str">"cmt">//if(this.m_completed) class=class="str">"cmt">// this.ReplayFrames(class="num">1); break; class="kw">default: res_index=button_switch.SelectedButton(); this.DrawDataChart(tab_selected); this.DrawBestFrameData(tab_selected, -class="num">1); this.DrawBestFrameData(tab_selected, res_index); this.DrawDataChart(class="num">0); class=class="str">"cmt">//if(this.m_completed) class=class="str">"cmt">// this.ReplayFrames(class="num">1); break; } this.m_w=w; this.m_h=h; } if(!this.m_completed) class="kw">return; if(id>CHARTEVENT_CUSTOM) { if(sparam==this.m_chart_stat_0.ButtonReplay().Name() && this.m_completed) { this.m_chart_stat_0.ButtonReplay().Hide(); this.ChartOptDraw(class="num">0, this.m_completed, true); } }
回放锁与多标签切换的事件处理
在 MT5 自定义面板的回放逻辑里,先置 m_completed 为 false 是为了防止同一帧被连续重跑多次,回放结束再解锁为 true。这个布尔锁直接影响 ReplayFrames(delay_ms) 能否安全被反复调用,delay_ms 若设成 1 则几乎实时刷图,设成较大值则肉眼可追帧。 回放完成后必须主动把 Replay 按钮重新 Show() 并调 ChartRedraw(),否则界面会停在最后一帧不刷新。下面是这段控制流的核心代码片段: 面板用 GetTabButton(0)~(4) 一口气抓 5 个标签指针,任一为 NULL 就直接 return,说明标签结构一旦缺一个整个事件分发就废了。随后用 GetSelectedTabID() 拿当前选中页,再用 sparam 比对各 tab 的 Name() 来决定走哪个分支。 切到 tab 0 时只画空结果表并 ChartRedraw(),注释里那段 ReplayFrames(1) 是被刻意注释掉的——意味着默认不在切页时自动重放,想追优化过程得自己手动点按钮。tab 1 的切换则转交 OnTabSwitchEvent(1) 处理,把绘制与数据装载从主事件里解耦。
this.m_completed=false; class=class="str">"cmt">// block it so as not to run it several times in a row this.ReplayFrames(delay_ms); class=class="str">"cmt">// replay procedure this.m_completed=true; class=class="str">"cmt">// unlock class=class="str">"cmt">//--- After replay is complete, show Replay button and redraw chart this.m_chart_stat_0.ButtonReplay().Show(); ::ChartRedraw(); } class=class="str">"cmt">//--- Get pointers to tab buttons CTabButton *tab_btn0=this.m_tab_control.GetTabButton(class="num">0); CTabButton *tab_btn1=this.m_tab_control.GetTabButton(class="num">1); CTabButton *tab_btn2=this.m_tab_control.GetTabButton(class="num">2); CTabButton *tab_btn3=this.m_tab_control.GetTabButton(class="num">3); CTabButton *tab_btn4=this.m_tab_control.GetTabButton(class="num">4); if(tab_btn0==NULL || tab_btn1==NULL || tab_btn2==NULL || tab_btn3==NULL || tab_btn4==NULL) class="kw">return; class=class="str">"cmt">//--- Get ID of selected tab class="type">int tab_selected=this.m_tab_control.GetSelectedTabID(); class=class="str">"cmt">//--- If class="kw">switch event has arrived to tab class="num">0 if(sparam==tab_btn0.Name()) { class=class="str">"cmt">//--- On tab class="num">0, draw chart with last pass line and two tables with empty results this.DrawDataChart(class="num">0); class=class="str">"cmt">//--- It starts replay of performed optimization class=class="str">"cmt">//--- (it can take a class="type">long time - if desired, you can click Replay button to print charts) class=class="str">"cmt">//if(this.m_completed) class=class="str">"cmt">// this.ReplayFrames(class="num">1); ::ChartRedraw(); class="kw">return; } class=class="str">"cmt">//--- Get pointer to chart of selected tab CStatChart *chart_stat=this.GetChartStats(tab_selected); if(tab_selected==class="num">0 || chart_stat==NULL) class="kw">return; class=class="str">"cmt">//--- Get pointers to chart buttons of selected tab(tab index class="num">1-class="num">4) CButtonTriggered *button_min=chart_stat.ButtonResultMin(); CButtonTriggered *button_mid=chart_stat.ButtonResultMid(); CButtonTriggered *button_max=chart_stat.ButtonResultMax(); if(button_min==NULL || button_mid==NULL || button_max==NULL) class="kw">return; class=class="str">"cmt">//--- If class="kw">switch event has arrived to tab class="num">1 if(sparam==tab_btn1.Name()) { class=class="str">"cmt">//--- call handler for switching to tab this.OnTabSwitchEvent(class="num">1); } class=class="str">"cmt">//--- If class="kw">switch event has arrived to tab class="num">2 if(sparam==tab_btn2.Name()) {
◍ 标签页与结果按钮的事件分发逻辑
在 MT5 自定义面板里,标签切换和结果档位按钮都走同一个事件入口,靠 sparam 比对控件 Name() 来路由。下面这段分发代码把 4 个标签页和每页的 min/mid/max 三个结果按钮分别映射到 OnTabSwitchEvent 与 OnButtonSwitchEvent,参数就是页号和档位索引(0/1/2)。
片段:标签页与按钮事件分发 class=class="str">"cmt">//--- 若事件来自标签3 if(sparam==tab_btn3.Name()) this.OnTabSwitchEvent(class="num">3); class=class="str">"cmt">//--- 若事件来自最小结果按钮 if(sparam==button_min.Name()) this.OnButtonSwitchEvent(tab_selected, class="num">0);
class=class="str">"cmt">//--- If class="kw">switch event has arrived to tab class="num">3 if(sparam==tab_btn3.Name()) { class=class="str">"cmt">//--- call handler for switching to tab this.OnTabSwitchEvent(class="num">3); } class=class="str">"cmt">//--- If class="kw">switch event has arrived to tab class="num">4 if(sparam==tab_btn4.Name()) { class=class="str">"cmt">//--- call handler for switching to tab this.OnTabSwitchEvent(class="num">4); } class=class="str">"cmt">//--- If event has arrived click on minimum result button of selected tab if(sparam==button_min.Name()) { class=class="str">"cmt">//--- call handler for toggle button switching this.OnButtonSwitchEvent(tab_selected, class="num">0); } class=class="str">"cmt">//--- If event has arrived click on mid result button of selected tab if(sparam==button_mid.Name()) { class=class="str">"cmt">//--- call handler for toggle button switching this.OnButtonSwitchEvent(tab_selected, class="num">1); } class=class="str">"cmt">//--- If event has arrived click on max result button of selected tab if(sparam==button_max.Name()) { class=class="str">"cmt">//--- call handler for toggle button switching this.OnButtonSwitchEvent(tab_selected, class="num">2); } class="type">void CFrameViewer::OnTabSwitchEvent(const class="type">int tab_id) { class=class="str">"cmt">//--- Get pointer to chart of selected tab CStatChart *chart_stat=this.GetChartStats(tab_id); if(chart_stat==NULL) class="kw">return; class=class="str">"cmt">//--- Get pointer to chart toggle button of selected tab CButtonSwitch *button_switch=chart_stat.ButtonResult(); if(button_switch==NULL) class="kw">return; class=class="str">"cmt">//--- Index of pressed button class="type">uint butt_index=button_switch.SelectedButton(); class=class="str">"cmt">//--- Initialize chart of results on tab_id and this.DrawDataChart(tab_id); class=class="str">"cmt">//--- call method that controls display of control elements on all tabs this.ControlObjectsView(tab_id); class=class="str">"cmt">//--- Draw all three best passes this.DrawBestFrameData(tab_id, -class="num">1); class=class="str">"cmt">//--- Highlight pass selected by button this.DrawBestFrameData(tab_id, butt_index); }
「切换标签时重绘优化结果视图」
在 MT5 优化结果查看器的内部实现里,用户点开某个结果标签(tab_id)会触发 OnButtonSwitchEvent,此时先重绘该标签的数据图表,再把全部三次最优遍历画出来,最后用 butt_id 指定的按钮高亮其中一条遍历。这种「先全画、再单高亮」的顺序能避免图表层叠错位。 DrawDataChart 负责把统计表、参数表和优化曲线一起铺到界面上:统计表从左上角 (4,4) 起、单元格宽 CELL_W*2、高 CELL_H;参数表紧接统计表下边界加 4 像素偏移;优化图则根据 m_completed 标记决定是否带完成态渲染。 ControlObjectsView 只处理当前激活标签(GetSelectedTabID 拿到索引),用 table_stat.X2()+10 和 tab 右边界减 10 算出图头区间,再按 tab_index 分支摆控件。以 case 0 优化标签为例,Replay 按钮被移到图头水平中线 (x1+x2)/2 处,且仅当 m_completed 为真才 Show 并 BringToTop,其余标签的按钮直接隐藏。外汇与贵金属 EA 回测优化界面改动后,建议直接在 MT5 附带的 FrameViewer 类里断点验证这套坐标逻辑。
class="type">void CFrameViewer::OnButtonSwitchEvent(const class="type">int tab_id, const class="type">uint butt_id) { class=class="str">"cmt">//--- Initialize chart of results on tab_id this.DrawDataChart(tab_id); class=class="str">"cmt">//--- Draw all three best passes this.DrawBestFrameData(tab_id, -class="num">1); class=class="str">"cmt">//--- Highlight pass selected by butt_id button this.DrawBestFrameData(tab_id, butt_id); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Draws data tables and optimization chart | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CFrameViewer::DrawDataChart(const class="type">uint tab_id) { class=class="str">"cmt">//--- Draw table of statistics, table of input parameters, and optimization chart this.TableStatDraw(tab_id, class="num">4, class="num">4, CELL_W*class="num">2, CELL_H, false); this.TableInpDraw(tab_id, class="num">4, this.GetTableStats(tab_id).Y2()+class="num">4, CELL_W*class="num">2, CELL_H, this.GetTableInputs(tab_id).RowsTotal(), false); this.ChartOptDraw(tab_id, this.m_completed, true); class=class="str">"cmt">//--- call method that controls display of control elements on all tabs this.ControlObjectsView(tab_id); } class=class="str">"cmt">//+-------------------------------------------------------------------+ class=class="str">"cmt">//|Controls view of control objects on optimization charts | class=class="str">"cmt">//+-------------------------------------------------------------------+ class="type">void CFrameViewer::ControlObjectsView(const class="type">uint tab_id) { class=class="str">"cmt">//--- Get index of active tab class="type">int tab_index=this.m_tab_control.GetSelectedTabID(); class=class="str">"cmt">//--- Get pointer to active tab and optimization statistics table CTab *tab=this.m_tab_control.GetTab(tab_index); CTableDataControl *table_stat=this.GetTableStats(tab_index); if(tab==NULL || table_stat==NULL) class="kw">return; class=class="str">"cmt">//--- Coordinates of left and right boundaries of header of optimization results chart class="type">int w=class="num">0, cpx=class="num">0, x=class="num">0, y=class="num">0; class="type">int x1=table_stat.X2()+class="num">10; class="type">int x2=tab.GetField().Right()-class="num">10; class=class="str">"cmt">//--- Depending on selected tab index class="kw">switch(tab_index) { class=class="str">"cmt">//--- Optimization case class="num">0 : class=class="str">"cmt">//--- Shift Replay button to center of header w=this.m_chart_stat_0.ButtonReplay().Width(); cpx=(x1+x2)/class="num">2; x=cpx-w/class="num">2; this.m_chart_stat_0.ButtonReplay().MoveX(x); class=class="str">"cmt">//--- If optimization is completed, show button in foreground if(this.m_completed) { this.m_chart_stat_0.ButtonReplay().Show(); this.m_chart_stat_0.ButtonReplay().BringToTop(); } class=class="str">"cmt">//--- Hide buttons of all other tabs this.m_chart_stat_1.ButtonsResultHide();
切换统计标签时的按钮重排逻辑
在 MT5 面板里切到不同绩效标签(夏普、净利、盈利因子、恢复因子)时,代码用 switch-case 把非当前标签的结果按钮全藏掉,只把对应标签的切换键提到最前。 以 case 1 为例:先隐藏重放按钮,再按进度条下沿加 CELL_H+2 像素算出新 Y 坐标,把标签1的结果键移过去。CELL_H 通常是单元格行高,若你改了面板字号,这个值不跟着调就会错位。 每个 case 都重复「BringToTop 自己、Hide 其他三个」的动作,说明对象数组 m_chart_stat_1~4 与标签索引一一对应。外汇与贵金属图表加载这类面板属高风险操作环境,参数误改可能导致界面重绘异常,建议在策略测试器里先验证。 直接看这段就能复现定位逻辑:
this.m_chart_stat_2.ButtonsResultHide(); this.m_chart_stat_3.ButtonsResultHide(); this.m_chart_stat_4.ButtonsResultHide(); break; class=class="str">"cmt">//--- Sharpe Ratio case class="num">1 : class=class="str">"cmt">//--- Hide Replay button this.m_chart_stat_0.ButtonReplay().Hide(); class=class="str">"cmt">//--- Get Y coordinate and move toggle button to it y=this.m_chart_stat_1.ProgressBarY1()+CELL_H+class="num">2; this.m_chart_stat_1.ButtonResult().MoveY(y); class=class="str">"cmt">//--- Move toggle button on tab class="num">1 to foreground, class=class="str">"cmt">//--- and hide all other buttons on other tabs this.m_chart_stat_1.ButtonsResultBringToTop(); this.m_chart_stat_2.ButtonsResultHide(); this.m_chart_stat_3.ButtonsResultHide(); this.m_chart_stat_4.ButtonsResultHide(); break; class=class="str">"cmt">//--- Net Profit case class="num">2 : this.m_chart_stat_0.ButtonReplay().Hide(); class=class="str">"cmt">//--- Get Y coordinate and move toggle button to it y=this.m_chart_stat_2.ProgressBarY1()+CELL_H+class="num">2; this.m_chart_stat_2.ButtonResult().MoveY(y); class=class="str">"cmt">//--- Move toggle button on tab class="num">2 to foreground, class=class="str">"cmt">//--- and hide all other buttons on other tabs this.m_chart_stat_2.ButtonsResultBringToTop(); this.m_chart_stat_1.ButtonsResultHide(); this.m_chart_stat_3.ButtonsResultHide(); this.m_chart_stat_4.ButtonsResultHide(); break; class=class="str">"cmt">//--- Profit Factor case class="num">3 : this.m_chart_stat_0.ButtonReplay().Hide(); class=class="str">"cmt">//--- Get Y coordinate and move toggle button to it y=this.m_chart_stat_3.ProgressBarY1()+CELL_H+class="num">2; this.m_chart_stat_3.ButtonResult().MoveY(y); class=class="str">"cmt">//--- Move toggle button on tab class="num">3 to foreground, class=class="str">"cmt">//--- and hide all other buttons on other tabs this.m_chart_stat_3.ButtonsResultBringToTop(); this.m_chart_stat_1.ButtonsResultHide(); this.m_chart_stat_2.ButtonsResultHide(); this.m_chart_stat_4.ButtonsResultHide(); break; class=class="str">"cmt">//--- Recovery Factor case class="num">4 : this.m_chart_stat_0.ButtonReplay().Hide();
◍ 回放开局先把按钮层级理清
在标签切换的 break 分支里,先把第 4 页结果按钮的 Y 坐标算出来:取进度条底部坐标 m_chart_stat_4.ProgressBarY1(),再叠一个格子高度 CELL_H 加 2 像素偏移,然后 MoveY 落位。这样按钮不会和第 4 页的进度条控件重叠。 随后调用 ButtonsResultBringToTop() 把第 4 页按钮提到最前,并对前三个标签页的按钮统一执行 ButtonsResultHide(),避免多 tab 按钮在图表上互相遮挡。外汇与贵金属图表多控件叠加时易误触,这类显隐管理能降低手动操作风险。 优化跑完后的回放由 ReplayFrames(delay_ms) 负责:先 FrameFirst() 把帧指针归零,用 FrameNext 逐帧取 pass/name/id/value/data,每帧 frame_counter 自增并打印“Playing with pause %d ms: frame %d”。delay_ms 直接喂给 Sleep(),所以设 200 时每帧至少停 200 毫秒,可在 MT5 策略测试器里改这个值控制回放节奏。 DrawFrameDataByPass() 则按指定 pass_num 搜帧:同样 FrameFirst() 起手,循环 FrameNext 直到命中目标 pass,再把该帧数据往图表上画。想验证某次优化_pass 的可视化结果,直接传 pass_num 调它即可。
class=class="str">"cmt">//--- Get Y coordinate and move toggle button to it y=this.m_chart_stat_4.ProgressBarY1()+CELL_H+class="num">2; this.m_chart_stat_4.ButtonResult().MoveY(y); class=class="str">"cmt">//--- Move toggle button on tab class="num">4 to foreground, class=class="str">"cmt">//--- and hide all other buttons on other tabs this.m_chart_stat_4.ButtonsResultBringToTop(); this.m_chart_stat_1.ButtonsResultHide(); this.m_chart_stat_2.ButtonsResultHide(); this.m_chart_stat_3.ButtonsResultHide(); break; class="kw">default: break; } class=class="str">"cmt">//--- Redraw chart ::ChartRedraw(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Replaying frames after optimization completion | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CFrameViewer::ReplayFrames(const class="type">int delay_ms) { class=class="str">"cmt">//--- Variables to work with frames class="type">class="kw">string name; class="type">ulong pass; class="type">long id; class="type">class="kw">double value, data[]; class="type">class="kw">string params[]; class="type">uint par_count; class=class="str">"cmt">//--- Frame counter class="type">int frame_counter=class="num">0; class=class="str">"cmt">//--- Reset progress bar counters this.m_progress_bar.Reset(); this.m_progress_bar.Update(false); class=class="str">"cmt">//--- Move frame pointer to the beginning and start iterating through frames ::FrameFirst(); while(!::IsStopped() && ::FrameNext(pass, name, id, value, data)) { class=class="str">"cmt">//--- Increase frame counter and prepare header text of optimization chart frame_counter++; class="type">class="kw">string text=::StringFormat("Playing with pause %d ms: frame %d", delay_ms, frame_counter); class=class="str">"cmt">//--- Get input parameters of Expert Advisor, for which frame is formed, frame data, and print them on chart if(this.DrawFrameData(class="num">0, text, clrNONE, class="num">0, pass, params, par_count, data)) ::ChartRedraw(); class=class="str">"cmt">//--- Wait for delay_ms milliseconds ::Sleep(delay_ms); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Prints data of specified frame on optimization chart | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CFrameViewer::DrawFrameDataByPass(const class="type">uint tab_id, const class="type">ulong pass_num, const class="type">class="kw">string text, class="type">color clr, const class="type">uint line_width, class="type">class="kw">double &data[]) { class=class="str">"cmt">//--- Variables to work with frames class="type">class="kw">string name; class="type">ulong pass; class="type">long id; class="type">uint par_count; class="type">class="kw">double value; class="type">class="kw">string params[]; class=class="str">"cmt">//--- Move frame pointer to the beginning and start search of pass_num frame ::FrameFirst(); while(::FrameNext(pass, name, id, value, data))
「把优化前三名画到图表上」
CFrameViewer 的 DrawBestFrameData 方法负责把某一项优化标准下的前三名回测结果直接绘制到 MT5 图表。调用时传入 tab_id(1~4)和 res_index(-1~2),tab_id 超出 1~4 或 res_index 大于 2 会直接 PrintFormat 报错并 return,不会动图表。 tab_id 与标题的对应写死在三元表达式里:1=Sharpe Ratio、2=Net Profit、3=Profit Factor、4=Recovery Factor。也就是说,你点哪个分页标签,就决定图表头部显示哪种标准下的「Optimization Completed」字样。 FillArrayBestFrames 先 ZeroMemory 清空传入的 ulong array_passes[3],再填回三个最优 pass 的索引。若 res_index 为负数,循环把三趟 pass 全用 clrNONE 画出来(盈利/亏损序列自动配色);若 res_index 是 0/1/2,则只画对应那一条,颜色取 m_selected_color、线宽取 m_line_width。 实盘前先在策略测试器跑完优化,再切到对应 tab 点按钮验证:若三线重叠难以分辨,把 m_line_width 从 0 调成 1~2 能立刻看清回撤段差异。外汇与贵金属品种波动剧烈,优化结果仅代表历史样本,实盘复制可能显著偏离。
{
class=class="str">"cmt">//--- If passe number matches the desired one -
class=class="str">"cmt">//--- get frame data and print it in table
class=class="str">"cmt">//--- and on chart on tab tab_id
if(pass==pass_num)
{
if(DrawFrameData(tab_id, text, clr, line_width, pass, params, par_count, data))
class="kw">return true;
}
}
class=class="str">"cmt">//--- Pass not found
class="kw">return false;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Draws charts of top three passes by optimization criterion |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CFrameViewer::DrawBestFrameData(const class="type">uint tab_id, const class="type">int res_index)
{
class=class="str">"cmt">//--- If incorrect identifiers of table and pressed button are passed, exit
if(tab_id<class="num">1 || tab_id>class="num">4 || res_index>class="num">2)
{
::PrintFormat("%s: Error. Incorrect table(%u) or selected button(%d) identifiers passed",__FUNCTION__, tab_id, res_index);
class="kw">return;
}
class=class="str">"cmt">//--- Arrays for getting results of passes
class="type">ulong array_passes[class="num">3];
class="type">class="kw">double data[];
class=class="str">"cmt">//--- Create header text of pass chart
class="type">class="kw">string res=
(
tab_id==class="num">1 ? "Results by Sharpe Ratio" :
tab_id==class="num">2 ? "Results by Net Profit" :
tab_id==class="num">3 ? "Results by Profit Factor" :
tab_id==class="num">4 ? "Results by Recovery Factor" :
""
);
class="type">class="kw">string text="Optimization Completed: "+res;
class=class="str">"cmt">//--- Fill in array_passes array by indexes of three best passes
this.FillArrayBestFrames(tab_id, array_passes);
class=class="str">"cmt">//--- If index of pass button is set to negative number -
if(res_index<class="num">0)
{
class=class="str">"cmt">//--- print all three passes on chart
class=class="str">"cmt">//--- (line class="type">color is specified as clrNONE for automatic line class="type">color selection of profitable or loss series)
for(class="type">int i=class="num">0; i<(class="type">int)array_passes.Size(); i++)
this.DrawFrameDataByPass(tab_id, array_passes[i], text, clrNONE, class="num">0, data);
}
class=class="str">"cmt">//--- Otherwise, print series indicated by index of pressed button on chart(res_index),
class=class="str">"cmt">//--- by class="type">color set in m_selected_color, and width specified in m_line_width
else
this.DrawFrameDataByPass(tab_id, array_passes[res_index], text, this.m_selected_color, this.m_line_width, data);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Fills array with frame indexes of top three passes |
class=class="str">"cmt">//| for specified optimization criterion(by tab index) |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CFrameViewer::FillArrayBestFrames(const class="type">uint tab_id, class="type">ulong &array_passes[])
{
class=class="str">"cmt">//--- Clear array of optimization pass indexes passed to method
::ZeroMemory(array_passes);按优化指标挑出前三轮回测
在 MT5 优化框架里,想从一堆回测结果中自动捞出表现最好的几轮,核心是先按某个指标排序再取尾部。下面这段逻辑支持用 tab_id 在 1~4 之间指定指标类型:1 对应夏普比率、2 对应净利润、3 对应盈利因子、4 对应恢复因子,超出范围直接报错退出。 代码先校验 tab_id 合法性,再把它强转成 ENUM_FRAME_PROP 枚举,调用 Sort 按该属性升序排列帧列表。排完序后,最大值的那一轮会落在列表末尾,用 Total()-1 取下标就能拿到最优帧,把它的 Pass 号写进 array_passes[2]。 剩下两轮靠 FrameSearchLess 从最优帧往前找「属性值更小」的相邻帧,倒序填进 array_passes[1] 和 [0];若搜不到更小值的帧,就复用后一位的 Pass 号。这样 array_passes 三个槽位就存了前三好的优化批次,方便后续做参数对比。 外汇与贵金属品种做这类优化回测波动剧烈,历史最优不代表未来实盘概率占优,切换品种前建议先在小周期样本验证排序稳定性。
class=class="str">"cmt">//FRAME_PROP_PASS_NUM, // Pass number class=class="str">"cmt">//FRAME_PROP_SHARPE_RATIO, // Sharpe Ratio result class=class="str">"cmt">//FRAME_PROP_NET_PROFIT, // Net Profit result class=class="str">"cmt">//FRAME_PROP_PROFIT_FACTOR, // Profit Factor result class=class="str">"cmt">//FRAME_PROP_RECOVERY_FACTOR, // Recovery Factor result class=class="str">"cmt">//--- By using tab ID, determine class="kw">property by which best optimization passes will be searched class=class="str">"cmt">//--- Check tab ID to be within class="num">1 to class="num">4 if(tab_id<FRAME_PROP_SHARPE_RATIO || tab_id>FRAME_PROP_RECOVERY_FACTOR) { ::PrintFormat("%s: Error: Invalid tab ID passed(%u)",__FUNCTION__, tab_id); class="kw">return false; } class=class="str">"cmt">//--- Convert table ID to frame class="kw">property ENUM_FRAME_PROP prop=(ENUM_FRAME_PROP)tab_id; class=class="str">"cmt">//--- Sort frame list in ascending order by class="kw">property, class=class="str">"cmt">//--- which corresponds to tab_id value as ENUM_FRAME_PROP this.m_list_frames.Sort(prop); class=class="str">"cmt">//--- After sorting, frame with best result will be at list end class=class="str">"cmt">//--- Using index, get frame from list with maximum result value and class="type">int index=this.m_list_frames.Total()-class="num">1; CFrameData *frame_next=this.m_list_frames.At(index); if(frame_next==NULL) class="kw">return false; class=class="str">"cmt">//--- register pass number to last cell of array_passes array_passes[class="num">2]=frame_next.Pass(); class=class="str">"cmt">//--- Now find objects for which optimization result in descending order is less than maximum found class=class="str">"cmt">//--- In loop from class="num">1 to class="num">0 (remaining cells of array_passes) for(class="type">int i=class="num">1; i>=class="num">0; i--) { class=class="str">"cmt">//--- look for previous object with class="kw">property value less than that of frame_next object frame_next=this.FrameSearchLess(frame_next, prop); class=class="str">"cmt">//--- In next cell of array_passes, enter pass number of found object class=class="str">"cmt">//--- If object is not found, it means that there are no objects with value less than that of frame_next object, class=class="str">"cmt">//--- and in this case, enter its previous value to next cell of array_passes array_passes[i]=(frame_next!=NULL ? frame_next.Pass() : array_passes[i+class="num">1]); } class=class="str">"cmt">//--- Success class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Searches for and returns pointer to frame object, | class=class="str">"cmt">//| with class="kw">property value less than sample | class=class="str">"cmt">//+------------------------------------------------------------------+ CFrameData *CFrameViewer::FrameSearchLess(CFrameData *frame, const class="type">int mode) { class=class="str">"cmt">//--- Depending on type of frame class="kw">property class="kw">switch(mode) { class=class="str">"cmt">//--- to temporary object record corresponding class="kw">property of object passed to method case FRAME_PROP_SHARPE_RATIO : this.m_frame_tmp.SetSharpeRatio(frame.SharpeRatio()); break; case FRAME_PROP_NET_PROFIT : this.m_frame_tmp.SetNetProfit(frame.NetProfit()); break; case FRAME_PROP_PROFIT_FACTOR : this.m_frame_tmp.SetProfitFactor(frame.ProfitFactor()); break;
◍ 回测结果里捞最优帧的底层取法
这段逻辑干的事很直接:先把帧数组按指定属性排序,再用 SearchLess 找属性值刚好低于临时帧的那一帧,拿不到就返回 NULL。RecoveryFactor 这类属性在 switch 里被单独赋值,其余情况走 default 把 Pass 存进临时帧,说明排序键是可切换的。 DrawBestFrameDataAll 从标签 1 开始循环到 TabsTotal()-1,对每个标签调 DrawBestFrameData(i,-1),也就是每个优化结果页都画前三名 pass。想验证的话,开 MT5 跑完一轮 EA 优化,看右侧多标签图是否每页都带三条最优曲线。 DrawFrameData 一进来就卡 tab_id>4 直接 false 并 PrintFormat 报错,意味着自定义面板只支持 0~4 号标签。它内部取前景画布、两张数据表和一个统计图的指针,任一为 NULL 就退出,随后用 FrameInputs(pass,...) 拉该 pass 的 EA 输入参数,再画参数表——表起点 y 坐标是 table_stat.Y2()+4,单元格宽高走 CELL_W*2 与 CELL_H 常量。外汇与贵金属优化结果受点差滑点影响大,回测靠前帧实盘可能失效,属高风险验证。
case FRAME_PROP_RECOVERY_FACTOR : this.m_frame_tmp.SetRecoveryFactor(frame.RecoveryFactor()); break; class="kw">default : this.m_frame_tmp.SetPass(frame.Pass()); break; } class=class="str">"cmt">//--- Sort array of frames by specified class="kw">property and this.m_list_frames.Sort(mode); class=class="str">"cmt">//--- get index of nearest object with lower class="kw">property value, or -class="num">1 class="type">int index=this.m_list_frames.SearchLess(&this.m_frame_tmp); class=class="str">"cmt">//--- Get object by index from list and class="kw">return pointer to it, or NULL CFrameData *obj=this.m_list_frames.At(index); class="kw">return obj; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Prints on optimization results charts | class=class="str">"cmt">//| on each tab three best passes | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CFrameViewer::DrawBestFrameDataAll(class="type">void) { class=class="str">"cmt">//--- In a loop through all tabs from tab class="num">1, draw charts of top three passes for each tab for(class="type">int i=class="num">1; i<this.m_tab_control.TabsTotal(); i++) this.DrawBestFrameData(i,-class="num">1); } class=class="str">"cmt">//+-------------------------------------------------------------------+ class=class="str">"cmt">//| Retrieving data of current frame and printing it on specified tab | class=class="str">"cmt">//| in table and on optimization results chart | class=class="str">"cmt">//+-------------------------------------------------------------------+ class="type">bool CFrameViewer::DrawFrameData(const class="type">uint tab_id, const class="type">class="kw">string text, class="type">color clr, const class="type">uint line_width, class="type">ulong &pass, class="type">class="kw">string ¶ms[], class="type">uint &par_count, class="type">class="kw">double &data[]) { class=class="str">"cmt">//--- Check passed tab ID if(tab_id>class="num">4) { ::PrintFormat("%s: Error: Invalid tab ID passed(%u)",__FUNCTION__, tab_id); class="kw">return false; } class=class="str">"cmt">//--- Get pointers to objects used on specified tab CCanvas *foreground=this.m_tab_control.GetTabForeground(tab_id); CTableDataControl *table_stat=this.GetTableStats(tab_id); CTableDataControl *table_inp=this.GetTableInputs(tab_id); CStatChart *chart_stat=this.GetChartStats(tab_id); if(foreground==NULL || table_stat==NULL || table_inp==NULL || chart_stat==NULL) class="kw">return false; class=class="str">"cmt">//--- Get input parameters of Expert Advisor, for which frame is formed, frame data, and print them on chart ::ResetLastError(); if(::FrameInputs(pass, params, par_count)) { class=class="str">"cmt">//--- Draw table of input parameters on chart this.TableInpDraw(tab_id, class="num">4, table_stat.Y2()+class="num">4, CELL_W*class="num">2, CELL_H, par_count, false); class=class="str">"cmt">//--- Iterate through parameters, params[i], class="type">class="kw">string looks as "parameter=value"
「把输入参数刷进优化结果表」
在 MT5 自绘优化面板的逻辑里,输入参数表需要逐行填充名称和数值,同时用底色区分可优化与不可优化项。下面这段循环就是干这个的:先按等号切分参数字符串,再查该参数是否开放了优化区间。
for(class="type">uint i=class="num">0; i<par_count; i++) { class=class="str">"cmt">//--- Fill in table with names and values of input parameters class="type">class="kw">string array[]; class=class="str">"cmt">//--- Split class="type">class="kw">string in params[i] into two substrings and update cells in class="type">class="kw">string of test parameters table if(::StringSplit(params[i],&class="macro">#x27;=&class="macro">#x27;,array)==class="num">2) { class=class="str">"cmt">//--- Fill in strings of optimized parameters with pale yellow class="type">color, class=class="str">"cmt">//--- parameters that are not available for optimization - to pale pink, the rest - to class="kw">default colors class="type">bool enable=false; class="type">class="kw">double value=class="num">0, start=class="num">0, step=class="num">0, stop=class="num">0; class="type">color clr=clrMistyRose; if(::ParameterGetRange(array[class="num">0], enable, value, start, step, stop)) clr=(enable ? clrLightYellow : clrNONE); class=class="str">"cmt">//--- Get two cells of table by parameter index and print text of parameter name and its value to them CTableCell *cell_0=table_inp.GetCell(i, class="num">0); CTableCell *cell_1=table_inp.GetCell(i, class="num">1); if(cell_0!=NULL && cell_1!=NULL) { class=class="str">"cmt">//--- Update captions in cells cell_0.SetText(array[class="num">0]); cell_1.SetText(array[class="num">1]); cell_0.TextOut(foreground, class="num">4, CELL_H/class="num">2, clr, class="num">0, TA_VCENTER); cell_1.TextOut(foreground, class="num">4, CELL_H/class="num">2, clr, class="num">0, TA_VCENTER); } } } class=class="str">"cmt">//--- Update optimization statistics table class=class="str">"cmt">//--- Table header class="type">class="kw">string foreground.FillRectangle(table_stat.X1()+class="num">1, class="num">4+class="num">1, table_stat.X1()+CELL_W*class="num">2-class="num">1, class="num">4+CELL_H-class="num">1, 0x00FFFFFF); foreground.FontSet("Calibri", -class="num">100, FW_BLACK); foreground.TextOut(class="num">4+(CELL_W*class="num">2)/class="num">2, class="num">4+CELL_H/class="num">2, ::StringFormat("Optimization results(pass %I64u)", pass), ::ColorToARGB(clrMidnightBlue), TA_CENTER|TA_VCENTER); class=class="str">"cmt">//--- In loop by number of strings in table class="type">int total=table_stat.RowsTotal(); for(class="type">int i=class="num">0; i<total; i++) {
for(class="type">uint i=class="num">0; i<par_count; i++) { class=class="str">"cmt">//--- Fill in table with names and values of input parameters class="type">class="kw">string array[]; class=class="str">"cmt">//--- Split class="type">class="kw">string in params[i] into two substrings and update cells in class="type">class="kw">string of test parameters table if(::StringSplit(params[i],&class="macro">#x27;=&class="macro">#x27;,array)==class="num">2) { class=class="str">"cmt">//--- Fill in strings of optimized parameters with pale yellow class="type">color, class=class="str">"cmt">//--- parameters that are not available for optimization - to pale pink, the rest - to class="kw">default colors class="type">bool enable=false; class="type">class="kw">double value=class="num">0, start=class="num">0, step=class="num">0, stop=class="num">0; class="type">color clr=clrMistyRose; if(::ParameterGetRange(array[class="num">0], enable, value, start, step, stop)) clr=(enable ? clrLightYellow : clrNONE); class=class="str">"cmt">//--- Get two cells of table by parameter index and print text of parameter name and its value to them CTableCell *cell_0=table_inp.GetCell(i, class="num">0); CTableCell *cell_1=table_inp.GetCell(i, class="num">1); if(cell_0!=NULL && cell_1!=NULL) { class=class="str">"cmt">//--- Update captions in cells cell_0.SetText(array[class="num">0]); cell_1.SetText(array[class="num">1]); cell_0.TextOut(foreground, class="num">4, CELL_H/class="num">2, clr, class="num">0, TA_VCENTER); cell_1.TextOut(foreground, class="num">4, CELL_H/class="num">2, clr, class="num">0, TA_VCENTER); } } } class=class="str">"cmt">//--- Update optimization statistics table class=class="str">"cmt">//--- Table header class="type">class="kw">string foreground.FillRectangle(table_stat.X1()+class="num">1, class="num">4+class="num">1, table_stat.X1()+CELL_W*class="num">2-class="num">1, class="num">4+CELL_H-class="num">1, 0x00FFFFFF); foreground.FontSet("Calibri", -class="num">100, FW_BLACK); foreground.TextOut(class="num">4+(CELL_W*class="num">2)/class="num">2, class="num">4+CELL_H/class="num">2, ::StringFormat("Optimization results(pass %I64u)", pass), ::ColorToARGB(clrMidnightBlue), TA_CENTER|TA_VCENTER); class=class="str">"cmt">//--- In loop by number of strings in table class="type">int total=table_stat.RowsTotal(); for(class="type">int i=class="num">0; i<total; i++) {
回测面板里逐行填回测指标
在 MT5 的自定义统计面板里,每一行对应一个回测核心指标,需要把后台算好的 data[] 数组按行写进表格第二列。下面这段逻辑就是遍历行号、用 StringFormat 按精度格式化后塞进 cell_1,同时顺手做高亮。 Sharpe Ratio、Net Profit、Profit Factor、Recovery Factor 都保留两位小数,Trades 和 Deals 直接取整,Equity DD 带百分号,OnTester() 返回值用 %G 科学计数。若某一行指针为空就跳过,避免 EA 在初始化阶段崩。 高亮规则很直白:只有当 tab_id>0 且当前行号等于 tab_id-1 时才染成浅绿 C'223,242,231',其余保持 clrNONE。这样你在多品种切换时,能一眼看出当前标签对应哪行数据。外汇与贵金属回测受点差跳空影响大,这类面板数值仅反映历史样本,实盘表现可能有明显偏差。
class=class="str">"cmt">//--- get two cells of current class="type">class="kw">string and CTableCell *cell_0=table_stat.GetCell(i, class="num">0); CTableCell *cell_1=table_stat.GetCell(i, class="num">1); if(cell_0!=NULL && cell_1!=NULL) { class=class="str">"cmt">//--- update values of pass results in second cell class="type">class="kw">string text="---"; class="kw">switch(i) { case class="num">0 : text=::StringFormat("%.2f", data[class="num">0]); break; class=class="str">"cmt">// Sharpe Ratio case class="num">1 : text=::StringFormat("%.2f", data[class="num">1]); break; class=class="str">"cmt">// Net Profit case class="num">2 : text=::StringFormat("%.2f", data[class="num">2]); break; class=class="str">"cmt">// Profit Factor case class="num">3 : text=::StringFormat("%.2f", data[class="num">3]); break; class=class="str">"cmt">// Recovery Factor case class="num">4 : text=::StringFormat("%.0f", data[class="num">4]); break; class=class="str">"cmt">// Trades case class="num">5 : text=::StringFormat("%.0f", data[class="num">5]); break; class=class="str">"cmt">// Deals case class="num">6 : text=::StringFormat("%.2f%%", data[class="num">6]); break; class=class="str">"cmt">// Equity DD case class="num">7 : text=::StringFormat("%G", data[class="num">7]); break; class=class="str">"cmt">// OnTester() class="kw">default: break; } class=class="str">"cmt">//--- Highlight background of table class="type">class="kw">string corresponding to selected tab with class="type">color. class=class="str">"cmt">//--- Remaining strings will have class="kw">default class="type">color class="type">color clr=(tab_id>class="num">0 ? (i==tab_id-class="num">1 ? C&class="macro">#x27;class="num">223,class="num">242,class="num">231&class="macro">#x27; : clrNONE) : clrNONE); class=class="str">"cmt">//--- Update captions in cells cell_0.TextOut(foreground, class="num">4, CELL_H/class="num">2, clr, class="num">0, TA_VCENTER); cell_1.SetText(text); cell_1.TextOut(foreground, class="num">4, CELL_H/class="num">2, clr, class="num">0, TA_VCENTER); }
◍ 把优化结果画进自定义标签页
在 MT5 优化框架里,把每一帧的盈亏平衡数据塞进图表,靠的是一段数组拷贝加画布刷新。下面这段逻辑先把当前帧的余额序列截出来,再交给统计图表对象渲染。
class="type">class="kw">double seria[]; ArrayCopy(seria, data, class="num">0, DATA_COUNT, ArraySize(data)-DATA_COUNT); chart_stat.AddSeria(seria, data[class="num">1]>class="num">0); chart_stat.Update(clr, line_width, false); if(tab_id==class="num">0) this.m_progress_bar.AddResult(data[class="num">1]>class="num">0, false);
class="type">class="kw">double seria[]; ArrayCopy(seria, data, class="num">0, DATA_COUNT, ArraySize(data)-DATA_COUNT); chart_stat.AddSeria(seria, data[class="num">1]>class="num">0); chart_stat.Update(clr, line_width, false); if(tab_id==class="num">0) this.m_progress_bar.AddResult(data[class="num">1]>class="num">0, false); class="type">int x1=chart_stat.HeaderX1(); class="type">int y1=chart_stat.HeaderY1(); class="type">int x2=chart_stat.HeaderX2(); class="type">int y2=chart_stat.HeaderY2(); class="type">int x=(x1+x2)/class="num">2; class="type">int y=(y1+y2)/class="num">2; foreground.FillRectangle(x1, y1, x2, y2, 0x00FFFFFF); foreground.FontSet("Calibri", -class="num">100, FW_BLACK); foreground.TextOut(x, y, text, ColorToARGB(clrMidnightBlue), TA_CENTER|TA_VCENTER); foreground.Update(false); class="kw">return true; } else PrintFormat("%s: FrameInputs() failed. Error %d",__FUNCTION__, GetLastError()); class="kw">return false; } class="type">void CFrameViewer::TableStatDraw(const class="type">uint tab_id, const class="type">int x, const class="type">int y, const class="type">int w, const class="type">int h, const class="type">bool chart_redraw) { if(tab_id>class="num">4) { PrintFormat("%s: Error: Invalid tab ID passed(%u)",__FUNCTION__, tab_id); class="kw">return; } CCanvas *background=this.m_tab_control.GetTabBackground(tab_id); CCanvas *foreground=this.m_tab_control.GetTabForeground(tab_id); CTableDataControl *table_stat=this.GetTableStats(tab_id); if(background==NULL || foreground==NULL || table_stat==NULL) class="kw">return; background.FillRectangle(x, y, x+CELL_W*class="num">2, y+CELL_H, ColorToARGB(C&class="macro">#x27;class="num">195,class="num">209,class="num">223&class="macro">#x27;));
「优化结果面板的表头与网格绘制」
在 MT5 自定义面板里把优化结果做成两张列表,第一步是先铺好标题区和空表体。下面这段代码负责在前景层画一个宽两列的标题条,并在背景层按 2 列 × DATA_COUNT 行拉出灰阶网格,单元格高 CELL_H、宽 CELL_W,边框色用 C'200,200,200'。 标题条写死为 "Optimization results",字体 Calibri、粗体 FW_BLACK、深蓝 clrMidnightBlue,居中对齐。注意 FillRectangle 的填充色是 0x00FFFFFF,即不带透明通道的纯白,盖住底层避免重绘残影。 空表体只填左侧表头、不填数值:循环里 row 从 0 到 RowsTotal()-1,col 取 0 或 1。col 为偶数时按 row 映射成 Sharpe Ratio、Net Profit、Profit Factor、Recovery Factor、Trades、Deals、Equity DD 共 7 类指标名;col 为奇数时仅当 tab_id==0 写 " --- " 占位,其余标签留空。 实跑时若你改了 DATA_COUNT 或 CELL_W/CELL_H,要同步确认 TABLE_OPT_STAT_ID+10*tab_id 的 ID 偏移不与其他面板撞车,否则多标签切换可能互相覆盖。外汇与贵金属品种优化受点差和滑点影响大,回测面板数字仅代表历史样本,实盘表现可能明显偏离。
foreground.FillRectangle(x+class="num">1, y+class="num">1, x+CELL_W*class="num">2-class="num">1, y+CELL_H-class="num">1, 0x00FFFFFF); foreground.FontSet("Calibri", -class="num">100, FW_BLACK); foreground.TextOut(x+(CELL_W*class="num">2)/class="num">2, y+CELL_H/class="num">2, "Optimization results", ::ColorToARGB(clrMidnightBlue), TA_CENTER|TA_VCENTER); class=class="str">"cmt">//--- Set table&class="macro">#x27;s identifier and draw table grid table_stat.SetID(TABLE_OPT_STAT_ID+class="num">10*tab_id); table_stat.DrawGrid(background, x, y+CELL_H, class="num">0, DATA_COUNT, class="num">2, CELL_H, CELL_W, C&class="macro">#x27;class="num">200,class="num">200,class="num">200&class="macro">#x27;, false); class=class="str">"cmt">//--- Draw empty table of optimization results - only headers, without values class=class="str">"cmt">//--- In a loop by table rows class="type">int total=table_stat.RowsTotal(); for(class="type">int row=class="num">0; row<total; row++) { class=class="str">"cmt">//--- iterate through columns of rows for(class="type">int col=class="num">0; col<class="num">2; col++) { class=class="str">"cmt">//--- Get table cell in current row and column CTableCell *cell=table_stat.GetCell(row, col); class=class="str">"cmt">//--- Define text in cell class=class="str">"cmt">//--- For left cell, these will be headers of results of parameters optimized if(col%class="num">2==class="num">0) { class="type">class="kw">string text="OnTester()"; class="kw">switch(row) { case class="num">0 : text="Sharpe Ratio"; break; case class="num">1 : text="Net Profit"; break; case class="num">2 : text="Profit Factor"; break; case class="num">3 : text="Recovery Factor"; break; case class="num">4 : text="Trades"; break; case class="num">5 : text="Deals"; break; case class="num">6 : text="Equity DD"; break; class="kw">default: break; } cell.SetText(text); } class=class="str">"cmt">//--- For right cell, text will be strikeout for table initialized else cell.SetText(tab_id==class="num">0 ? " --- " : ""); class=class="str">"cmt">//--- Print corresponding text in cell
参数表与优化图的画布落点
在 CFrameViewer 里,TableInpDraw 负责把优化输入参数画到指定标签页。tab_id 超过 4 会直接 PrintFormat 报错并返回,说明这套框架最多只预留了 5 个标签页的画布指针。 表头先用背景画布填一个 RGB(195,209,223) 的矩形,前景再叠一个 0x00FFFFFF 的透明矩形,然后用 Calibri 粗体在中间写“Input parameters”,颜色取 clrMidnightBlue。表格网格由 table_inp.DrawGrid 画出,列宽 CELL_W、行高 CELL_H,线色 C'200,200,200'。 ChartOptDraw 同样先卡 tab_id>4 的边界,再取该页的 background 与 foreground 指针。两个绘制函数末尾都调 background.Update(false) 和 foreground.Update(chart_redraw),后者决定是否重绘图表——接 MT5 调试时,把 chart_redraw 置 true 才能看到实时刷新。
class="type">void CFrameViewer::TableInpDraw(const class="type">uint tab_id, const class="type">int x, const class="type">int y, const class="type">int w, const class="type">int h, const class="type">uint rows, const class="type">bool chart_redraw) { class=class="str">"cmt">//--- Check passed tab ID if(tab_id>class="num">4) { ::PrintFormat("%s: Error: Invalid tab ID passed(%u)",__FUNCTION__, tab_id); class="kw">return; } class=class="str">"cmt">//--- Get pointers to objects used on specified tab CCanvas *background=this.m_tab_control.GetTabBackground(tab_id); CCanvas *foreground=this.m_tab_control.GetTabForeground(tab_id); CTableDataControl *table_inp=this.GetTableInputs(tab_id); if(background==NULL || foreground==NULL || table_inp==NULL) class="kw">return; class=class="str">"cmt">//--- Draw header of optimization parameters table background.FillRectangle(x, y, x+CELL_W*class="num">2, y+CELL_H, ::ColorToARGB(C&class="macro">#x27;class="num">195,class="num">209,class="num">223&class="macro">#x27;)); foreground.FillRectangle(x+class="num">1, y+class="num">1, x+CELL_W*class="num">2-class="num">1, y+CELL_H-class="num">1, 0x00FFFFFF); foreground.FontSet("Calibri", -class="num">100, FW_BLACK); foreground.TextOut(x+(CELL_W*class="num">2)/class="num">2, y+CELL_H/class="num">2, "Input parameters", ::ColorToARGB(clrMidnightBlue), TA_CENTER|TA_VCENTER); class=class="str">"cmt">//--- Set table&class="macro">#x27;s identifier and draw table grid table_inp.SetID(TABLE_OPT_INP_ID+class="num">10*tab_id); table_inp.DrawGrid(background, x, y+CELL_H, class="num">0, rows, class="num">2, CELL_H, CELL_W, C&class="macro">#x27;class="num">200,class="num">200,class="num">200&class="macro">#x27;, false); class=class="str">"cmt">//--- Update background and foreground canvas background.Update(false); foreground.Update(chart_redraw); } class="type">void CFrameViewer::ChartOptDraw(const class="type">uint tab_id, const class="type">bool opt_completed, const class="type">bool chart_redraw) { class=class="str">"cmt">//--- Check passed tab ID if(tab_id>class="num">4) { ::PrintFormat("%s: Error: Invalid tab ID passed(%u)",__FUNCTION__, tab_id); class="kw">return; } class=class="str">"cmt">//--- Get pointers to objects used on specified tab CCanvas *background=this.m_tab_control.GetTabBackground(tab_id); CCanvas *foreground=this.m_tab_control.GetTabForeground(tab_id);
◍ 优化结果图表的边界与表头绘制逻辑
在 MT5 自定义优化面板里,图表区域不是随便铺的。代码先取表格控件右边界加 10 像素作为图表左缘 x1,顶部对齐统计表 y1,右侧取到标签页右内边距减 10,底部则刨掉标签按钮高度再加 12 像素余量。 尺寸有硬下限:宽度不足 480 或高度不足 180 时直接拉到最小值。这意味着即便你把面板缩得很小,优化结果图也不会塌成不可读的一条线,最小可见面积固定为 480×180。 表头文案按 tab_id 切换——1 是 Sharpe Ratio、2 是 Net Profit、3 是 Profit Factor、4 是 Recovery Factor,其余显示 Click to Replay;优化未结束时整体染成灰蓝 C'195,209,223' 并提示 Waiting。背景用 LightGreen 标识跑完,前景透明叠加文字,字体锁 Calibri 加粗。 最后两步容易看漏:先整块擦成透明,再调 chart_stat.Update 重绘。如果跳过擦除,旧帧会叠在新帧上,MT5 里表现为残影。外汇与贵金属回测优化属高风险验证,参数空间结论仅代表历史样本,实盘可能显著偏离。
CTab *tab=this.m_tab_control.GetTab(tab_id); CTableDataControl *table_stat=this.GetTableStats(tab_id); CStatChart *chart_stat=this.GetChartStats(tab_id); if(background==NULL || foreground==NULL || tab==NULL || table_stat==NULL || chart_stat==NULL) class="kw">return; class=class="str">"cmt">//--- Calculate coordinates of four corners of optimization results chart class="type">int x1=table_stat.X2()+class="num">10; class="type">int y1=table_stat.Y1(); class="type">int x2=tab.GetField().Right()-class="num">10; class="type">int y2=tab.GetField().Bottom()-tab.GetButton().Height()-class="num">12; class=class="str">"cmt">//--- Check size limits by minimum width and height(class="num">480 x class="num">180) class="type">int w_min=class="num">480; if(x2-x1<w_min) x2=x1+w_min; if(y2-y1<class="num">180) y2=y1+class="num">180; class=class="str">"cmt">//--- Set dimensions of bounding rectangle of optimization results chart chart_stat.SetChartBounds(x1, y1, x2, y2); class=class="str">"cmt">//--- Color and text of chart header class="type">color clr=clrLightGreen; class=class="str">"cmt">// header class="type">color at optimization completion class="type">class="kw">string suff= ( tab_id==class="num">1 ? "Results by Sharpe Ratio" : tab_id==class="num">2 ? "Results by Net Profit" : tab_id==class="num">3 ? "Results by Profit Factor" : tab_id==class="num">4 ? "Results by Recovery Factor" : "Click to Replay" ); class="type">class="kw">string text="Optimization Completed: "+suff; class=class="str">"cmt">//--- If optimization is not completed, specify class="type">color and text of header if(!opt_completed) { clr=C&class="macro">#x27;class="num">195,class="num">209,class="num">223&class="macro">#x27;; text=::StringFormat("Optimization%sprogress%s", (tab_id==class="num">0 ? " " : " in "), (tab_id==class="num">0 ? "" : ": Waiting ... ")); } class=class="str">"cmt">//--- Draw header and text background.FillRectangle(x1, class="num">4, x2, y1, ::ColorToARGB(clr)); foreground.FillRectangle(x1, class="num">4, x2, y2, 0x00FFFFFF); foreground.FontSet("Calibri", -class="num">100, FW_BLACK); foreground.TextOut((x1+x2)/class="num">2, class="num">4+CELL_H/class="num">2, text, ::ColorToARGB(clrMidnightBlue), TA_CENTER|TA_VCENTER); class=class="str">"cmt">//--- Erase whole chart of optimization results background.FillRectangle(x1, y1, x2, y2, 0x00FFFFFF); foreground.FillRectangle(x1, y1, x2, y2, 0x00FFFFFF); class=class="str">"cmt">//--- Update optimization chart chart_stat.Update(clrNONE, class="num">0, chart_redraw); }
「把帧查看器类挂进现成 EA」
把标准库里的 ExpertMAMA.mq5(路径 \MQL5\Experts\Advisors\)复制到新建的 \MQL5\Experts\FrameViewer\ 目录,改名 ExpertMAMA_Frames.mq5。这一步只是挪文件,不改逻辑,目的是给后续接入留干净的工作副本。 在文件末尾直接追加 CFrameViewer 类的源码,然后声明一个该类类型的对象,并在 EA 的 OnTick 或处理函数中调用与类名同名的成员函数。这样优化跑起来时,帧数据会被实时推到独立窗口。 输入变量名里的下划线('_')可以删掉,比如 InpSignalMAPeriod 写成 InpSignalMAPeriod 缩短为 InpSignalMAPeriod 去下划线形态,表格单元格能宽出几个字符,视觉优化面板不至于挤变形。 编译后跑优化,测试设置本身不重要,用默认 MA 周期 12、偏移 6、SMA 收价就能起。优化前会弹出一个新图表窗口承载所有控件,可拖到副屏,不必在结果图和可视化聊天窗之间来回切。外汇与贵金属优化存在过拟合高风险,实盘前需多周期验证。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ExpertMAMA.mq5 | class=class="str">"cmt">//| Copyright class="num">2000-class="num">2025, MetaQuotes Ltd. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2000-class="num">2025, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Include | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include <Expert\Expert.mqh> class="macro">#include <Expert\Signal\SignalMA.mqh> class="macro">#include <Expert\Trailing\TrailingMA.mqh> class="macro">#include <Expert\Money\MoneyNone.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Inputs | class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//--- inputs for expert input class="type">class="kw">string InpExpertTitle = "ExpertMAMA"; class="type">int Expert_MagicNumber = class="num">12003; class="type">bool Expert_EveryTick = false; class=class="str">"cmt">//--- inputs for signal input class="type">int InpSignalMAPeriod = class="num">12; input class="type">int InpSignalMAShift = class="num">6; input ENUM_MA_METHOD InpSignalMAMethod = MODE_SMA; input ENUM_APPLIED_PRICE InpSignalMAApplied = PRICE_CLOSE; class=class="str">"cmt">//--- inputs for trailing input class="type">int InpTrailingMAPeriod = class="num">12; input class="type">int InpTrailingMAShift = class="num">0;
在 OnInit 里把信号与跟踪挂到专家对象
EA 启动阶段最关键的一步,是把均线信号和均线跟踪止损这两个模块正确挂到全局的 CExpert 实例上。任何一步返回失败,OnInit 都会带负代码退出,MT5 终端日志里能看到具体是哪一号错误。 下面这段初始化代码,先尝试用当前品种和周期初始化专家对象,失败就打印函数名并 Deinit 返回 -1。随后 new 一个 CSignalMA,若指针为空返回 -2,这在外存紧张或基类构造异常时可能出现。 signal 挂进专家用 InitSignal,返回 false 则 -3;接着设 PeriodMA / Shift / Method / Applied 四个参数,再跑 ValidationSettings 做合法性校验,不通过返回 -4。均线跟踪对象 CTrailingMA 的挂载逻辑完全一致,创建失败 -5、InitTrailing 失败 -6。 把这些负返回值记下来很有用:在策略测试器里若 EA 起不来,对照 -1 到 -6 能直接定位是信号层还是跟踪层的问题,不用逐行断点。外汇与贵金属品种波动剧烈,EA 初始化异常可能让你错过关键价位,上实盘前务必在模拟环境跑通整套 OnInit。
input ENUM_MA_METHOD InpTrailingMAMethod = MODE_SMA; input ENUM_APPLIED_PRICE InpTrailingMAApplied= PRICE_CLOSE; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Global expert object | class=class="str">"cmt">//+------------------------------------------------------------------+ CExpert ExtExpert; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Initialization function of the expert | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit(class="type">void) { class=class="str">"cmt">//--- Initializing expert if(!ExtExpert.Init(Symbol(),Period(),Expert_EveryTick,Expert_MagicNumber)) { class=class="str">"cmt">//--- failed printf(__FUNCTION__+": error initializing expert"); ExtExpert.Deinit(); class="kw">return(-class="num">1); } class=class="str">"cmt">//--- Creation of signal object CSignalMA *signal=new CSignalMA; if(signal==NULL) { class=class="str">"cmt">//--- failed printf(__FUNCTION__+": error creating signal"); ExtExpert.Deinit(); class="kw">return(-class="num">2); } class=class="str">"cmt">//--- Add signal to expert(will be deleted automatically)) if(!ExtExpert.InitSignal(signal)) { class=class="str">"cmt">//--- failed printf(__FUNCTION__+": error initializing signal"); ExtExpert.Deinit(); class="kw">return(-class="num">3); } class=class="str">"cmt">//--- Set signal parameters signal.PeriodMA(InpSignalMAPeriod); signal.Shift(InpSignalMAShift); signal.Method(InpSignalMAMethod); signal.Applied(InpSignalMAApplied); class=class="str">"cmt">//--- Check signal parameters if(!signal.ValidationSettings()) { class=class="str">"cmt">//--- failed printf(__FUNCTION__+": error signal parameters"); ExtExpert.Deinit(); class="kw">return(-class="num">4); } class=class="str">"cmt">//--- Creation of trailing object CTrailingMA *trailing=new CTrailingMA; if(trailing==NULL) { class=class="str">"cmt">//--- failed printf(__FUNCTION__+": error creating trailing"); ExtExpert.Deinit(); class="kw">return(-class="num">5); } class=class="str">"cmt">//--- Add trailing to expert(will be deleted automatically)) if(!ExtExpert.InitTrailing(trailing)) { class=class="str">"cmt">//--- failed printf(__FUNCTION__+": error initializing trailing"); ExtExpert.Deinit(); class="kw">return(-class="num">6); } class=class="str">"cmt">//--- Set trailing parameters
◍ EA初始化失败的逐级拦截
这段代码是 MT5 专家顾问初始化尾段与事件转发的核心。先把 trailing 对象的四个参数(周期、偏移、方法、应用价)灌进去,紧接着就调 ValidationSettings() 做校验,不通过直接打印错误并返回 -7,同时把 ExtExpert 反初始化掉。 资金模块用 CMoneyNone 占位,new 出来若为 NULL 返回 -8;挂到 Expert 上若 InitMoney 失败返回 -9;连 money.ValidationSettings() 也单独再卡一道,失败返回 -10。指标初始化失败则返回 -11。任何一步报错都不会带着半成品跑策略。 OnDeinit、OnTick、OnTrade 三个事件函数都只是薄转发层:卸载时调 ExtExpert.Deinit(),每笔报价和每笔成交分别丢给 ExtExpert.OnTick() 与 ExtExpert.OnTrade()。你拷这段进 MT5 时,重点看自己改了 InpTrailingMAPeriod 等外部输入后,校验返回值是不是还走 -7 到 -11 的递增编号,方便定位是哪层配置崩了。外汇与贵金属品种波动剧烈,这类初始化拦截能降低实盘带病启动的概率,但无法消除滑点与跳空风险。
trailing.Period(InpTrailingMAPeriod); trailing.Shift(InpTrailingMAShift); trailing.Method(InpTrailingMAMethod); trailing.Applied(InpTrailingMAApplied); class=class="str">"cmt">//--- Check trailing parameters if(!trailing.ValidationSettings()) { class=class="str">"cmt">//--- failed printf(__FUNCTION__+": error trailing parameters"); ExtExpert.Deinit(); class="kw">return(-class="num">7); } class=class="str">"cmt">//--- Creation of money object CMoneyNone *money=new CMoneyNone; if(money==NULL) { class=class="str">"cmt">//--- failed printf(__FUNCTION__+": error creating money"); ExtExpert.Deinit(); class="kw">return(-class="num">8); } class=class="str">"cmt">//--- Add money to expert(will be deleted automatically)) if(!ExtExpert.InitMoney(money)) { class=class="str">"cmt">//--- failed printf(__FUNCTION__+": error initializing money"); ExtExpert.Deinit(); class="kw">return(-class="num">9); } class=class="str">"cmt">//--- Set money parameters class=class="str">"cmt">//--- Check money parameters if(!money.ValidationSettings()) { class=class="str">"cmt">//--- failed printf(__FUNCTION__+": error money parameters"); ExtExpert.Deinit(); class="kw">return(-class="num">10); } class=class="str">"cmt">//--- Tuning of all necessary indicators if(!ExtExpert.InitIndicators()) { class=class="str">"cmt">//--- failed printf(__FUNCTION__+": error initializing indicators"); ExtExpert.Deinit(); class="kw">return(-class="num">11); } class=class="str">"cmt">//--- succeed class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Deinitialization function of the expert | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { ExtExpert.Deinit(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Function-event handler "tick" | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick(class="type">void) { ExtExpert.OnTick(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Function-event handler "trade" | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTrade(class="type">void) { ExtExpert.OnTrade(); } class=class="str">"cmt">//+------------------------------------------------------------------+
「把优化结果画进图表的那几行钩子」
EA 在 MT5 里跑批量优化时,默认会把中断后的残次 pass 缓存下来,下次点优化可能从断点续跑。想每次都干净重跑,加一行 #property tester_no_cache 就能强制清缓存,这个细节不注意容易误以为参数空间没扫全。
可视化部分靠 CFrameViewer 对象接手。宏里 REPLAY_DELAY_MS=100 控制回放延迟毫秒数,STAT_LINES=1 只画一条权益线,SELECTED_LINE_WD=3 和 SELECTED_LINE_CLR=clrDodgerBlue 决定选中 pass 的线宽与颜色,改这几个数就能调观察体验。
OnTester 里自定义了优化判据:用夏普比率绝对值乘净利润,净利润为负时把判据取反,再丢给 fw.OnTester()。外汇与贵金属杠杆高、滑点跳空频繁,这套判据只是筛选候选集,实盘前仍要在历史分段里手动复核。
OnTesterInit 在测试初始化时调 fw.OnTesterInit 把上面宏参传进去布局图表,OnTesterDeinit 负责收尾——把这两处和 FrameViewer.mqh 里的实现对照看,就能知道线是怎么刷出来的。
class=class="str">"cmt">//| Function-event handler "timer" | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTimer(class="type">void) { ExtExpert.OnTimer(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Code required to visualize optimization | class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//--- When debugging, if press "Stop" during optimization, next run of optimization will class="kw">continue incomplete passes from stop point class=class="str">"cmt">//--- In order for each new optimization run to start anew, define preprocessor directive class="macro">#class="kw">property tester_no_cache class=class="str">"cmt">//--- Define macro substitutions class="macro">#define REPLAY_DELAY_MS class="num">100 class=class="str">"cmt">// Optimization replay delay in milliseconds class="macro">#define STAT_LINES class="num">1 class=class="str">"cmt">// Number of optimization statistics lines displayed class="macro">#define SELECTED_LINE_WD class="num">3 class=class="str">"cmt">// Thickness of line of selected optimization passage class="macro">#define SELECTED_LINE_CLR clrDodgerBlue class=class="str">"cmt">// Color of line of selected optimization passage class=class="str">"cmt">//--- Connect code to work with the optimization results by frame viewer class="macro">#include "FrameViewer.mqh" class=class="str">"cmt">//--- Declare frame viewer object CFrameViewer fw; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Tester function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double OnTester() { class=class="str">"cmt">//--- here insert your own function to calculate optimization criterion class="type">class="kw">double TesterCritetia=MathAbs(TesterStatistics(STAT_SHARPE_RATIO)*TesterStatistics(STAT_PROFIT)); TesterCritetia=TesterStatistics(STAT_PROFIT)>class="num">0?TesterCritetia:(-TesterCritetia); class=class="str">"cmt">//--- call at each end of testing and pass optimization criterion as parameter fw.OnTester(TesterCritetia); class=class="str">"cmt">//--- class="kw">return(TesterCritetia); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TesterInit function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTesterInit() { class=class="str">"cmt">//--- prepare chart for displaying balance lines class=class="str">"cmt">//--- STAT_LINES sets number of balance lines on chart, class=class="str">"cmt">//--- SELECTED_LINE_WD - sets width, SELECTED_LINE_CLR - sets class="type">color of line of selected passage fw.OnTesterInit(STAT_LINES, SELECTED_LINE_WD, SELECTED_LINE_CLR); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TesterDeinit function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTesterDeinit() { class=class="str">"cmt">//--- completing optimization
把优化结果变成可回放画面
在 MT5 策略测试器的框架里,OnTesterDeinit 和 OnTesterPass 是两个容易被忽略的生命周期钩子。前者在优化收尾时触发,后者在每一组参数通过测试时触发,把可视化框架的对应方法接进去,就能在测试结束后直接看图形输出而不是只盯净值曲线。 OnChartEvent 里那行把 REPLAY_DELAY_MS 传进去的调用,决定的是回放帧之间的间隔毫秒数。这个值设太小,CPU 占用会飙;设太大,复盘动画卡顿,肉眼难捕捉价格行为细节。外汇与贵金属市场高杠杆、高波动,回测画面仅作概率参考,不构成方向判断。 下面这段是接好框架后的最小可用骨架,复制进 EA 即可在优化完点表头重播:
fw.OnTesterDeinit(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TesterPass function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTesterPass() { class=class="str">"cmt">//--- handle test results and display graphics fw.OnTesterPass(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Event handling on chart | 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=class="str">"cmt">//--- starts playback of frames upon completion of optimization when clicking on header fw.OnChartEvent(id,lparam,dparam,sparam,REPLAY_DELAY_MS); class=class="str">"cmt">// REPLAY_DELAY_MS - pause in ms between replay frames }
◍ 把这条线请下神坛
这套可视化优化框架说到底只是给策略测试器加了一层自选显示层:Table.mqh 66.62 KB、Controls.mqh 183.33 KB、FrameViewer.mqh 187.03 KB 三个类库加上 ExpertMAMA_Frames.mq5(19.26 KB)的 EA,解压 MQL5.zip(52 KB)进终端目录就能跑。它能把测试报告里的任意字段或每次优化 pass 后自算的数据画到图表上,但画什么、怎么画完全看你自己的交易逻辑,不是官方给的圣杯。 fxsaber 在 2025 年 3 月 21 日的评论点得很直白:opt 格式公开之后,只有传输 opt 文件里没有的数据才值得用帧。也就是说,如果你要的只是参数组合和权益曲线,原生 opt 够了;只有当你要在优化过程里塞入自建指标或中间计算结果,这套 GUI 才有存在价值。 外汇和贵金属市场高杠杆、滑点诡变,任何优化画面都只是历史数据的投影,实盘仍可能偏离。把代码下下来,先拿 MAMA 这个样例 EA 在 MT5 里走一遍优化,确认你的自定义字段真能落进 FrameViewer 再谈改造,比对着文章脑补有用得多。