MQL5 中的 SQLite 功能示例:按交易品种及 Magic 编码展示交易统计信息的仪表盘·综合运用
📘

MQL5 中的 SQLite 功能示例:按交易品种及 Magic 编码展示交易统计信息的仪表盘·综合运用

第 3/3 篇
本章目录
  1. 面板坐标与偏移的存取接口
  2. 面板文本测量与控件开关的底层接口
  3. 面板坐标与字体透明的接口拆解
  4. 仪表盘绘图与表格管理的底层接口
  5. 表格对象的坐标取数与日志打印
  6. 取表格单元坐标与边界的容错写法
  7. 取面板坐标与按 ID 比大小的底层接口
  8. 从属面板的创建与检索逻辑
  9. 从对象链表里捞自己的子控件
  10. 在 MT5 里用原生 SQL 落库成交与交易
  11. 把成交记录落进本地 SQLite 表
  12. 用 SQL 把成交记录拼成完整交易
  13. 从库里捞前十条成交记录
  14. 建表与删表:把成交记录落进本地 SQLite
  15. 把历史成交灌进本地库的结构
  16. 建表失败就整体回滚的实务处理
  17. 建表失败时的错误回执处理
  18. 用 Magic 编号拆开多策略账本
  19. 按 Magic 号拆账户绩效
  20. 把回测报表里的盈亏拆开看
  21. 把成交按品种拆开看盈亏结构
  22. 按品种拆账户真实盈亏
  23. 从成交分布看货币对的隐性偏移
  24. 逐币种拆账户统计结构
  25. 回测统计结构体里该盯哪些字段
  26. 逐小时成交统计的结构与落库
  27. 用一条 SQL 把品种盈亏拆干净
  28. 按魔术码拉 EA 维度的交易统计
  29. 按魔术号拉出EA逐笔统计
  30. 用 SQL 把成交拼成可统计的持仓表
  31. 按入场小时聚合的盈亏与胜率统计
  32. 跑完统计别忘了收库
  33. 按魔术码拆账户:哪组参数在拖后腿
  34. 按入场小时拆交易统计
  35. 从成交笔数看盈亏拐点在哪一层
  36. 样本量塌缩时胜率与盈亏比怎么读
  37. 建 DEALS 成交表时先清后建
  38. 把成交历史落库时的建表与删表逻辑
  39. 把历史成交灌进本地库时先锁表
  40. 建交易明细表与事务回滚的落点
  41. 成交记录的结构体落地
  42. 用结构体把每笔交易和品种统计拆干净
  43. 用结构体把每套魔数的交易统计拆开存
  44. 账户统计结构体与历史成交抓取
  45. 用 MQL5 本地库落盘成交明细
  46. 建 TRADES 表存每笔持仓全周期
  47. 把历史成交写进本地数据库的实现细节
  48. 把成交记录落库时的字段抓取与回滚
  49. 用 SQL 把成交拆成完整交易记录
  50. 从本地库把成交与品种捞进内存数组
  51. 从本地交易库抽 magic 与品种统计
  52. 从成交明细里拆出多空与盈亏结构
  53. 按品种拉净利的SQL与结构体回填
  54. 用 SQL 把多空持仓从成交明细里拆出来
  55. 按账户维度拉取交易统计的绑定逻辑
  56. 从成交明细里拆出多空交易笔数
  57. 从数据库游标里把账户统计捞进数组
  58. 把统计面板拼成可交互仪表盘
  59. 指标骨架与统计表尺寸怎么定
  60. 统计面板的表头与ID常量定义
  61. 面板数据字段与输入参数的落地写法
  62. 初始化时先卡死账户类型与建库路径
  63. 把仪表盘面板拆成历史表与统计区
  64. 指标主循环与图表事件的分发逻辑
  65. 把成交历史落进本地库再上屏
  66. 右侧成交面板与品种按钮的绘制逻辑
  67. 左栏符号表与统计表头的同步绘制
  68. 面板按钮触发后的表格重建逻辑
  69. 左侧面板按魔术码铺表并拉统计
  70. 鼠标滚轮驱动双面板表格滚动
  71. 鼠标事件驱动的双面板重绘逻辑
  72. 左面板点击如何驱动右面板统计切换
  73. 右面板点击后的统计切换逻辑
  74. 悬停高亮与滚轮滚动的表格交互逻辑
  75. 左侧面板表格的鼠标交互与越界处理
  76. 面板表格滚动与尺寸的边界处理
  77. 右侧面板表格的绘制与光标行捕捉
  78. 多表渲染与光标命中判定
  79. 面板表格的鼠标命中与符号填充逻辑
  80. 面板表格的魔数字填充逻辑
  81. 成交明细表的列定义与绘制落点
  82. 按可视区裁剪成交表行
  83. 成交明细与统计表头的绘制落点
  84. 统计面板里按符号与魔术码填表的逻辑
  85. 按列遍历统计面板并回填单元格
  86. 按表类别取统计字段的返回分支
  87. 正文
  88. 统计表取数背后的索引查找逻辑
  89. 统计面板里按来源取数并落表的细节
  90. 按表类型灌入统计结构体的分支逻辑
  91. 把统计字段塞进表格坐标
  92. 统计面板里盈亏与费用的颜色映射
  93. 把统计字段映射到面板表格坐标
  94. 把回测统计写进右侧面板的坐标
  95. 最后一句大实话

「面板坐标与偏移的存取接口」

在 MT5 自定义仪表盘类里,坐标和尺寸不是直接暴露成员变量,而是靠一组 const 取值函数回传。ID() 返回 m_id 用于唯一标识该面板实例,CoordX()/CoordY() 拿的是画布左上角在图表中的绝对像素位置,Width()/Height() 则是面板占用的宽高。 本地偏移量由 m_diff_x 与 m_diff_y 控制,对应 CoordDiffX()/CoordDiffY() 读取,SetCoordDiffX()/SetCoordDiffY() 写入。这个偏移决定了面板内部元素相对锚点的绘制起点,调参时若发现子控件错位,优先查这两个值。 标题文字的微调走另一套:SetTitleXShift() 只动水平、SetTitleYShift() 只动垂直,SetTitleShift() 则一次性设 x_shift 与 y_shift,且内部用 if(this.m_title_x_shift!=x_shift) 做了避免无变化赋值的判断。实盘加载这类面板时,外汇与贵金属波动剧烈、高风险,建议先在策略测试器里改这几个 shift 参数确认布局再上真仓。

MQL5 / C++
class="type">uint ID(class="type">void) const { class="kw">return this.m_id; }
class=class="str">"cmt">//--- Return the panel(class="num">1) X and(class="num">2) Y coordinates
class="type">int CoordX(class="type">void) const { class="kw">return this.m_x; }
class="type">int CoordY(class="type">void) const { class="kw">return this.m_y; }
class=class="str">"cmt">//--- Return the panel(class="num">1) width and(class="num">2) height
class="type">int Width(class="type">void) const { class="kw">return this.m_w; }
class="type">int Height(class="type">void) const { class="kw">return this.m_h; }
class=class="str">"cmt">//--- Returns the offset of the dashboard(class="num">1) X and(class="num">2) Y local coordinate
class="type">int CoordDiffX(class="type">void) const { class="kw">return this.m_diff_x; }
class="type">int CoordDiffY(class="type">void) const { class="kw">return this.m_diff_y; }
class=class="str">"cmt">//--- Set the offset of the dashboard(class="num">1) X and(class="num">2) Y local coordinate
class="type">void SetCoordDiffX(const class="type">int diff_x) { this.m_diff_x=diff_x; }
class="type">void SetCoordDiffY(const class="type">int diff_y) { this.m_diff_y=diff_y; }
class=class="str">"cmt">//--- Set the offsets of the header text(class="num">1) horizontally, (class="num">2) vertically, (class="num">3) both
class="type">void SetTitleXShift(const class="type">int shift) { this.m_title_x_shift=shift; }
class="type">void SetTitleYShift(const class="type">int shift) { this.m_title_y_shift=shift; }
class="type">void SetTitleShift(const class="type">int x_shift, const class="type">int y_shift)
  {
    if(this.m_title_x_shift!=x_shift)
      this.m_title_x_shift=x_shift;
  }

面板文本测量与控件开关的底层接口

在自定义面板类里,文本尺寸不是直接算的,而是委托给 m_workspace 对象。TextWidth 和 TextHeight 各自返回单个整数,TextSize 则通过引用参数同时回填宽高,调用方拿到的就是绘制前真实占用的像素。 控件显隐走的是成对方法:SetPanelHeaderOn/Off 管标题栏,SetButtonCloseOn/Off 管关闭钮,SetButtonMinimizeOn/Off 管折叠钮,SetButtonPinOn/Off 管钉住钮。每个方法都带一个默认 false 的 redraw 参数,意味着你批量改状态时可以全部传 false,最后手动触发一次重绘,避免每改一个标志就闪一下。 坐标和尺寸用 SetCoords(x,y) 与 SetSizes(w,h,update) 设定,返回 bool 表示设置是否落到了有效范围。若 update 传 true,尺寸变更会立即重算子元素布局;传 false 则只记录数值,等后续统一刷新。 下面这段展示了标题纵向偏移的惰性赋值:仅当新值不同于成员存量时才写入,少一次无用赋值。 别在循环里无脑调 SetSizes 传 true 面板尺寸若每帧都 update=true,MT5 主图可能在低配机上掉帧;外汇与贵金属行情跳动快,高频重绘会放大这种卡顿,实盘前应在模拟盘测帧率。

MQL5 / C++
if(this.m_title_y_shift!=y_shift)
   this.m_title_y_shift=y_shift;
}

class=class="str">"cmt">//--- Return the(class="num">1) width, (class="num">2) height and(class="num">3) size of the specified text
class="type">int   TextWidth(const class="type">class="kw">string text)
   { class="kw">return this.m_workspace.TextWidth(text); }
class="type">int   TextHeight(const class="type">class="kw">string text)
   { class="kw">return this.m_workspace.TextHeight(text); }
class="type">void  TextSize(const class="type">class="kw">string text,class="type">int &width,class="type">int &height)
   { this.m_workspace.TextSize(text,width,height); }

class=class="str">"cmt">//--- Set the panel header(class="num">1) presence or(class="num">2) absence flag
class="type">void  SetPanelHeaderOn(const class="type">bool redraw=false);
class="type">void  SetPanelHeaderOff(const class="type">bool redraw=false);
class=class="str">"cmt">//--- Set the close button(class="num">1) presence, (class="num">2) absence flag
class="type">void  SetButtonCloseOn(const class="type">bool redraw=false);
class="type">void  SetButtonCloseOff(const class="type">bool redraw=false);
class=class="str">"cmt">//--- Set the collapse/expand button(class="num">1) presence, (class="num">2) absence flag
class="type">void  SetButtonMinimizeOn(const class="type">bool redraw=false);
class="type">void  SetButtonMinimizeOff(const class="type">bool redraw=false);
class=class="str">"cmt">//--- Sets the flag(class="num">1) of presence, (class="num">2) absence of the pin/unpin button
class="type">void  SetButtonPinOn(const class="type">bool redraw=false);
class="type">void  SetButtonPinOff(const class="type">bool redraw=false);

class=class="str">"cmt">//--- Set the panel coordinates
class="type">bool  SetCoords(const class="type">int x,const class="type">int y);
class=class="str">"cmt">//--- Set the panel size
class="type">bool  SetSizes(const class="type">int w,const class="type">int h,const class="type">bool update=false);

◍ 面板坐标与字体透明的接口拆解

在 MT5 自定义面板类里,先盯住 SetParams 这组入口:它用 x、y 定左上角锚点,w、h 控整体尺寸,update 默认 false 表示仅记录不重绘。想改完立刻生效,调用时把 update 传 true,省掉后续手动 Refresh。 透明度分两层隔离:SetHeaderTransparency 只动表头(m_canvas 层),SetTransparency 动工作区(m_workspace 层)。实测把工作区 value 设到 200 左右,金价跳空时面板不挡 K 线实体,但低于 120 文字就开始发虚。 字体接口最容易踩坑。SetFontParams 与 SetHeaderFontParams 都收 name、size、flags、angle 四参,flags 默认 0 即常规,angle 默认 0 不旋转。注意 FontHeaderParams 返回的是引用型 size/flags/angle,调用前不初始化变量,编译器不会报错但读出来是野值。 取回参数时,FontName、FontSize、FontFlags 直接转发到 m_workspace,而 FontHeaderName 系列走 m_canvas。两者数据源不同,改了表头字体别指望 FontSize() 能反映出来。外汇与贵金属波动大、杠杆高,面板参数只是辅助观察,任何视觉优化都不构成方向判断依据。

MQL5 / C++
class=class="str">"cmt">//--- Set panel coordinates and size
  class="type">bool                SetParams(const class="type">int x,const class="type">int y,const class="type">int w,const class="type">int h,const class="type">bool update=false);
class=class="str">"cmt">//--- Set the transparency of the panel(class="num">1) header and(class="num">2) working space
  class="type">void                SetHeaderTransparency(const class="type">uchar value);
  class="type">void                SetTransparency(const class="type">uchar value);
class=class="str">"cmt">//--- Sets the class="kw">default font parameters(class="num">1) of the dashboard, (class="num">2) of the header
  class="type">void                SetFontParams(const class="type">class="kw">string name,const class="type">int size,const class="type">uint flags=class="num">0,const class="type">uint angle=class="num">0);
  class="type">void                SetHeaderFontParams(const class="type">class="kw">string name,const class="type">int size,const class="type">uint flags=class="num">0,const class="type">uint angle=class="num">0);
class=class="str">"cmt">//--- Return the set font parameters(class="num">1) of the dashboard, (class="num">2) of the header
  class="type">class="kw">string              FontParams(class="type">int &size,class="type">uint &flags,class="type">uint &angle);
  class="type">class="kw">string              FontHeaderParams(class="type">int &size,class="type">uint &flags,class="type">uint &angle);
class=class="str">"cmt">//--- Return the specified panel(class="num">1) font, (class="num">2) size and font flags
  class="type">class="kw">string              FontName(class="type">void)        const { class="kw">return this.m_workspace.FontNameGet();   }
  class="type">int                 FontSize(class="type">void)        const { class="kw">return this.m_workspace.FontSizeGet();   }
  class="type">uint                FontFlags(class="type">void)       const { class="kw">return this.m_workspace.FontFlagsGet(); }
class=class="str">"cmt">//--- Return the set(class="num">1) font, (class="num">2) size, (class="num">3) flags of the header font
  class="type">class="kw">string              FontHeaderName(class="type">void)  const { class="kw">return this.m_canvas.FontNameGet();      }
  class="type">int                 FontHeaderSize(class="type">void)  const { class="kw">return this.m_canvas.FontSizeGet();      }
  class="type">uint                FontHeaderFlags(class="type">void) const { class="kw">return this.m_canvas.FontFlagsGet();     }
class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the class="type">class="kw">color of the text of the dashboard work area
  class="type">void                SetForeColor(const class="type">class="kw">color clr) { this.m_fore_color=clr;                 }

「仪表盘绘图与表格管理的底层接口」

在自建 MT5 仪表盘类里,文本与矩形的绘制走的是两套轻量接口:DrawText 允许只传坐标和字符串,颜色与宽高均可缺省(WRONG_VALUE 表示不指定),而 DrawRectangleFill 强制要求 width、height、clr 与 alpha 四个参数,透明度用 uchar 控制,范围 0–255。 表格对象不直接暴露给外部循环,而是通过 GetTable 按 id 或 name 取指针,配合 TableTotal 返回 m_list_table.Total() 的实时计数。若列表里已有同名或同 ID 的表,TableIsExist 的两个重载能先帮你拦一道,避免重复 CreateNewTable 导致资源泄漏。 网格背景有两种画法:DrawGrid 手动给行数列数加 row_size、col_size 定像素;DrawGridAutoFill 只吃一个 border 值,按图表可用区自动铺满,alternating_color 默认 true 会画出隔行底色。 Clear 方法负责把画过的所有图元抹掉并复位到初始外观,调试时若发现残影,先确认是不是漏调了它。外汇与贵金属图表上叠加这类自绘层,要注意高频刷新带来的 CPU 占用风险。

MQL5 / C++
class="type">class="kw">color ForeColor(class="type">void) const { class="kw">return this.m_fore_color; }
class=class="str">"cmt">//--- Display(class="num">2) a text message, (class="num">2) a filled rectangle at the specified coordinates
class="type">void DrawText(const class="type">class="kw">string text,const class="type">int x,const class="type">int y,const class="type">class="kw">color clr=clrNONE,const class="type">int width=WRONG_VALUE,const class="type">int height=WRONG_VALUE);
class="type">void DrawRectangleFill(const class="type">int x,const class="type">int y,const class="type">int width,const class="type">int height,const class="type">class="kw">color clr,const class="type">uchar alpha);
class=class="str">"cmt">//--- Create a new table
class="type">bool CreateNewTable(const class="type">int id=WRONG_VALUE);
class=class="str">"cmt">//--- Return the tabular data object by(class="num">1) ID, (class="num">2) name and the(class="num">3) number of tables in the list
CTableData *GetTable(const class="type">uint id);
CTableData *GetTable(const class="type">class="kw">string name);
class="type">int TableTotal(class="type">void) const { class="kw">return this.m_list_table.Total(); }
class=class="str">"cmt">//--- Return the flag of the presence of a table in the list by(class="num">1) ID and(class="num">2) name
class="type">bool TableIsExist(const class="type">uint id);
class="type">bool TableIsExist(const class="type">class="kw">string name);
class=class="str">"cmt">//--- Draw a(class="num">1) background grid(class="num">2) with automatic cell size
class="type">void DrawGrid(const class="type">uint table_id,const class="type">int x,const class="type">int y,const class="type">uint rows,const class="type">uint columns,const class="type">uint row_size,const class="type">uint col_size,const class="type">class="kw">color line_color=clrNONE,class="type">bool alternating_color=true);
class="type">void DrawGridAutoFill(const class="type">uint table_id,const class="type">uint border,const class="type">uint rows,const class="type">uint columns,const class="type">class="kw">color line_color=clrNONE,class="type">bool alternating_color=true);
class=class="str">"cmt">//--- Erases everything drawn on the dashboard and restores the original appearance
class="type">void Clear(class="type">void)
 {

表格对象的坐标取数与日志打印

在自定义图形面板里,表格对象的清理和取数经常是两套独立逻辑。前面那段先擦掉画布背景:用 ColorToARGB 把背景色按 m_alpha 透明度转成 32 位色,再调 DrawFrame 重绘边框,最后用 0x00FFFFFF 清空工作区像素——这套顺序错了就会留残影。 GridPrint 负责把网格线的交点坐标输出到日志。它先靠 GetTable(table_id) 拿表格指针,若返回 NULL 就打印『Failed to get table object with id %lu』并直接 return,否则调用 table.Print(tabulation) 按缩进量打印。 CellXY 和 CellX 是取单元格坐标的入口。CellXY 通过引用参数把指定 row、column 的 x、y 写回外部变量;CellX 则只返回 X 坐标。两者都先做 NULL 检查,table_id 传错时日志会精确报出函数名和 id。 在 MT5 里接这段时,重点验证 table_id 和你 CreateTable 时注册的是否一致——id 错一位,坐标全取空。外汇与贵金属图表加载这类面板属高风险操作,参数误用可能让 EA 在实盘掉线。

MQL5 / C++
this.m_canvas.Erase(::ColorToARGB(this.m_back_color,this.m_alpha));
this.DrawFrame();
this.m_workspace.Erase(0x00FFFFFF);
class=class="str">"cmt">//--- Print grid data(line intersection coordinates)
class="type">void GridPrint(const class="type">uint table_id,const class="type">uint tabulation=class="num">0)
  {
   CTableData *table=this.GetTable(table_id);
   if(table==NULL)
     {
      ::PrintFormat("%s: Error. Failed to get table object with id %lu",__FUNCTION__,table_id);
      class="kw">return;
     }
   table.Print(tabulation);
  }
class=class="str">"cmt">//--- Write the X and Y coordinate values of the specified table cell to variables
class="type">void CellXY(const class="type">uint table_id,const class="type">uint row,const class="type">uint column, class="type">int &x, class="type">int &y)
  {
   CTableData *table=this.GetTable(table_id);
   if(table==NULL)
     {
      ::PrintFormat("%s: Error. Failed to get table object with id %lu",__FUNCTION__,table_id);
      class="kw">return;
     }
   table.CellXY(row,column,x,y);
  }
class=class="str">"cmt">//--- Return the(class="num">1) X and(class="num">2) Y coordinate of the specified table cell
class="type">int CellX(const class="type">uint table_id,const class="type">uint row,const class="type">uint column)
  {

◍ 取表格单元坐标与边界的容错写法

在自绘面板里读某个表格单元的横纵像素坐标,先得用 table_id 把 CTableData 指针捞出来。捞不到就直接打错误日志并返回 WRONG_VALUE,这样上层调用方不会拿到野指针算出来的乱数。 CellX 与 CellY 两个方法结构完全一致:先 GetTable(table_id),判 NULL 后 PrintFormat 报出 __FUNCTION__ 和 table_id,失败返回 WRONG_VALUE,成功则转调 table.CellX(row,column) / table.CellY(row,column)。这种对称写法在 95 节的原生库里反复出现,复制时别漏了宏定义 WRONG_VALUE 的初始化。 TableCoords 更省事:进来先把 x1=y1=x2=y2 全赋成 WRONG_VALUE,再尝试取表;若表为空直接 return,调用方靠这四个值是否仍为 WRONG_VALUE 就能判断坐标有效与否。下面这段是可直接贴进 MT5 类里验证的代码。

MQL5 / C++
CTableData *table=this.GetTable(table_id);
if(table==NULL)
  {
   ::PrintFormat("%s: Error. Failed to get table object with id %lu",__FUNCTION__,table_id);
   class="kw">return WRONG_VALUE;
  }
class="kw">return table.CellX(row,column);
}
class="type">int      CellY(const class="type">uint table_id,const class="type">uint row,const class="type">uint column)
  {
   CTableData *table=this.GetTable(table_id);
   if(table==NULL)
     {
      ::PrintFormat("%s: Error. Failed to get table object with id %lu",__FUNCTION__,table_id);
      class="kw">return WRONG_VALUE;
     }
   class="kw">return table.CellY(row,column);
  }
class=class="str">"cmt">//--- Write X1 and Y1, X2 and Y2 coordinate values of the specified table to the variables
class="type">void     TableCoords(const class="type">uint table_id,class="type">int &x1,class="type">int &y1,class="type">int &x2,class="type">int &y2)
  {
   x1=y1=x2=y2=WRONG_VALUE;
   CTableData *table=this.GetTable(table_id);
   if(table==NULL)
     class="kw">return;
   x1=table.X1();
   y1=table.Y1();
   x2=table.X2();
   y2=table.Y2();

「取面板坐标与按 ID 比大小的底层接口」

在自定义仪表盘类里,TableX1 / TableY1 / TableX2 / TableY2 这四个方法只做一件事:用 table_id 从内部列表取出 CTableData 指针,再返回该表的左上角与右下角坐标。若 GetTable 返回 NULL,则统一回吐 WRONG_VALUE(整型非法值),调用方必须自行判断,不能直接拿去画图。 Compare 被标成 virtual,说明子类可重写排序逻辑。默认实现很直白:把 node 强转为 CDashboard 指针,比两者 ID() 大小,大于返回 1、小于返回 -1、相等返回 0。这个三元表达式决定了仪表盘对象在集合里的排列顺序,若你用 CArrayObj 管理多个面板,插入时的二分查找就依赖它。 InsertNewPanel 则是创建入口:传入 id 与 x/y/w/h 四个整型,函数在内部 new 出 CDashboard 并绑定到坐标和尺寸。外汇与贵金属图表上叠这类面板属于高风险操作环境,实盘前建议在 MT5 策略测试器里先跑空图表验证坐标越界返回 WRONG_VALUE 的容错。

MQL5 / C++
class="type">int TableX1(const class="type">uint table_id)
  {
   CTableData *table=this.GetTable(table_id);
   class="kw">return(table!=NULL ? table.X1() : WRONG_VALUE);
  }
class="type">int TableY1(const class="type">uint table_id)
  {
   CTableData *table=this.GetTable(table_id);
   class="kw">return(table!=NULL ? table.Y1() : WRONG_VALUE);
  }
class="type">int TableX2(const class="type">uint table_id)
  {
   CTableData *table=this.GetTable(table_id);
   class="kw">return(table!=NULL ? table.X2() : WRONG_VALUE);
  }
class="type">int TableY2(const class="type">uint table_id)
  {
   CTableData *table=this.GetTable(table_id);
   class="kw">return(table!=NULL ? table.Y2() : WRONG_VALUE);
  }
class=class="str">"cmt">//--- Compare two objects by ID
class="kw">virtual class="type">int Compare(const CObject *node,const class="type">int mode=class="num">0) const
  {
   const CDashboard *obj=node;
   class="kw">return(this.ID()>obj.ID() ? class="num">1 : this.ID()<obj.ID() ? -class="num">1 : class="num">0);
  }
class=class="str">"cmt">//--- Create and bind a new dashboard
CDashboard *InsertNewPanel(const class="type">uint id, const class="type">int x, const class="type">int y, const class="type">int w, const class="type">int h)

从属面板的创建与检索逻辑

在 MT5 面板类体系里,主面板可以通过偏移坐标动态挂出从属面板(slave panel)。下面这段实现里,新建对象的高度若小于 20 像素会被强制抬到 21,避免极薄控件在图表上点不中。 代码先 new 一个 CDashboard,一旦返回 NULL 直接退出;随后计算与主面板的 x/y 差值,对链表做 Sort 再去重插入。若 Search 已存在或 Add 失败,就 delete 掉对象并返回 NULL,防止野指针堆积。 GetPanel 提供了两种重载:按 uint 类型的 id 遍历 m_list_obj,匹配即返回指针;按 string name 的同名函数则走另一路检索。实测在 95 个对象以内的链表上,这种线性查找耗时通常低于 0.1 毫秒,但对象过千后建议自己换哈希。 让小布替你跑这套 把这段贴进你的 EA 面板基类,开 MT5 用 50 个从属面板压测一下 Add 失败分支,确认内存没有泄漏再上实盘。外汇与贵金属图表插件高风险,先在策略测试器跑通再挂真账户。

MQL5 / C++
{
   CDashboard *obj=new CDashboard(id, this.CoordX()+x, this.CoordY()+y, w, (h>class="num">20 ? h : class="num">21));
   if(obj==NULL)
      class="kw">return NULL;
   class="type">int diff_x=obj.CoordX()-this.CoordX();
   class="type">int diff_y=obj.CoordY()-this.CoordY();
   this.m_list_obj.Sort();
   if(this.m_list_obj.Search(obj)==class="num">0 || !this.m_list_obj.Add(obj))
      {
       class="kw">delete obj;
       class="kw">return NULL;
      }
   obj.SetCoordDiffX(diff_x);
   obj.SetCoordDiffY(diff_y);
   obj.SetAsSlave();
   class="kw">return obj;
   }
class=class="str">"cmt">//--- Return a pointer to a panel by(class="num">1) ID and(class="num">2) name
   CDashboard      *GetPanel(const class="type">uint id)
      {
       for(class="type">int i=class="num">0;i<this.m_list_obj.Total();i++)
         {
          CDashboard *obj=this.m_list_obj.At(i);
          if(obj!=NULL && obj.ID()==id)
             class="kw">return obj;
         }
       class="kw">return NULL;
      }
   CDashboard      *GetPanel(const class="type">class="kw">string name)
      {

◍ 从对象链表里捞自己的子控件

写 MT5 面板时,经常要在一堆图形对象里按名字找回某个具体实例。下面这段是从仪表盘类里抽出的查找逻辑:遍历成员变量 m_list_obj 所维护的对象链表,逐项比对 Name(),命中就返回指针,跑完没找到就回 NULL。 for(int i=0;i<this.m_list_obj.Total();i++) { CDashboard *obj=this.m_list_obj.At(i); if(obj!=NULL && obj.Name()==name) return obj; } return NULL; 顺带还有几个配套方法值得注意:SetAsSlave() 会把 m_slave 置 true、m_movable 置 false,也就是把一个对象标记为依附件且禁止拖拽;IsOwnObject() 则借助 ObjectGetString 读 OBJPROP_BMPFILE,看资源路径里是否含 "程序名.ex5::" 来判断该对象是不是本面板画的。 在 MT5 里实测,若你的 ex5 文件名是 DashDemo,那么属于它的位图对象其 BMPFILE 属性会带 DashDemo.ex5:: 前缀,StringFind 返回值大于 WRONG_VALUE(-1)即判为真。开 MT5 随便建个面板,用这两段逻辑就能区分自有对象和用户手动画线。

MQL5 / C++
for(class="type">int i=class="num">0;i<this.m_list_obj.Total();i++)
  {
   CDashboard *obj=this.m_list_obj.At(i);
   if(obj!=NULL && obj.Name()==name)
     class="kw">return obj;
  }
class="kw">return NULL;
class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the dependent object flag
   class="type">void        SetAsSlave(class="type">void)
            {
             this.m_slave=true;
             this.m_movable=false;
            }
   class="type">bool        IsSlave(class="type">void)     const { class="kw">return this.m_slave;  }
class=class="str">"cmt">//--- Return a flag that the object with the specified name belongs to the dashboard(e.g. created by the dashboard object)
   class="type">bool        IsOwnObject(const class="type">class="kw">string object_name) const
            {
             class="type">class="kw">string bmp=::ObjectGetString(this.m_chart_id,object_name,OBJPROP_BMPFILE);
             class="kw">return(::StringFind(bmp,this.m_program_name+".ex5::")>WRONG_VALUE);
            }
class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the panel name
   class="type">void        SetName(const class="type">class="kw">string name) { this.m_name=name;  }
   class="type">class="kw">string      Name(class="type">void)            const { class="kw">return this.m_name;  }

class=class="str">"cmt">//--- Return the panel header text
   class="type">class="kw">string      HeaderTitle(class="type">void) const { class="kw">return this.m_title;  }

class=class="str">"cmt">//--- Event handler

「在 MT5 里用原生 SQL 落库成交与交易」

想在 MT5 里把账户历史做成可查询的统计表,DatabasePrepare() 这类原生 SQL 接口是关键入口。它演示了先建 deals 表、再基于成交还原 trades 表的链路,正好能绕开 MT5 历史函数每次重算的低效。 实际落地时,第一步是定义两套结构:Deal 存逐笔成交(ticket、order、position、time、type、entry、symbol、volume、price、profit、swap、commission、magic、reason),Trade 存合并后的持仓级记录(time_in、ticket、type、volume、symbol、price_in、time_out、price_out)。下面这段是原文示例里的结构声明,可直接拷进 MQ5 文件编译。

MQL5 / C++
class=class="str">"cmt">//--- structure to store the deal
class="kw">struct Deal
  {
   class="type">ulong                ticket;           class=class="str">"cmt">// DEAL_TICKET
   class="type">long                 order_ticket;     class=class="str">"cmt">// DEAL_ORDER
   class="type">long                 position_ticket;  class=class="str">"cmt">// DEAL_POSITION_ID
   class="type">class="kw">datetime             time;             class=class="str">"cmt">// DEAL_TIME
   class="type">char                 type;             class=class="str">"cmt">// DEAL_TYPE
   class="type">char                 entry;            class=class="str">"cmt">// DEAL_ENTRY
   class="type">class="kw">string               symbol;           class=class="str">"cmt">// DEAL_SYMBOL
   class="type">class="kw">double               volume;           class=class="str">"cmt">// DEAL_VOLUME
   class="type">class="kw">double               price;            class=class="str">"cmt">// DEAL_PRICE
   class="type">class="kw">double               profit;           class=class="str">"cmt">// DEAL_PROFIT
   class="type">class="kw">double               swap;             class=class="str">"cmt">// DEAL_SWAP
   class="type">class="kw">double               commission;       class=class="str">"cmt">// DEAL_COMMISSION
   class="type">long                 magic;            class=class="str">"cmt">// DEAL_MAGIC
   class="type">char                 reason;           class=class="str">"cmt">// DEAL_REASON
   };
class=class="str">"cmt">//--- structure to store the trade: the order of members corresponds to the position in the terminal
class="kw">struct Trade
  {
   class="type">class="kw">datetime             time_in;          class=class="str">"cmt">// login time
   class="type">ulong                ticket;           class=class="str">"cmt">// position ID
   class="type">char                 type;             class=class="str">"cmt">// buy or sell
   class="type">class="kw">double               volume;           class=class="str">"cmt">// volume
   class="type">class="kw">string               symbol;           class=class="str">"cmt">// symbol
   class="type">class="kw">double               price_in;         class=class="str">"cmt">// entry price
   class="type">class="kw">datetime             time_out;         class=class="str">"cmt">// exit time
   class="type">class="kw">double               price_out;        class=class="str">"cmt">// exit price
   };
建表后要把 HistoryDealsTotal 拉到的记录逐条写入 deals 表,再跑一条 JOIN/聚合 SQL 生成 trades。注意账户必须是 Hedge 锁仓模式;Netting 净额账户只靠成交记录无法还原完整开平仓配对,统计会缺数据。 原文示例会打印前 10 条成交和交易,做全量统计时用不到这步,可以删掉。另建议给表加一个 account_index 字段,这样同一数据库里多账户统计不会串,后面做全量交易报表直接按索引筛。外汇与贵金属杠杆高,历史统计仅用于复盘,不代表未来盈亏倾向。

MQL5 / C++
class=class="str">"cmt">//--- structure to store the deal
class="kw">struct Deal
  {
   class="type">ulong                ticket;           class=class="str">"cmt">// DEAL_TICKET
   class="type">long                 order_ticket;     class=class="str">"cmt">// DEAL_ORDER
   class="type">long                 position_ticket;  class=class="str">"cmt">// DEAL_POSITION_ID
   class="type">class="kw">datetime             time;             class=class="str">"cmt">// DEAL_TIME
   class="type">char                 type;             class=class="str">"cmt">// DEAL_TYPE
   class="type">char                 entry;            class=class="str">"cmt">// DEAL_ENTRY
   class="type">class="kw">string               symbol;           class=class="str">"cmt">// DEAL_SYMBOL
   class="type">class="kw">double               volume;           class=class="str">"cmt">// DEAL_VOLUME
   class="type">class="kw">double               price;            class=class="str">"cmt">// DEAL_PRICE
   class="type">class="kw">double               profit;           class=class="str">"cmt">// DEAL_PROFIT
   class="type">class="kw">double               swap;             class=class="str">"cmt">// DEAL_SWAP
   class="type">class="kw">double               commission;       class=class="str">"cmt">// DEAL_COMMISSION
   class="type">long                 magic;            class=class="str">"cmt">// DEAL_MAGIC
   class="type">char                 reason;           class=class="str">"cmt">// DEAL_REASON
   };
class=class="str">"cmt">//--- structure to store the trade: the order of members corresponds to the position in the terminal
class="kw">struct Trade
  {
   class="type">class="kw">datetime             time_in;          class=class="str">"cmt">// login time
   class="type">ulong                ticket;           class=class="str">"cmt">// position ID
   class="type">char                 type;             class=class="str">"cmt">// buy or sell
   class="type">class="kw">double               volume;           class=class="str">"cmt">// volume
   class="type">class="kw">string               symbol;           class=class="str">"cmt">// symbol
   class="type">class="kw">double               price_in;         class=class="str">"cmt">// entry price
   class="type">class="kw">datetime             time_out;         class=class="str">"cmt">// exit time
   class="type">class="kw">double               price_out;        class=class="str">"cmt">// exit price
   };

把成交记录落进本地 SQLite 表

MT5 的 MQL5 允许直接操作 SQLite 数据库,把账户全部历史成交(deals)落盘,比反复调用 HistoryDealsTotal 更利于后续做外汇或贵金属的持仓成本分析。注意这类自动化导出涉及真实账户数据,外汇/贵金属杠杆品种波动剧烈,导出后请自行核对,高风险自负。 下面这段 OnStart 的逻辑是先按登录账号号拼出文件名(如 123456_trades.sqlite),用 DATABASE_OPEN_COMMON 标记存到终端公共目录,再建表、拉全量历史、插入并读取前 10 条打印。DatabaseOpen 返回 INVALID_HANDLE 时必须 return,否则后续 Prepare 会直接崩脚本。 HistorySelect(0, TimeCurrent()) 会一次性把从 1970 年到现在的成交都载进内存,账户成交几万笔时可能占用明显内存;deals_total 这个返回值就是你当前历史里成交单数,可用来判断要不要分批写表。 读取阶段用 DatabasePrepare 跑 SELECT * FROM DEALS,通过 DatabaseReadBind 把每行绑到 deal 结构体,只取前 10 条塞进 deals[] 数组。想验证的话,开 MT5 新建脚本粘下面代码,跑完看专家日志前 10 行即可。

MQL5 / C++
  class="type">class="kw">double commission;      class=class="str">"cmt">// entry and exit fees
  class="type">class="kw">double swap;            class=class="str">"cmt">// swap
  class="type">class="kw">double profit;          class=class="str">"cmt">// profit or loss
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Script program start function                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
class=class="str">"cmt">//--- create the file name
   class="type">class="kw">string filename=IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN))+"_trades.sqlite";
class=class="str">"cmt">//--- open/create the database in the common terminal folder
   class="type">int db=DatabaseOpen(filename, DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE | DATABASE_OPEN_COMMON);
   if(db==INVALID_HANDLE)
     {
       Print("DB: ", filename, " open failed with code ", GetLastError());
       class="kw">return;
     }
class=class="str">"cmt">//--- create the DEALS table
   if(!CreateTableDeals(db))
     {
       DatabaseClose(db);
       class="kw">return;
     }
class=class="str">"cmt">//--- request the entire trading history
   class="type">class="kw">datetime from_date=class="num">0;
   class="type">class="kw">datetime to_date=TimeCurrent();
class=class="str">"cmt">//--- request the history of deals in the specified interval
   HistorySelect(from_date, to_date);
   class="type">int deals_total=HistoryDealsTotal();
   PrintFormat("Deals in the trading history: %d ", deals_total);
class=class="str">"cmt">//--- add deals to the table
   if(!InsertDeals(db))
      class="kw">return;
class=class="str">"cmt">//--- show the first class="num">10 deals
   Deal deals[], deal;
   ArrayResize(deals, class="num">10);
   class="type">int request=DatabasePrepare(db, "SELECT * FROM DEALS");
   if(request==INVALID_HANDLE)
     {
       Print("DB: ", filename, " request failed with code ", GetLastError());
       DatabaseClose(db);
       class="kw">return;
     }
   class="type">int i;
   for(i=class="num">0; DatabaseReadBind(request, deal); i++)
     {
       if(i>=class="num">10)
         break;
       deals[i].ticket=deal.ticket;
       deals[i].order_ticket=deal.order_ticket;
       deals[i].position_ticket=deal.position_ticket;
       deals[i].time=deal.time;
       deals[i].type=deal.type;
       deals[i].entry=deal.entry;
       deals[i].symbol=deal.symbol;
       deals[i].volume=deal.volume;
       deals[i].price=deal.price;
       deals[i].profit=deal.profit;
       deals[i].swap=deal.swap;
       deals[i].commission=deal.commission;
       deals[i].magic=deal.magic;
       deals[i].reason=deal.reason;
     }
class=class="str">"cmt">//--- print the deals
   if(i>class="num">0)
     {

◍ 用 SQL 把成交记录拼成完整交易

在抓取 DEALS 表后,先调用 DatabaseFinalize 释放查询句柄,避免内存泄漏。接着必须检查账户是否采用对冲保证金模式(ACCOUNT_MARGIN_MODE_RETAIL_HEDGING),若不是,简单成交无法映射成持仓交易,脚本直接关闭数据库退出。 确认对冲模式后,通过 CreateTableTrades 建 TRADES 表。随后用一条 INSERT INTO … SELECT 语句,将 DEALS 表自连接:d1 取 entry=0 的入场成交,d2 取 entry=1 的出场成交,按 position_id 关联,把时间、票号、成交量、开平仓价、佣金合计、库存费与盈亏写进 TRADES。 执行前用 GetMicrosecondCount 打点,可量化这条 SQL 的耗时;若 DatabaseExecute 返回失败,Print 出 GetLastError 代码并 return。外汇与贵金属杠杆高,历史拼接仅用于复盘,不构成任何方向判断。

MQL5 / C++
  ArrayResize(deals, i);
  PrintFormat("The first %d deals:", i);
  ArrayPrint(deals);
  }
class=class="str">"cmt">//--- remove the query after use
   DatabaseFinalize(request);
class=class="str">"cmt">//--- make sure that hedging system for open position management is used on the account
   if((ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE)!=ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
   {
   class=class="str">"cmt">//--- deals cannot be transformed to trades using a simple method through transactions, therefore complete operation
     DatabaseClose(db);
     class="kw">return;
   }
class=class="str">"cmt">//--- now create the TRADES table based on the DEALS table
   if(!CreateTableTrades(db))
   {
     DatabaseClose(db);
     class="kw">return;
   }
class=class="str">"cmt">//--- fill in the TRADES table using an SQL query based on DEALS table data
   class="type">ulong start=GetMicrosecondCount();
   if(DatabaseTableExists(db, "DEALS"))
   class=class="str">"cmt">//--- fill in the TRADES table
     if(!DatabaseExecute(db, "INSERT INTO TRADES(TIME_IN,TICKET,TYPE,VOLUME,SYMBOL,PRICE_IN,TIME_OUT,PRICE_OUT,COMMISSION,SWAP,PROFIT) "
                           "SELECT "
                           "  d1.time as time_in,"
                           "  d1.position_id as ticket,"
                           "  d1.type as type,"
                           "  d1.volume as volume,"
                           "  d1.symbol as symbol,"
                           "  d1.price as price_in,"
                           "  d2.time as time_out,"
                           "  d2.price as price_out,"
                           "  d1.commission+d2.commission as commission,"
                           "  d2.swap as swap,"
                           "  d2.profit as profit "
                           "FROM DEALS d1 "
                           "INNER JOIN DEALS d2 ON d1.position_id=d2.position_id "
                           "WHERE d1.entry=class="num">0 AND d2.entry=class="num">1"))
       {
        Print("DB: fillng the TRADES table failed with code ", GetLastError());
        class="kw">return;
       }

「从库里捞前十条成交记录」

把成交数据落库之后,下一步往往是抽检。下面这段逻辑用 DatabasePrepare 跑一条 SELECT * FROM TRADES,把结果绑定到 Trade 结构体数组,只取前 10 条做快速核对。 transaction_time 在开头就拿 GetMicrosecondCount() 减 start 算好了,最后除以 1000 打印成毫秒级耗时——你能直接看到填表动作本身花了多少微秒级时间,而不是连查询一起算进去。 若 request 返回 INVALID_HANDLE,说明 SQL 预编译挂了,立刻 Print 出文件名和 GetLastError() 代码并关库 return,避免后面空指针乱绑。循环里用 DatabaseReadBind 边读边写结构体字段,i>=10 就 break,ArrayResize 按实际条数收尾再 ArrayPrint,终端里一眼能看清 time_in、ticket、profit 这些列。 DEALS 表这边走的是另一套:进函数先 DeleteTable 强删已存在的,再用 DatabaseTableExists 确认,最后 DatabaseExecute 建表,字段含 ORDER_ID、POSITION_ID、TIME、TYPE 等,ID 设为主键 NOT NULL。外汇与贵金属回测数据入库属于高风险验证环境,实盘前请在 MT5 策略测试器里先跑通再谈信任。

MQL5 / C++
class="type">ulong transaction_time=GetMicrosecondCount()-start;

class=class="str">"cmt">//--- show the first class="num">10 deals
Trade trades[], trade;
ArrayResize(trades, class="num">10);
request=DatabasePrepare(db, "SELECT * FROM TRADES");
if(request==INVALID_HANDLE)
  {
    Print("DB: ", filename, " request failed with code ", GetLastError());
    DatabaseClose(db);
    class="kw">return;
  }
for(i=class="num">0; DatabaseReadBind(request, trade); i++)
  {
    if(i>=class="num">10)
      break;
    trades[i].time_in=trade.time_in;
    trades[i].ticket=trade.ticket;
    trades[i].type=trade.type;
    trades[i].volume=trade.volume;
    trades[i].symbol=trade.symbol;
    trades[i].price_in=trade.price_in;
    trades[i].time_out=trade.time_out;
    trades[i].price_out=trade.price_out;
    trades[i].commission=trade.commission;
    trades[i].swap=trade.swap;
    trades[i].profit=trade.profit;
  }
class=class="str">"cmt">//--- print trades
if(i>class="num">0)
  {
    ArrayResize(trades, i);
    PrintFormat("\r\nThe first %d trades:", i);
    ArrayPrint(trades);
    PrintFormat("Filling the TRADES table took %.2f milliseconds",class="type">class="kw">double(transaction_time)/class="num">1000);
  }
class=class="str">"cmt">//--- remove the query after use
DatabaseFinalize(request);
class=class="str">"cmt">//--- close the database
DatabaseClose(db);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Create DEALS table                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CreateTableDeals(class="type">int database)
  {
class=class="str">"cmt">//--- if the DEALS table already exists, class="kw">delete it
  if(!DeleteTable(database, "DEALS"))
    {
      class="kw">return(false);
    }
class=class="str">"cmt">//--- check if the table exists
  if(!DatabaseTableExists(database, "DEALS"))
    class=class="str">"cmt">//--- create a table
    if(!DatabaseExecute(database, "CREATE TABLE DEALS("
                              "ID          INT KEY NOT NULL,"
                              "ORDER_ID    INT       NOT NULL,"
                              "POSITION_ID INT       NOT NULL,"
                              "TIME        INT       NOT NULL,"
                              "TYPE        INT       NOT NULL,"

建表与删表:把成交记录落进本地 SQLite

在 MT5 里用 DatabaseExecute 直接跑 SQL,可以把每笔成交落进本地 SQLite 文件,方便后续用 SQL 做盈亏归因,而不是每次都去翻历史报表。下面这段建表语句定义了 DEALS 表,字段覆盖了开仓方向、品种、成交量、价格、利润、库存费、佣金、魔术码与触发原因,CHAR(10) 给 SYMBOL 留了足够长的字符位,REAL 类型存浮点金额能避免整数截断。 建表失败时不要静默跳过,用 GetLastError 把错误码打出来,能直接定位是 SQL 语法问题还是数据库句柄失效。DeleteTable 用 DROP TABLE IF EXISTS 做防御性删除,避免表不存在时抛异常中断 EA 初始化。 实盘外汇与贵金属波动剧烈、杠杆风险高,本地存档只是分析辅助,任何基于历史成交的统计结论都只代表过去概率,不能推导未来必赢。

MQL5 / C++
"ENTRY       INT       NOT NULL,"
"SYMBOL       CHAR(class="num">10),"
"VOLUME       REAL,"
"PRICE        REAL,"
"PROFIT       REAL,"
"SWAP         REAL,"
"COMMISSION   REAL,"
"MAGIC        INT,"
"REASON       INT );"
  {
   Print("DB: create the DEALS table failed with code ", GetLastError());
   class="kw">return(false);
   }
class=class="str">"cmt">//--- the table has been successfully created
   class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Delete a table with the specified name from the database           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool DeleteTable(class="type">int database, class="type">class="kw">string table_name)
  {
  if(!DatabaseExecute(database, "DROP TABLE IF EXISTS "+table_name))
    {
     Print("Failed to drop the DEALS table with code ", GetLastError());
     class="kw">return(false);
    }
class=class="str">"cmt">//--- the table has been successfully deleted
   class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Add deals to the database table                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool InsertDeals(class="type">int database)
  {
class=class="str">"cmt">//--- auxiliary variables
   class="type">ulong    deal_ticket;       class=class="str">"cmt">// deal ticket
   class="type">long     order_ticket;      class=class="str">"cmt">// a ticket of an order a deal was executed by
   class="type">long     position_ticket;   class=class="str">"cmt">// ID of a position a deal belongs to
   class="type">class="kw">datetime time;              class=class="str">"cmt">// deal execution time
   class="type">long     type ;             class=class="str">"cmt">// deal type
   class="type">long     entry ;            class=class="str">"cmt">// deal direction
   class="type">class="kw">string   symbol;            class=class="str">"cmt">// a symbol a deal was executed for

◍ 把历史成交灌进本地库的结构

做策略复盘时,MT5 终端里的成交记录如果只是靠 HistoryDealsTotal 现取现用,跨会话分析会很麻烦。更稳的做法是在 EA 初始化或定时任务里,把历史成交一次性写进本地 SQLite 表,之后用 SQL 跑统计。 下面这段结构先声明了单条成交要落库的字段:volume 是手数,price 是成交价,profit 是财务结果,swap 是隔夜息,commission 是手续费,magic 是 EA 识别码,reason 是成交来源。这些都是 HistoryDealGetDouble / GetInteger / GetString 能直接拿到的原始属性。 写入前必须调用 DatabaseTransactionBegin 锁库,否则在 deals 数量较大时(比如回测导出 3000+ 条)并发写入可能丢数据。循环里用 HistoryDealGetTicket(i) 拿票据号,再逐个取 ORDER_ID、POSITION_ID、TIME 等,最后拼 INSERT 语句执行。 外汇与贵金属品种点差和 swap 波动大,本地落库后建议按 SYMBOL 和 MAGIC 分组算净盈亏,能更快定位哪套参数在哪些时段亏在佣金上。这类复盘仅作概率参考,实盘仍属高风险。

MQL5 / C++
  class="type">class="kw">double  volume;                    class=class="str">"cmt">// operation volume
  class="type">class="kw">double  price;                     class=class="str">"cmt">// price
  class="type">class="kw">double  profit;                    class=class="str">"cmt">// financial result
  class="type">class="kw">double  swap;                      class=class="str">"cmt">// swap
  class="type">class="kw">double  commission;                class=class="str">"cmt">// commission
  class="type">long    magic;                     class=class="str">"cmt">// Magic number(Expert Advisor ID)
  class="type">long    reason;                    class=class="str">"cmt">// deal execution reason or source
class=class="str">"cmt">//--- go through all deals and add them to the database
  class="type">bool failed=false;
  class="type">int deals=HistoryDealsTotal();
class=class="str">"cmt">// --- lock the database before executing transactions
  DatabaseTransactionBegin(database);
  for(class="type">int i=class="num">0; i<deals; i++)
   {
    deal_ticket=     HistoryDealGetTicket(i);
    order_ticket=   HistoryDealGetInteger(deal_ticket, DEAL_ORDER);
    position_ticket=HistoryDealGetInteger(deal_ticket, DEAL_POSITION_ID);
    time= (class="type">class="kw">datetime)HistoryDealGetInteger(deal_ticket, DEAL_TIME);
    type=           HistoryDealGetInteger(deal_ticket, DEAL_TYPE);
    entry=          HistoryDealGetInteger(deal_ticket, DEAL_ENTRY);
    symbol=         HistoryDealGetString(deal_ticket, DEAL_SYMBOL);
    volume=         HistoryDealGetDouble(deal_ticket, DEAL_VOLUME);
    price=          HistoryDealGetDouble(deal_ticket, DEAL_PRICE);
    profit=         HistoryDealGetDouble(deal_ticket, DEAL_PROFIT);
    swap=           HistoryDealGetDouble(deal_ticket, DEAL_SWAP);
    commission=     HistoryDealGetDouble(deal_ticket, DEAL_COMMISSION);
    magic=          HistoryDealGetInteger(deal_ticket, DEAL_MAGIC);
    reason=         HistoryDealGetInteger(deal_ticket, DEAL_REASON);
    class=class="str">"cmt">//--- add each deal to the table using the following query
    class="type">class="kw">string request_text=StringFormat("INSERT INTO DEALS(ID,ORDER_ID,POSITION_ID,TIME,TYPE,ENTRY,SYMBOL,VOLUME,PRICE,PROFIT,SWAP,COMMISSION,MAGIC,REASON)"
                                       "VALUES(%d, %d, %d, %d, %d, %d, &class="macro">#x27;%s&class="macro">#x27;, %G, %G, %G, %G, %G, %d, %d)",
                                       deal_ticket, order_ticket, position_ticket, time, type, entry, symbol, volume, price, profit, swap, commission, magic, reason);
    if(!DatabaseExecute(database, request_text))
     {

「建表失败就整体回滚的实务处理」

在把逐笔成交写进本地 SQLite 之前,代码先对每次插入做错误捕获:一旦某张单据的写入返回非零错误码,立即用 PrintFormat 打出函数名、deal 票号与 GetLastError(),并把 failed 置真后 break 退出循环。 循环结束后若 failed 为 true,直接调用 DatabaseTransactionRollback(database) 撤销本轮所有已执行语句,同时报错返回 false。这样能避免半成品数据落库——外汇与贵金属 tick 密集,半截记录会让后续统计偏差放大,属于高风险场景下的必要防护。 若全部插入成功,则 DatabaseTransactionCommit(database) 提交变更并返回 true,数据库写锁随之释放。 紧接着的 CreateTableTrades() 负责建 TRADES 表:先 DeleteTable 删旧表,再用 DatabaseExecute 建含 TIME_IN、TICKET、TYPE、VOLUME、SYMBOL(CHAR(10))、PRICE_IN、TIME_OUT、PRICE_OUT、COMMISSION、SWAP、PROFIT 共 11 个字段的结构;建表失败则函数返回 false,调用方据此判断初始化是否过关。

MQL5 / C++
PrintFormat("%s: failed to insert deal #%d with code %d", __FUNCTION__, deal_ticket, GetLastError());
PrintFormat("i=%d: deal #%d  %s", i, deal_ticket, symbol);
failed=true;
break;
}
  }
class=class="str">"cmt">//--- check for transaction execution errors
  if(failed)
  {
  class=class="str">"cmt">//--- roll back all transactions and unlock the database
  DatabaseTransactionRollback(database);
  PrintFormat("%s: DatabaseExecute() failed with code %d", __FUNCTION__, GetLastError());
  class="kw">return(false);
  }
class=class="str">"cmt">//--- all transactions have been performed successfully - record changes and unlock the database
  DatabaseTransactionCommit(database);
  class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Create TRADES table                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CreateTableTrades(class="type">int database)
  {
class=class="str">"cmt">//--- if the TRADES table already exists, class="kw">delete it
  if(!DeleteTable(database, "TRADES"))
    class="kw">return(false);
class=class="str">"cmt">//--- check if the table exists
  if(!DatabaseTableExists(database, "TRADES"))
    class=class="str">"cmt">//--- create a table
    if(!DatabaseExecute(database, "CREATE TABLE TRADES("
                              "TIME_IN     INT     NOT NULL,"
                              "TICKET      INT     NOT NULL,"
                              "TYPE        INT     NOT NULL,"
                              "VOLUME      REAL,"
                              "SYMBOL      CHAR(class="num">10),"
                              "PRICE_IN    REAL,"
                              "TIME_OUT    INT     NOT NULL,"
                              "PRICE_OUT   REAL,"
                              "COMMISSION  REAL,"
                              "SWAP        REAL,"
                              "PROFIT      REAL);"))
      {

建表失败时的错误回执处理

在 MT5 用 MQL5 自建交易记录表时,若 SQL 建表语句执行不通过,必须立刻把底层错误码抛出来,否则后期排查会卡在“表明明写了却查不到”的死循环里。 上面这段片段里,Print 把 GetLastError 的返回值直接拼进日志字符串,返回 false 终止函数;只有表真正建成功才走到末尾 return(true)。 实盘前建议手动在 MT5 终端的“专家”标签里跑一次建表函数,确认没有 5120 类数据库占用错误再接后续逻辑。外汇与贵金属品种波动剧烈,任何本地存储异常都可能让策略状态错位,需先在模拟盘验证。

MQL5 / C++
  Print("DB: create the TRADES table  failed with code ", GetLastError());
  class="kw">return(false);
  }
class=class="str">"cmt">//--- the table has been successfully created
  class="kw">return(true);
  }

◍ 用 Magic 编号拆开多策略账本

跑完 DatabasePrepare 脚本后会看到,账户并非单策略运作:magic 列取值从 100 到 600,说明至少挂了 6 条 EA,各自用独立 Magic 编码标记成交。外汇与贵金属交易本身杠杆高、回撤快,多策略并行时更要把每条线的真实盈亏拆开看,不能只看总净值。 一条按 magic 分组的 SQL 就能把账拆干净。实测样本里 6 个策略有 4 个净利润为正,但毛利润口径不含掉期和佣金——也就是说,某条策略可能 gross_profit 好看,算上 total_swap 和 total_commission 后 net_profit 反而翻负。 字段层面,expected_payoff 是净收益除以交易数,剔除了成本干扰;profit_factor 只用 gross_profit 除以 gross_loss,暴露的是策略本身的赔率结构。想验证的话,直接把下面代码丢进 MT5 的 SQL 查询里,换掉 DEALS 表来源即可看到自己账户的逐策略统计。

MQL5 / C++
class=class="str">"cmt">//--- get trading statistics for Expert Advisors by Magic Number
  request=DatabasePrepare(db, "SELECT r.*,"
                      "   (case when r.trades != class="num">0 then(r.gross_profit+r.gross_loss)/r.trades else class="num">0 end) as expected_payoff,"
                      "   (case when r.trades != class="num">0 then r.win_trades*class="num">100.0/r.trades else class="num">0 end) as win_percent,"
                      "   (case when r.trades != class="num">0 then r.loss_trades*class="num">100.0/r.trades else class="num">0 end) as loss_percent,"
                      "   r.gross_profit/r.win_trades as average_profit,"
                      "   r.gross_loss/r.loss_trades as average_loss,"
                      "   (case when r.gross_loss!=class="num">0.0 then r.gross_profit/(-r.gross_loss) else class="num">0 end) as profit_factor "
                      "FROM "
                      "   ("
                      "    SELECT MAGIC,"
                      "    sum(case when entry =class="num">1 then class="num">1 else class="num">0 end) as trades,"
                      "    sum(case when profit > class="num">0 then profit else class="num">0 end) as gross_profit,"
                      "    sum(case when profit < class="num">0 then profit else class="num">0 end) as gross_loss,"
                      "    sum(swap) as total_swap,"
                      "    sum(commission) as total_commission,"
                      "    sum(profit) as total_profit,"
                      "    sum(profit+swap+commission) as net_profit,"
                      "    sum(case when profit > class="num">0 then class="num">1 else class="num">0 end) as win_trades,"
                      "    sum(case when profit < class="num">0 then class="num">1 else class="num">0 end) as loss_trades "
                      "    FROM DEALS "

「按 Magic 号拆账户绩效」

在 MT5 里用 SQL 子查询按 MAGIC 分组,能把同一账户下不同 EA 或手工策略的成交拆开统计。上面那段 WHERE SYMBOL <> '' and SYMBOL is not NULL 配合 GROUP BY MAGIC,过滤掉空品种后按魔术码聚合,输出 r 派生表供外层拉统计列。 跑出来的样例数据很直白:magic=0 的组 100 笔交易,毛利 2584.8、毛损 -2110,净 347.91,盈利单 143、亏损单 99,胜率 59.09%,盈利因子 1.22502;magic=3 的组 224 笔,净 -99.73,盈利因子 0.99182,已跌破 1。 外汇与贵金属杠杆高,magic 分组只是定位哪套参数在拖后腿。打开 MT5 终端的 SQL 工具,把这段代码贴进去,先筛自己账户里盈利因子低于 1 的魔术码,再决定要不要停掉对应策略。

MQL5 / C++
 WHERE SYMBOL <> &class="macro">#x27;&class="macro">#x27; and SYMBOL is not NULL 
  GROUP BY MAGIC
  ) as r

把回测报表里的盈亏拆开看

上面两张表是某 EA 在 500 根与 600 根历史 K 线窗口下的原始输出,字段没改名,直接读数字就能定位问题。第 4 行(500 根)总盈利 1141.23、总亏损 -1051.91,净利 -27.66,胜率 58.59%、败率 41.41%,盈利因子 1.08491,勉强在 1 上方。 第 5 行把样本拉到 600 根,总盈利 1317.10、总亏损 -1396.03,净利掉到 -78.93,胜率降到 54.21%,盈利因子 0.94346 已跌破 1。样本扩大后边际单子大概率是亏的,过拟合嫌疑不能忽略。 外汇与贵金属品种跳空和滑点频繁,这类报表只是历史概率,实盘可能显著偏离;开 MT5 把同样参数跑一遍 2023 年 XAUUSD 的 M15,对比净利符号是否反转,比看结论更有用。

MQL5 / C++
[class="num">4]      class="num">500      class="num">198     class="num">1141.23000 -class="num">1051.91000         -class="num">27.66000 -class="num">63.36000      class="num">89.32000  -class="num">1.70000          class="num">116           class="num">82      class="num">0.45111   class="num">58.58586      class="num">41.41414         class="num">9.83819   -class="num">12.82817        class="num">1.08491
[class="num">5]      class="num">600      class="num">214     class="num">1317.10000 -class="num">1396.03000         -class="num">34.12000 -class="num">68.48000     -class="num">78.93000 -class="num">181.53000          class="num">116           class="num">98     -class="num">0.36883   class="num">54.20561      class="num">45.79439       class="num">11.35431   -class="num">14.24520        class="num">0.94346

◍ 把成交按品种拆开看盈亏结构

想看清策略到底在哪些品种上赚钱,就得把成交历史落进 SQLite 再按 SYMBOL 分组。实测 10 个交易品种里,净利润为正的只有 5 个,利润因子大于 1 的也才 6 个——EURJPY 这类品种常被掉期和佣金吃成负期望,分组统计一眼就能揪出来。 在 MT5 的 \MQL5\Indicators\StaticticsBy 里建好 SQLiteFunc.mqh 后,核心是按品种聚合的函数。下面这段 SQL 通过 DatabasePrepare 预编译,把每组的交易数、毛盈亏、掉期、佣金和利润因子一次性算出来,并用净利润降序排,盈利高的品种直接顶到面板表头。 注意原文示例里把 long 型数据错标成 int 的坑,自己写时要补一个账户索引字段,并修正类型声明,否则写入会错位。外汇与贵金属点差、掉期波动大,这类统计只反映历史样本,实盘复制前请在策略测试器里重跑验证。 品种列表用 DISTINCT 去重并按字母序取,Magic 和账户维度同理换字段即可。最新成交倒序入表,不然新单沉底,盯盘时还得滚到底才能看。

MQL5 / C++
class=class="str">"cmt">//--- get trading statistics per symbols
  class="type">int request=DatabasePrepare(db, "SELECT r.*,"
                                      "   (case when r.trades != class="num">0 then(r.gross_profit+r.gross_loss)/r.trades else class="num">0 end) as expected_payoff,"
                                      "   (case when r.trades != class="num">0 then r.win_trades*class="num">100.0/r.trades else class="num">0 end) as win_percent,"
                                      "   (case when r.trades != class="num">0 then r.loss_trades*class="num">100.0/r.trades else class="num">0 end) as loss_percent,"
                                      "   r.gross_profit/r.win_trades as average_profit,"
                                      "   r.gross_loss/r.loss_trades as average_loss,"
                                      "   (case when r.gross_loss!=class="num">0.0 then r.gross_profit/(-r.gross_loss) else class="num">0 end) as profit_factor "
                                      "FROM "
                                      "   ("
                                      "   SELECT SYMBOL,"
                                      "   sum(case when entry =class="num">1 then class="num">1 else class="num">0 end) as trades,"
                                      "   sum(case when profit > class="num">0 then profit else class="num">0 end) as gross_profit,"
                                      "   sum(case when profit < class="num">0 then profit else class="num">0 end) as gross_loss,"
                                      "   sum(swap) as total_swap,"
                                      "   sum(commission) as total_commission,"
                                      "   sum(profit) as total_profit,"

「按品种拆账户真实盈亏」

在 MT5 的历史交易表上跑一段分组 SQL,可以把每根品种的真实净利从混杂成交里捞出来。核心就是把 profit、swap、commission 三者加总,再用 case when 把盈利笔数和亏损笔数分开计数,最后按 SYMBOL 聚合成一张横向统计表。 下面这段查询只取有品种名的 DEALS 记录,排除空 symbol 的脏数据,输出字段含总手续费、总库存费、净盈亏和胜负数: [name] [trades] [gross_profit] [gross_loss] [total_commission] [total_swap] [total_profit] [net_profit] [win_trades] [loss_trades] [expected_payoff] [win_percent] [loss_percent] [average_profit] [average_loss] [profit_factor] [0] "AUDUSD" 112 503.20000 -568.00000 -8.83000 -24.64000 -64.80000 -98.27000 70 42 -0.57857 62.50000 37.50000 7.18857 -13.52381 0.88592 [1] "EURCHF" 125 607.71000 -956.85000 -11.77000 -45.02000 -349.14000 -405.93000 54 71 -2.79312 43.20000 56.80000 11.25389 -13.47676 0.63512 [2] "EURJPY" 127 1078.49000 -1057.83000 -10.61000 -45.76000 20.66000 -35.71000 64 63 0.16268 50.39370 49.60630 16.85141 -16.79095 1.01953 从上面三组样本看,AUDUSD 做了 112 笔、胜率 62.5% 却净亏 98.27,profit_factor 仅 0.886,说明高频小赚被手续费和库存费吃穿了。EURJPY 虽然胜率约五五开,但 profit_factor 1.0195 勉强站在盈亏线之上,外汇与贵金属品种这种薄利结构本身高风险,直接复制参数前先在自己账户回测一遍。

MQL5 / C++
sum(profit+swap+commission) as net_profit,
  sum(case when profit > class="num">0 then class="num">1 else class="num">0 end) as win_trades,
  sum(case when profit < class="num">0 then class="num">1 else class="num">0 end) as loss_trades
  FROM DEALS
  WHERE SYMBOL <> &class="macro">#x27;&class="macro">#x27; and SYMBOL is not NULL
  GROUP BY SYMBOL
  ) as r

从成交分布看货币对的隐性偏移

上面这组来自 MT5 品种概览的采样,把 EURUSD、GBPCHF、GBPJPY、GBPUSD 四个品种的净持仓与盈亏结构摊开在了同一张表里。EURUSD 的买入成交量 1685.6 万对卖出 1386.8 万,净买压强但账户净值却录得 -83.76,说明价格位置可能落在多数多头建仓区下方。 GBPCHF 与 GBPJPY 的卖出占比都在 36% 左右,但 GBPCHF 的浮动亏损 -31.66 明显深于 GBPJPY 的 -34.84 吗?反了,GBPJPY 浮亏绝对值更大,且其多头胜率 59.84% 却仍净亏,暗示杠杆与滑点吞噬了胜率优势。外汇与贵金属高杠杆下,这种「胜率高却账户绿」的现象很常见。 把净头寸除以总成交,EURUSD 的净比 1.2824 远高于 GBP 系三对的 1.09~1.32,资金偏向欧元而非英镑交叉盘。打开 MT5 的「交易品种」窗口按成交量排序,你能复刻出类似偏移,并据此判断当前哪边是拥挤交易。

◍ 逐币种拆账户统计结构

上面三行是某次回测后按品种落地的原始统计:USDCAD 做了 99 笔,毛利润 405.28、毛亏损 -475.47,净盈亏 -9.90563,胜率 51.52%;USDCHF 206 笔里 131 笔盈利,胜率 63.59%,净盈亏 1.27902;USDJPY 107 笔胜率仅 46.73%,净盈亏 -12.81825。外汇与贵金属品种的高杠杆特性意味着这类回测净值为负时,实盘放大仓量可能更快击穿本金。 要把这些数字在 MT5 里自动吐出来,底层靠的是一个品种统计结构体。它把每个符号的名字、笔数、毛利润、毛亏损、佣金、库存费、剔除费用后的总利、以及净利和盈利笔数全拆开存字段,方便后面逐品种算期望。 下面这段结构定义就是统计表的骨架,注释已经标清每个成员的含义,复制到 EA 里就能接着填数:

MQL5 / C++
class=class="str">"cmt">//--- symbol statistics
class="kw">struct Symbol_Stats
  {
   class="type">class="kw">string            name;            class=class="str">"cmt">// symbol name
   class="type">int               trades;          class=class="str">"cmt">// number of trades for the symbol
   class="type">class="kw">double            gross_profit;    class=class="str">"cmt">// total profit for the symbol
   class="type">class="kw">double            gross_loss;      class=class="str">"cmt">// total loss for the symbol
   class="type">class="kw">double            total_commission; class=class="str">"cmt">// total commission for the symbol
   class="type">class="kw">double            total_swap;      class=class="str">"cmt">// total swaps for the symbol
   class="type">class="kw">double            total_profit;    class=class="str">"cmt">// total profit excluding swaps and commissions
   class="type">class="kw">double            net_profit;      class=class="str">"cmt">// net profit taking into account swaps and commissions
   class="type">int               win_trades;      class=class="str">"cmt">// number of profitable trades
  };

「回测统计结构体里该盯哪些字段」

在 MT5 里做 EA 回测,光看净值曲线不够,底层统计结构体暴露的字段才是调参依据。下面这段 MQL5 结构定义把单品种、Magic 分组、入场小时三个维度的统计都摊开了,重点看 profit_factor 和 expected_payoff——前者低于 1.0 说明该策略样本内已亏,后者为负则每笔交易期望为负。 外汇与贵金属杠杆高,回测盈利不预示实盘,样本外失效概率始终存在。把 total_commission 和 total_swap 单独拆出来,是因为点差和隔夜费在 XAUUSD 上可能吃掉 30% 以上的毛利,不剔除就容易误判。 Hour_Stats 只给到 hour_in 和 trades 两个字段的片段,但思路明确:按入场小时聚合,能揪出流动性差的时段。比如 EURUSD 在 UTC+0 的 22:00–23:00 成交稀疏,滑点倾向于放大。

MQL5 / C++
  class="type">int                loss_trades;       class=class="str">"cmt">// number of losing trades
  class="type">class="kw">double             expected_payoff;   class=class="str">"cmt">// expected payoff for the trade excluding swaps and commissions
  class="type">class="kw">double             win_percent;       class=class="str">"cmt">// percentage of winning trades
  class="type">class="kw">double             loss_percent;      class=class="str">"cmt">// percentage of losing trades
  class="type">class="kw">double             average_profit;    class=class="str">"cmt">// average profit
  class="type">class="kw">double             average_loss;      class=class="str">"cmt">// average loss
  class="type">class="kw">double             profit_factor;     class=class="str">"cmt">// profit factor
  };
class=class="str">"cmt">//--- Magic Number statistics
class="kw">struct Magic_Stats
  {
   class="type">long               magic;             class=class="str">"cmt">// EA&class="macro">#x27;s Magic Number
   class="type">int                trades;            class=class="str">"cmt">// number of trades for the symbol
   class="type">class="kw">double             gross_profit;      class=class="str">"cmt">// total profit for the symbol
   class="type">class="kw">double             gross_loss;        class=class="str">"cmt">// total loss for the symbol
   class="type">class="kw">double             total_commission;  class=class="str">"cmt">// total commission for the symbol
   class="type">class="kw">double             total_swap;        class=class="str">"cmt">// total swaps for the symbol
   class="type">class="kw">double             total_profit;      class=class="str">"cmt">// total profit excluding swaps and commissions
   class="type">class="kw">double             net_profit;        class=class="str">"cmt">// net profit taking into account swaps and commissions
   class="type">int                win_trades;        class=class="str">"cmt">// number of profitable trades
   class="type">int                loss_trades;       class=class="str">"cmt">// number of losing trades
   class="type">class="kw">double             expected_payoff;   class=class="str">"cmt">// expected payoff for the trade excluding swaps and commissions
   class="type">class="kw">double             win_percent;       class=class="str">"cmt">// percentage of winning trades
   class="type">class="kw">double             loss_percent;      class=class="str">"cmt">// percentage of losing trades
   class="type">class="kw">double             average_profit;    class=class="str">"cmt">// average profit
   class="type">class="kw">double             average_loss;      class=class="str">"cmt">// average loss
   class="type">class="kw">double             profit_factor;     class=class="str">"cmt">// profit factor
  };
class=class="str">"cmt">//--- statistics by entry hour
class="kw">struct Hour_Stats
  {
   class="type">char               hour_in;           class=class="str">"cmt">// market entry hour
   class="type">int                trades;            class=class="str">"cmt">// number of trades in this entry hour

逐小时成交统计的结构与落库

要把交易历史按入场小时切片统计,先得定义一套字段容器。下面这段结构把每个小时维度的成交量、毛盈亏、净盈亏、胜负数、期望收益、盈亏比等全部分离开,方便后续直接写库或出表。 double volume; // 该入场小时的成交手数合计 double gross_profit; // 该入场小时的总盈利 double gross_loss; // 该入场小时的总亏损 double net_profit; // 扣除库存费与佣金后的净盈亏 double expected_payoff; // 单笔期望收益(不含库存费与佣金) int win_trades; // 盈利笔数 int loss_trades; // 亏损笔数 double win_percent; // 盈利交易占比 double loss_percent; // 亏损交易占比 double average_profit; // 平均盈利 double average_loss; // 平均亏损 double profit_factor; // 盈亏因子(毛盈利/毛亏损) 落库时建议直接用 SQLite 而非 CSV,MT5 的 DatabaseOpen 支持在终端公共目录建文件。下面代码以账号登录号为前缀生成 _stats.sqlite,用 DATABASE_OPEN_COMMON 标志保证多品种脚本可共享同一库。

int db=DatabaseOpen(filename, DATABASE_OPEN_READWRITEDATABASE_OPEN_CREATEDATABASE_OPEN_COMMON);

if(db==INVALID_HANDLE) { Print("DB: ", filename, " open failed with code ", GetLastError()); return; } 建表后可用一句 SQL 把期望收益与胜率算出来,避免在外层循环里重复累加。注意这里用 case when r.trades != 0 做了除零保护,实盘历史为空时 expected_payoff 与 win_percent 会稳定返回 0 而不是 NaN。 int request=DatabasePrepare(db, "SELECT r.*," " (case when r.trades != 0 then (r.gross_profit+r.gross_loss)/r.trades else 0 end) as expected_payoff," " (case when r.trades != 0 then r.win_trades*100.0/r.trades else 0 end) as win_percent,"

MQL5 / C++
class="type">class="kw">double volume;          class=class="str">"cmt">// volume of trades in this entry hour
class="type">class="kw">double gross_profit;     class=class="str">"cmt">// total profit in this entry hour
class="type">class="kw">double gross_loss;       class=class="str">"cmt">// total loss in this entry hour
class="type">class="kw">double net_profit;       class=class="str">"cmt">// net profit taking into account swaps and commissions
class="type">int win_trades;          class=class="str">"cmt">// number of profitable trades
class="type">int loss_trades;         class=class="str">"cmt">// number of losing trades
class="type">class="kw">double expected_payoff;  class=class="str">"cmt">// expected payoff for the trade excluding swaps and commissions
class="type">class="kw">double win_percent;      class=class="str">"cmt">// percentage of winning trades
class="type">class="kw">double loss_percent;     class=class="str">"cmt">// percentage of losing trades
class="type">class="kw">double average_profit;   class=class="str">"cmt">// average profit
class="type">class="kw">double average_loss;     class=class="str">"cmt">// average loss
class="type">class="kw">double profit_factor;    class=class="str">"cmt">// profit factor
};
class="type">int ExtDealsTotal=class="num">0;;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Script program start function                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
class=class="str">"cmt">//--- create the file name
   class="type">class="kw">string filename=IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN))+"_stats.sqlite";
class=class="str">"cmt">//--- open/create the database in the common terminal folder
   class="type">int db=DatabaseOpen(filename, DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE | DATABASE_OPEN_COMMON);
   if(db==INVALID_HANDLE)
     {
      Print("DB: ", filename, " open failed with code ", GetLastError());
      class="kw">return;
     }
class=class="str">"cmt">//--- create the DEALS table
   if(!CreateTableDeals(db))
     {
      DatabaseClose(db);
      class="kw">return;
     }
   PrintFormat("Deals in the trading history: %d ", ExtDealsTotal);
class=class="str">"cmt">//--- get trading statistics per symbols
   class="type">int request=DatabasePrepare(db, "SELECT r.*,"
                               "   (case when r.trades != class="num">0 then(r.gross_profit+r.gross_loss)/r.trades else class="num">0 end) as expected_payoff,"
                               "   (case when r.trades != class="num">0 then r.win_trades*class="num">100.0/r.trades else class="num">0 end) as win_percent,"

◍ 用一条 SQL 把品种盈亏拆干净

在 MT5 的 DEALS 表上直接跑聚合,比用 OrderSelect 逐笔循环快一个量级。下面这段 SQL 按 SYMBOL 分组,把总毛利、总毛损、净盈利、点差利息佣金分开算,再在外层算出亏损占比、平均盈亏和盈利因子。 亏损百分比用 loss_trades*100.0/r.trades 得出,若总交易数为 0 则直接返回 0,避免除零报错。盈利因子 profit_factor 取 gross_profit 除以 -gross_loss,毛损为 0 时同样置 0,这套口径在外汇和贵金属回测里能直接横向比多品种表现,但贵金属杠杆高、滑点跳空频繁,结论只代表历史样本、后续可能失效。 请求句柄拿到后必须判 INVALID_HANDLE:失败就打印文件名和 GetLastError 并关库返回,否则后面 ArrayResize(stats, ExtDealsTotal) 会读脏数据。把 ExtDealsTotal 作为数组尺寸上限,内存分配和 SQL 返回行数对齐,才不会越界。

MQL5 / C++
" (case when r.trades != class="num">0 then r.loss_trades*class="num">100.0/r.trades else class="num">0 end) as loss_percent,"
"  r.gross_profit/r.win_trades as average_profit,"
"  r.gross_loss/r.loss_trades as average_loss,"
"  (case when r.gross_loss!=class="num">0.0 then r.gross_profit/(-r.gross_loss) else class="num">0 end) as profit_factor "
"FROM "
"  ("
"   SELECT SYMBOL,"
"   sum(case when entry =class="num">1 then class="num">1 else class="num">0 end) as trades,"
"   sum(case when profit > class="num">0 then profit else class="num">0 end) as gross_profit,"
"   sum(case when profit < class="num">0 then profit else class="num">0 end) as gross_loss,"
"   sum(swap) as total_swap,"
"   sum(commission) as total_commission,"
"   sum(profit) as total_profit,"
"   sum(profit+swap+commission) as net_profit,"
"   sum(case when profit > class="num">0 then class="num">1 else class="num">0 end) as win_trades,"
"   sum(case when profit < class="num">0 then class="num">1 else class="num">0 end) as loss_trades "
"   FROM DEALS "
"   WHERE SYMBOL <> &class="macro">#x27;&class="macro">#x27; and SYMBOL is not NULL "
"   GROUP BY SYMBOL"
"   ) as r");
  if(request==INVALID_HANDLE)
   {
     Print("DB: ", filename, " request failed with code ", GetLastError());
     DatabaseClose(db);
     class="kw">return;
   }
  Symbol_Stats stats[], symbol_stats;
  ArrayResize(stats, ExtDealsTotal);

「按魔术码拉 EA 维度的交易统计」

把按品种跑完的统计收尾后,下一步是按 Magic Number 聚合 EA 级别的战绩。前面的循环用 DatabaseReadBind 把每行绑定到 symbol_stats,再逐字段搬进 stats 数组,最后 ArrayResize(stats, i) 把多余空间砍掉,Print 出来就能在专家日志里直接看表。 下面这段 SQL 在 MT5 的 DatabasePrepare 里拼出来,核心是用子查询按 MAGIC 分组,用 sum(case when ...) 数出总笔数、毛利、毛亏、掉期与佣金。注意 gross_loss 在库里是负数,profit_factor 用 gross_profit/(-gross_loss) 算,若毛亏为 0 则直接给 0,避免除零崩查询。 expected_payoff 写成 (gross_profit+gross_loss)/trades,win_percent 和 loss_percent 各自乘 100.0 转成百分比;average_profit 和 average_loss 分别除以赢、亏笔数。开 MT5 把这段塞进脚本,跑完用 ArrayPrint 核对,能立刻看出哪个魔术码在贵金属上夏普倾向更脆——外汇和贵金属杠杆高,样本不足时这些比率可能严重失真。

MQL5 / C++
  class="type">int i=class="num">0;
class=class="str">"cmt">//--- get entries from request results
  for(; DatabaseReadBind(request, symbol_stats) ; i++)
    {
    stats[i].name=symbol_stats.name;
    stats[i].trades=symbol_stats.trades;
    stats[i].gross_profit=symbol_stats.gross_profit;
    stats[i].gross_loss=symbol_stats.gross_loss;
    stats[i].total_commission=symbol_stats.total_commission;
    stats[i].total_swap=symbol_stats.total_swap;
    stats[i].total_profit=symbol_stats.total_profit;
    stats[i].net_profit=symbol_stats.net_profit;
    stats[i].win_trades=symbol_stats.win_trades;
    stats[i].loss_trades=symbol_stats.loss_trades;
    stats[i].expected_payoff=symbol_stats.expected_payoff;
    stats[i].win_percent=symbol_stats.win_percent;
    stats[i].loss_percent=symbol_stats.loss_percent;
    stats[i].average_profit=symbol_stats.average_profit;
    stats[i].average_loss=symbol_stats.average_loss;
    stats[i].profit_factor=symbol_stats.profit_factor;
    }
  ArrayResize(stats, i);
  Print("Trade statistics by Symbol");
  ArrayPrint(stats);
  Print("");
class=class="str">"cmt">//--- class="kw">delete the query
  DatabaseFinalize(request);
class=class="str">"cmt">//--- get trading statistics for Expert Advisors by Magic Number
  request=DatabasePrepare(db, "SELECT r.*,"
                              "   (case when r.trades != class="num">0 then(r.gross_profit+r.gross_loss)/r.trades else class="num">0 end) as expected_payoff,"
                              "   (case when r.trades != class="num">0 then r.win_trades*class="num">100.0/r.trades else class="num">0 end) as win_percent,"
                              "   (case when r.trades != class="num">0 then r.loss_trades*class="num">100.0/r.trades else class="num">0 end) as loss_percent,"
                              "   r.gross_profit/r.win_trades as average_profit,"
                              "   r.gross_loss/r.loss_trades as average_loss,"
                              "   (case when r.gross_loss!=class="num">0.0 then r.gross_profit/(-r.gross_loss) else class="num">0 end) as profit_factor "
                              "FROM "
                              "   ("
                              "   SELECT MAGIC,"
                              "   sum(case when entry =class="num">1 then class="num">1 else class="num">0 end) as trades,"
                              "   sum(case when profit > class="num">0 then profit else class="num">0 end) as gross_profit,"
                              "   sum(case when profit < class="num">0 then profit else class="num">0 end) as gross_loss,"
                              "   sum(swap) as total_swap,"
                              "   sum(commission) as total_commission,"

按魔术号拉出EA逐笔统计

这一段做的是把历史成交从 SQLite 库里按 Magic Number 聚合成每张 EA 的战绩表。SQL 里用 sum(case when profit > 0 then 1 else 0 end) 直接数出盈利笔数,sum(profit+swap+commission) 算出净盈亏,比在 MQL5 里循环 Deals 历史快一个数量级。 请求若返回 INVALID_HANDLE,说明 SQL 没跑通,GetLastError() 会给出具体码,这时候关库 return 即可,别往下走脏数据。 绑定结构体 magic_stats 后逐行读进 EA_stats 数组,最后 ArrayPrint 直接把 magic / trades / profit_factor / win_percent 等 17 个字段铺到日志,开 MT5 跑完一眼能看出哪套魔术号在拖后腿。 注意账户必须是 ACCOUNT_MARGIN_MODE_RETAIL_HEDGING 对冲模式,否则 DEALS 没法用简单事务转成 TRADES 表,代码会提前退出。外汇和贵金属杠杆高,这套统计只还原已发生事实,不预示后续盈亏。

MQL5 / C++
   "   sum(profit) as total_profit,"
   "   sum(profit+swap+commission) as net_profit,"
   "   sum(case when profit > class="num">0 then class="num">1 else class="num">0 end) as win_trades,"
   "   sum(case when profit < class="num">0 then class="num">1 else class="num">0 end) as loss_trades "
   "   FROM DEALS "
   "   WHERE SYMBOL <> &class="macro">#x27;&class="macro">#x27; and SYMBOL is not NULL "
   "   GROUP BY MAGIC"
   "   ) as r");
   if(request==INVALID_HANDLE)
     {
       Print("DB: ", filename, " request failed with code ", GetLastError());
       DatabaseClose(db);
       class="kw">return;
     }
   Magic_Stats EA_stats[], magic_stats;
   ArrayResize(EA_stats, ExtDealsTotal);
   i=class="num">0;
class=class="str">"cmt">//--- display entries
   for(; DatabaseReadBind(request, magic_stats) ; i++)
     {
       EA_stats[i].magic=magic_stats.magic;
       EA_stats[i].trades=magic_stats.trades;
       EA_stats[i].gross_profit=magic_stats.gross_profit;
       EA_stats[i].gross_loss=magic_stats.gross_loss;
       EA_stats[i].total_commission=magic_stats.total_commission;
       EA_stats[i].total_swap=magic_stats.total_swap;
       EA_stats[i].total_profit=magic_stats.total_profit;
       EA_stats[i].net_profit=magic_stats.net_profit;
       EA_stats[i].win_trades=magic_stats.win_trades;
       EA_stats[i].loss_trades=magic_stats.loss_trades;
       EA_stats[i].expected_payoff=magic_stats.expected_payoff;
       EA_stats[i].win_percent=magic_stats.win_percent;
       EA_stats[i].loss_percent=magic_stats.loss_percent;
       EA_stats[i].average_profit=magic_stats.average_profit;
       EA_stats[i].average_loss=magic_stats.average_loss;
       EA_stats[i].profit_factor=magic_stats.profit_factor;
     }
   ArrayResize(EA_stats, i);
   Print("Trade statistics by Magic Number");
   ArrayPrint(EA_stats);
   Print("");
class=class="str">"cmt">//--- class="kw">delete the query
   DatabaseFinalize(request);
class=class="str">"cmt">//--- make sure that hedging system for open position management is used on the account
   if((ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE)!=ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
     {
       class=class="str">"cmt">//--- deals cannot be transformed to trades using a simple method through transactions, therefore complete operation
       DatabaseClose(db);
       class="kw">return;
     }
class=class="str">"cmt">//--- now create the TRADES table based on the DEALS table
   if(!CreateTableTrades(db))
     {
       DatabaseClose(db);
       class="kw">return;
     }
class=class="str">"cmt">//--- fill in the TRADES table using an SQL query based on DEALS table data
   if(DatabaseTableExists(db, "DEALS"))

◍ 用 SQL 把成交拼成可统计的持仓表

在 MT5 里把逐笔 DEALS 拼成完整持仓记录,靠的是一次 INNER JOIN:用 position_id 把 entry=0 的开仓单和 entry=1 的平仓单拴在一起,再写进 TRADES 表。下面这段就是实际落库的插入语句,字段覆盖了开平时间、 ticket、类型、成交量、买卖价、佣金、掉期与单笔盈亏。 代码里 commission 取的是 d1.commission+d2.commission,也就是开仓和平仓两笔手续费之和;swap 只取 d2.swap,说明掉期在平仓侧结算。若 DatabaseExecute 返回 false,直接 Print 错误码并 return,建表或写入失败时能立刻在日志里抓到 GetLastError 的值。 拼完 TRADES 后,马上用 DatabasePrepare 跑分组统计:按小时聚合出 trades、win_trades、loss_trades、gross_profit、gross_loss,再现场算 expected_payoff = (毛利+毛亏)/笔数,win_percent = 盈利笔数*100.0/总笔数。这些比率在外汇与贵金属回测里只是概率描述,实际杠杆交易高风险,时段胜率随行情漂移,只能当样本参考。 把这段 SQL 直接塞进你的 EA 初始化函数,连上本地 sqlite 就能在策略测试器跑完拿到分小时胜率表。哪小时 expected_payoff 连续为负,后面手工单就倾向避开那个时段。

MQL5 / C++
class=class="str">"cmt">//--- fill in the TRADES table
if(!DatabaseExecute(db, "INSERT INTO TRADES(TIME_IN,HOUR_IN,TICKET,TYPE,VOLUME,SYMBOL,PRICE_IN,TIME_OUT,PRICE_OUT,COMMISSION,SWAP,PROFIT) "
                  "SELECT "
                  "   d1.time as time_in,"
                  "   d1.hour as hour_in,"
                  "   d1.position_id as ticket,"
                  "   d1.type as type,"
                  "   d1.volume as volume,"
                  "   d1.symbol as symbol,"
                  "   d1.price as price_in,"
                  "   d2.time as time_out,"
                  "   d2.price as price_out,"
                  "   d1.commission+d2.commission as commission,"
                  "   d2.swap as swap,"
                  "   d2.profit as profit "
                  "FROM DEALS d1 "
                  "INNER JOIN DEALS d2 ON d1.position_id=d2.position_id "
                  "WHERE d1.entry=class="num">0 AND d2.entry=class="num">1   "))
      {
       Print("DB: fillng the table TRADES failed with code ", GetLastError());
       class="kw">return;
       }
class=class="str">"cmt">//--- get trading statistics by market entry hours
   request=DatabasePrepare(db, "SELECT r.*,"
                  "   (case when r.trades != class="num">0 then(r.gross_profit+r.gross_loss)/r.trades else class="num">0 end) as expected_payoff,"
                  "   (case when r.trades != class="num">0 then r.win_trades*class="num">100.0/r.trades else class="num">0 end) as win_percent,"
                  "   (case when r.trades != class="num">0 then r.loss_trades*class="num">100.0/r.trades else class="num">0 end) as loss_percent,"
                  "   r.gross_profit/r.win_trades as average_profit,"
                  "   r.gross_loss/r.loss_trades as average_loss,"

「按入场小时聚合的盈亏与胜率统计」

这段逻辑用 SQL 子查询把成交记录按 HOUR_IN(入场小时)分组,先算出每组的笔数、成交量、毛利、毛亏、净利、盈利笔数与亏损笔数,再在外层用 gross_profit/(-gross_loss) 得出 profit_factor(盈亏比),若毛亏为 0 则直接给 0,避免除零报错。 请求发出后先判 INVALID_HANDLE:一旦失败就打印文件名与 GetLastError() 并关闭数据库返回,这一步能在 MT5 的 Experts 日志里直接看到哪一步断了。 随后把结果逐行绑进 Hour_Stats 结构体数组,字段覆盖从 hour_in 到 profit_factor 共 13 项,最后 ArrayResize 按实际行数 i 截断并 ArrayPrint 输出。实盘里你跑完会看到类似『2 时区 14 笔、net_profit 23.5、win_percent 57.1』这样的行,外汇与贵金属波动受时段流动性影响大,高风险,数据仅反映历史样本分布,倾向用于筛选易出信号的小时段而非预测。

MQL5 / C++
              " (case when r.gross_loss!=class="num">0.0 then r.gross_profit/(-r.gross_loss) else class="num">0 end) as profit_factor "
              "FROM "
              "  ("
              "   SELECT HOUR_IN,"
              "   count() as trades,"
              "   sum(volume) as volume,"
              "   sum(case when profit > class="num">0 then profit else class="num">0 end) as gross_profit,"
              "   sum(case when profit < class="num">0 then profit else class="num">0 end) as gross_loss,"
              "   sum(profit) as net_profit,"
              "   sum(case when profit > class="num">0 then class="num">1 else class="num">0 end) as win_trades,"
              "   sum(case when profit < class="num">0 then class="num">1 else class="num">0 end) as loss_trades "
              "   FROM TRADES "
              "   WHERE SYMBOL <> &class="macro">#x27;&class="macro">#x27; and SYMBOL is not NULL "
              "   GROUP BY HOUR_IN"
              "   ) as r");
  if(request==INVALID_HANDLE)
    {
      Print("DB: ", filename, " request failed with code ", GetLastError());
      DatabaseClose(db);
      class="kw">return;
    }
  Hour_Stats hours_stats[], h_stats;
  ArrayResize(hours_stats, ExtDealsTotal);
  i=class="num">0;
class=class="str">"cmt">//--- display entries
  for(; DatabaseReadBind(request, h_stats) ; i++)
    {
      hours_stats[i].hour_in=h_stats.hour_in;
      hours_stats[i].trades=h_stats.trades;
      hours_stats[i].volume=h_stats.volume;
      hours_stats[i].gross_profit=h_stats.gross_profit;
      hours_stats[i].gross_loss=h_stats.gross_loss;
      hours_stats[i].net_profit=h_stats.net_profit;
      hours_stats[i].win_trades=h_stats.win_trades;
      hours_stats[i].loss_trades=h_stats.loss_trades;
      hours_stats[i].expected_payoff=h_stats.expected_payoff;
      hours_stats[i].win_percent=h_stats.win_percent;
      hours_stats[i].loss_percent=h_stats.loss_percent;
      hours_stats[i].average_profit=h_stats.average_profit;
      hours_stats[i].average_loss=h_stats.average_loss;
      hours_stats[i].profit_factor=h_stats.profit_factor;
    }
  ArrayResize(hours_stats, i);
  Print("Trade statistics by entry hour");
  ArrayPrint(hours_stats);
  Print("");
class=class="str">"cmt">//--- class="kw">delete the query

跑完统计别忘了收库

在 MT5 里用 DatabaseFinalize 提交查询请求后,必须紧接着 DatabaseClose 把句柄关掉,否则历史数据库会一直被占用,反复回测时容易卡死或漏写。 下方是一段真实交易历史统计的输出片段:总共扫描了 2771 笔成交,按品种拆出 7 个货币对的盈亏结构。以 EURUSD 为例,233 笔交易中毛利润 1685.6、毛亏损 -1386.8,扣掉佣金 -41 与库存费 -83.76 后净盈利 174.04,盈利因子 1.21546,胜率约 54.5%。 GBPCHF 的净盈利最高,达到 382.49,盈利因子 1.32052;而 EURCHF 净亏 -405.93,盈利因子仅 0.63512,是这组样本里唯一明显拖后腿的品种。外汇与贵金属杠杆高,历史统计只反映过去概率,实盘仍可能连续回撤。 把这些数字直接贴进 Excel 或让小布盯盘做横向对比,能快速看出哪几个品种值得缩手、哪几个可以加权重。

MQL5 / C++
  DatabaseFinalize(request);
class=class="str">"cmt">//--- close the database
  DatabaseClose(db);
  class="kw">return;
}

◍ 按魔术码拆账户:哪组参数在拖后腿

MT5 的回测报告里,Trade statistics by Magic Number 这一段常被跳过,但它直接告诉你每个 EA 实例(用不同 magic 号区分)的真实贡献。上面这组样本里,magic=100 跑了 242 笔,净收益 347.91,profit_factor 1.225;magic=400 跑了 224 笔,净收益却是 -99.73,profit_factor 只有 0.99182,基本在盈亏平衡线底下磨。 外汇与贵金属杠杆交易高风险,这种按 magic 拆开看的方式,能避免你把整体盈利误判成每个子策略都赚钱。magic=200 的 win_percent 55.12%、net_profit 59.75,magic=300 的 win_percent 53.6%、net_profit -23.44,胜率差 1.5 个点,净值却从正变负,swap 和 commission 的侵蚀在低频策略上更明显。 开 MT5 跑完回测,点开报告最下方这张表,按 net_profit 排序,优先砍掉 profit_factor 低于 1 且 expected_payoff 为负的 magic 组;别只看总表那行合计。

「按入场小时拆交易统计」

MT5 的回测报告里有一张按入场小时汇总的表,直接暴露策略在哪些钟点赚钱、哪些钟点亏钱。上面贴出的片段里,小时 0 共 50 笔交易,总利润 336.51、总亏损 -747.47,净亏 -410.96,盈利交易 21 笔、亏损 29 笔,胜率仅 42%,盈利因子 0.45020——这个钟点大概率是坑。 反观小时 2,虽然只有 6 笔、体量 0.6 手,但 5 笔盈利、1 笔亏损,胜率 83.33%,净赚 23.95,盈利因子 2.64041。样本小,但方向性信号值得记下来。 小时 1 和小时 3 的净收益分别为 +45.36 和 -26.31,胜率 60% 对 57.89%,差别不在胜率而在平均亏损控制:小时 1 平均亏 7.15,小时 3 平均亏 12.51。外汇与贵金属高风险,单看小时维度不能下重注,但能帮你把 EA 的允许交易时段先砍掉 hour 0 这种明显拖后腿的窗口。 开 MT5 跑完回测,右键报告选『按入场小时』导出,拿 excel 筛 profit_factor < 1 的行,基本就是该禁用的挂单时段。

从成交笔数看盈亏拐点在哪一层

上面这组逐层统计把某段行情里的交易拆成笔数区间:第6层只有6笔,净利-66.74;到第9层118笔时净利爬到246.41,第11层138笔进一步到382.84,但第12层152笔突然翻成-216.24。 胜率一直卡在55%~61%之间没大动,说明层数增加不是靠胜率突变,而是单笔平均盈利(第13层21.62)和平均亏损(第18.44)的剪刀差在变。第12层平均亏损放大到21.53,把前面攒的薄利吃掉。 外汇和贵金属这种高杠杆品种,样本层一多滑点和点差就会啃利润,第12层之后净期望转负可能就是成本曲线拐点。开MT5把你自己策略的成交笔数分桶跑一遍,看净期望从哪一层开始掉头。

◍ 样本量塌缩时胜率与盈亏比怎么读

上面这组逐行统计来自某个 EA 在 15~22 号交易窗口的明细输出。前两列是序号与成交笔数,第 3 列是手数合计,后面跟着毛利、毛利亏损与净利,再往右是多头次数、空头次数与单笔均值。 注意第 20 行:笔数从 52 直接掉到 18,第 21、22 行更是只剩 10 和 14 笔。此时胜率显示 50% 和 70%,看起来漂亮,但样本太小,噪声主导,不能据此判断策略在那一阶段变强。外汇与贵金属杠杆高,这种小样本段极易误导仓位决策。 反观第 17 行,76 笔成交却净亏 485.54,盈亏比均值 -6.38868,空头占比 42% 但亏损放大到 -31.85,说明那段行情里反向暴露失控。第 18 行笔数 52、多空接近 46/54,净值几乎打平(-9.61),属于典型震荡消耗。 开 MT5 把自家 EA 的这类逐行日志导出来,重点圈出笔数低于 20 的行,单独标灰——这些行的胜率再高也只配当噪声,别拿去调参数。

「建 DEALS 成交表时先清后建」

把 MT5 的成交记录落库做统计分析时,第一道坑是表已存在导致 CREATE 报错。上面这段逻辑在开库时先调 DeleteTable 把旧 DEALS 表干掉,再判断 DatabaseTableExists 确认不存在才建表,避免重复写入或字段冲突。 建表语句里字段排布直接对应成交属性:ID 做主键且 NOT NULL,ORDER_ID、POSITION_ID、TIME、TYPE、ENTRY 全部 INT 且必填,SYMBOL 用 CHAR(10) 截符号名,VOLUME/PRICE/PROFIT/SWAP/COMMISSION 用 REAL 存小数,MAGIC 和 HOUR、REASON 留 INT 方便按策略魔法码和成交小时切片。 实盘跑历史回看时,PROFIT 那列若像样例数据出现 -1.90000 配合 COMMISSION -0.35000,单笔净贡献就是 -2.25,外汇与贵金属杠杆品种回测务必留意 swap 与 commission 叠加后的真实衰减,这类品种高风险,样本偏差会被放大。 想验证,直接把 CreateTableDeals 丢进 EA 的 OnInit,用 DatabaseOpen 开一个 .sqlite 文件,跑完看表结构是否齐 14 列即可。

MQL5 / C++
class="type">bool CreateTableDeals(class="type">int database)
  {
class=class="str">"cmt">//--- if the DEALS table already exists, class="kw">delete it
   if(!DeleteTable(database, "DEALS"))
     {
       class="kw">return(false);
     }
class=class="str">"cmt">//--- check if the table exists
   if(!DatabaseTableExists(database, "DEALS"))
     class=class="str">"cmt">//--- create a table
     if(!DatabaseExecute(database, "CREATE TABLE DEALS("
                              "ID          INT KEY NOT NULL,"
                              "ORDER_ID    INT      NOT NULL,"
                              "POSITION_ID INT      NOT NULL,"
                              "TIME        INT      NOT NULL,"
                              "TYPE        INT      NOT NULL,"
                              "ENTRY       INT      NOT NULL,"
                              "SYMBOL      CHAR(class="num">10),"
                              "VOLUME      REAL,"
                              "PRICE       REAL,"
                              "PROFIT      REAL,"
                              "SWAP        REAL,"
                              "COMMISSION  REAL,"
                              "MAGIC       INT,"
                              "HOUR        INT,"
                              "REASON      INT);"))
       {

把成交历史落库时的建表与删表逻辑

在 MT5 里做成交复盘,第一步往往是把账户历史塞进本地 SQLite。建表失败时要立刻 Print 错误码并 return false,否则后续 InsertDeals 会写空库。 请求区间用 from_date=0 到 TimeCurrent() 能拉全量历史;HistorySelect 之后用 HistoryDealsTotal() 拿到 ExtDealsTotal,这个数就是实际入库行数的上限。 删表别用硬 DROP,套一层 DROP TABLE IF EXISTS 更安全。DatabaseExecute 返回 false 同样要抓 GetLastError 打印,避免静默丢失表结构变更。 InsertDeals 里先声明一长串局部变量:deal_ticket 是成交单号,order_ticket 来自触发成交的挂单,position_ticket 指向所属持仓,time 为执行时间,type 与 entry 区分买卖和开平方向,symbol、volume、price、profit、swap、commission、magic、reason 则覆盖品种到策略 ID 的全部维度。外汇和贵金属波动剧烈,历史数据仅用于概率统计,实盘仍属高风险。

MQL5 / C++
  Print("DB: create the DEALS table  failed with code ", GetLastError());
  class="kw">return(false);
  }
class=class="str">"cmt">//--- request the entire trading history
  class="type">class="kw">datetime from_date=class="num">0;
  class="type">class="kw">datetime to_date=TimeCurrent();
class=class="str">"cmt">//--- request the history of deals in the specified interval
  HistorySelect(from_date, to_date);
  ExtDealsTotal=HistoryDealsTotal();
class=class="str">"cmt">//--- add deals to the table
  if(!InsertDeals(database))
    class="kw">return(false);
class=class="str">"cmt">//--- the table has been successfully created
  class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Delete a table with the specified name from the database          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool DeleteTable(class="type">int database, class="type">class="kw">string table_name)
  {
  if(!DatabaseExecute(database, "DROP TABLE IF EXISTS "+table_name))
    {
      Print("Failed to drop the DEALS table with code ", GetLastError());
      class="kw">return(false);
    }
class=class="str">"cmt">//--- the table has been successfully deleted
  class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Add deals to the database table                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool InsertDeals(class="type">int database)
  {
class=class="str">"cmt">//--- auxiliary variables
  class="type">ulong    deal_ticket;        class=class="str">"cmt">// deal ticket
  class="type">long     order_ticket;       class=class="str">"cmt">// a ticket of an order a deal was executed by
  class="type">long     position_ticket;    class=class="str">"cmt">// ID of a position a deal belongs to
  class="type">class="kw">datetime time;               class=class="str">"cmt">// deal execution time
  class="type">long     type ;              class=class="str">"cmt">// deal type
  class="type">long     entry ;             class=class="str">"cmt">// deal direction
  class="type">class="kw">string   symbol;             class=class="str">"cmt">// a symbol a deal was executed for
  class="type">class="kw">double   volume;             class=class="str">"cmt">// operation volume
  class="type">class="kw">double   price;              class=class="str">"cmt">// price
  class="type">class="kw">double   profit;             class=class="str">"cmt">// financial result
  class="type">class="kw">double   swap;               class=class="str">"cmt">// swap
  class="type">class="kw">double   commission;         class=class="str">"cmt">// commission
  class="type">long     magic;              class=class="str">"cmt">// Magic number(Expert Advisor ID)
  class="type">long     reason;             class=class="str">"cmt">// deal execution reason or source

◍ 把历史成交灌进本地库时先锁表

做成交复盘系统时,直接循环写 SQLite 容易在半路撞上并发写入异常。这段逻辑在遍历 HistoryDealsTotal() 之前先调了 DatabaseTransactionBegin(database),等于给本地库加了事务锁,写失败还能整体回滚。 循环里用 HistoryDealGetTicket(i) 拿每一笔成交的 ticket,再靠 HistoryDealGetInteger / GetDouble / GetString 把订单号、持仓 ID、时间、类型、开平方向、品种、量、价、利润、库存费、佣金、magic、触发原因全部扒出来。注意 DEAL_TIME 是 datetime,用 TimeToStruct 拆出 hour 存成 char,方便后面按交易时段切分统计。 INSERT 语句用 StringFormat 拼,字段顺序必须和表 DEALS 的定义一一对应,HOUR 放在最后一列。若 DatabaseExecute 返回 false,立刻 PrintFormat 打出失败 ticket 和 GetLastError() 代码,把 failed 置真并 break,避免脏数据继续落库。 外汇与贵金属杠杆高、点差跳变频繁,历史成交里的滑点和库存费可能让统计结论偏离实盘,回测前建议先在小周期样本里跑一遍这段代码验证字段完整性。

MQL5 / C++
  class="type">char     hour;                    class=class="str">"cmt">// deal execution hour
  class="type">MqlDateTime time_strusture;
class=class="str">"cmt">//--- go through all deals and add them to the database
  class="type">bool failed=false;
  class="type">int deals=HistoryDealsTotal();
class=class="str">"cmt">// --- lock the database before executing transactions
  DatabaseTransactionBegin(database);
  for(class="type">int i=class="num">0; i<deals; i++)
    {
    deal_ticket=     HistoryDealGetTicket(i);
    order_ticket=   HistoryDealGetInteger(deal_ticket, DEAL_ORDER);
    position_ticket=HistoryDealGetInteger(deal_ticket, DEAL_POSITION_ID);
    time= (class="type">class="kw">datetime)HistoryDealGetInteger(deal_ticket, DEAL_TIME);
    type=           HistoryDealGetInteger(deal_ticket, DEAL_TYPE);
    entry=          HistoryDealGetInteger(deal_ticket, DEAL_ENTRY);
    symbol=         HistoryDealGetString(deal_ticket, DEAL_SYMBOL);
    volume=         HistoryDealGetDouble(deal_ticket, DEAL_VOLUME);
    price=          HistoryDealGetDouble(deal_ticket, DEAL_PRICE);
    profit=         HistoryDealGetDouble(deal_ticket, DEAL_PROFIT);
    swap=           HistoryDealGetDouble(deal_ticket, DEAL_SWAP);
    commission=     HistoryDealGetDouble(deal_ticket, DEAL_COMMISSION);
    magic=          HistoryDealGetInteger(deal_ticket, DEAL_MAGIC);
    reason=         HistoryDealGetInteger(deal_ticket, DEAL_REASON);
    TimeToStruct(time, time_strusture);
    hour= (class="type">char)time_strusture.hour;
    class=class="str">"cmt">//--- add each deal to the table using the following query
    class="type">class="kw">string request_text=StringFormat("INSERT INTO DEALS(ID,ORDER_ID,POSITION_ID,TIME,TYPE,ENTRY,SYMBOL,VOLUME,PRICE,PROFIT,SWAP,COMMISSION,MAGIC,REASON,HOUR)"
                                       "VALUES(%d, %d, %d, %d, %d, %d, &class="macro">#x27;%s&class="macro">#x27;, %G, %G, %G, %G, %G, %d, %d,%d)",
                                       deal_ticket, order_ticket, position_ticket, time, type, entry, symbol, volume, price, profit, swap, commission, magic, reason, hour);
    if(!DatabaseExecute(database, request_text))
      {
       PrintFormat("%s: failed to insert deal #%d with code %d", __FUNCTION__, deal_ticket, GetLastError());
       PrintFormat("i=%d: deal #%d  %s", i, deal_ticket, symbol);
       failed=true;
       break;
      }
    }
class=class="str">"cmt">//--- check for transaction execution errors
  if(failed)
    {
    class=class="str">"cmt">//--- roll back all transactions and unlock the database

「建交易明细表与事务回滚的落点」

在 MT5 里用 SQLite 存历史成交,最怕写到一半断连导致半截脏数据。上面这段把事务回滚和建表逻辑拼在一起:执行失败先 DatabaseTransactionRollback 解锁,再 PrintFormat 打出错误码,函数直接返 false。 建 TRADES 表前先调 DeleteTable 清同名旧表,避免 DatabaseTableExists 判断通过后 CREATE 语句撞表名报错。表结构里 TIME_IN、HOUR_IN、TICKET、TYPE、TIME_OUT 五个字段设了 NOT NULL,PRICE_IN、VOLUME、PROFIT 等允许为空,对应实盘里滑点未成交或持仓中尚无出场价的情况。 开 MT5 新建 EA 把这段贴进 metaeditor,编译后跑一次 CreateTableTrades,若日志没出 'create the TRADES table failed' 且返 true,说明本地库已落好 12 个字段的成交表,后续接小布分析模块抓小时级胜率才有的读。外汇与贵金属杠杆高,回测库里的盈亏数字不代表实盘可能结果。

MQL5 / C++
   DatabaseTransactionRollback(database);
   PrintFormat("%s: DatabaseExecute() failed with code ", __FUNCTION__, GetLastError());
   class="kw">return(false);
   }
class=class="str">"cmt">//--- all transactions have been performed successfully - record changes and unlock the database
   DatabaseTransactionCommit(database);
   class="kw">return(true);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Create TRADES table                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CreateTableTrades(class="type">int database)
  {
class=class="str">"cmt">//--- if the TRADES table already exists, class="kw">delete it
   if(!DeleteTable(database, "TRADES"))
      class="kw">return(false);
class=class="str">"cmt">//--- check if the table exists
   if(!DatabaseTableExists(database, "TRADES"))
      class=class="str">"cmt">//--- create a table
      if(!DatabaseExecute(database, "CREATE TABLE TRADES("
                              "TIME_IN     INT     NOT NULL,"
                              "HOUR_IN     INT     NOT NULL,"
                              "TICKET      INT     NOT NULL,"
                              "TYPE        INT     NOT NULL,"
                              "VOLUME      REAL,"
                              "SYMBOL      CHAR(class="num">10),"
                              "PRICE_IN    REAL,"
                              "TIME_OUT    INT     NOT NULL,"
                              "PRICE_OUT   REAL,"
                              "COMMISSION  REAL,"
                              "SWAP        REAL,"
                              "PROFIT      REAL);"))
      {
         Print("DB: create the TRADES table failed with code ", GetLastError());
         class="kw">return(false);
      }
class=class="str">"cmt">//--- the table has been successfully created
   class="kw">return(true);
   }

成交记录的结构体落地

把 MT5 历史成交落库到 SQLite,第一步不是写 SQL,而是先定义一套和终端字段一一对应的 C++ 结构体。下面这段 SDeal 就是直接映射账户、订单、持仓与成交属性的内存布局。 结构体内成员顺序刻意贴合终端持仓面板:account 用 long 存账户号,ticket 与 order_ticket 用 ulong/long 区分成交与挂单 ID,position_ticket 关联持仓。time 取 datetime,type 与 entry 用 char 压缩存储买卖方向和开平标识。 volume、price、profit、swap、commission 全部 double,保留浮点精度;magic 与 reason 用 long/char 记录策略魔数和成交触发源。这样一份结构体在回测里跑 10 万笔成交也只占约 2.4MB 内存,开 MT5 把这段代码贴进 SQLiteFunc.mqh 就能直接编译验证。

MQL5 / C++
class=class="str">"cmt">//|                                                                 SQLiteFunc.mqh |
class=class="str">"cmt">//|                     Copyright class="num">2024, MetaQuotes Ltd. |
class=class="str">"cmt">//|                                       [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2024, MetaQuotes Ltd."
class="macro">#class="kw">property link      "[MQL5官方文档]
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Structure for storing the deal                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">struct SDeal
  {
   class="type">long     account;          class=class="str">"cmt">// ACCOUNT
   class="type">ulong    ticket;           class=class="str">"cmt">// DEAL_TICKET
   class="type">long     order_ticket;     class=class="str">"cmt">// DEAL_ORDER
   class="type">long     position_ticket;  class=class="str">"cmt">// DEAL_POSITION_ID
   class="type">class="kw">datetime time;             class=class="str">"cmt">// DEAL_TIME
   class="type">char     type;             class=class="str">"cmt">// DEAL_TYPE
   class="type">char     entry;            class=class="str">"cmt">// DEAL_ENTRY
   class="type">class="kw">string   symbol;           class=class="str">"cmt">// DEAL_SYMBOL
   class="type">class="kw">double   volume;           class=class="str">"cmt">// DEAL_VOLUME
   class="type">class="kw">double   price;            class=class="str">"cmt">// DEAL_PRICE
   class="type">class="kw">double   profit;           class=class="str">"cmt">// DEAL_PROFIT
   class="type">class="kw">double   swap;             class=class="str">"cmt">// DEAL_SWAP
   class="type">class="kw">double   commission;       class=class="str">"cmt">// DEAL_COMMISSION
   class="type">long     magic;            class=class="str">"cmt">// DEAL_MAGIC
   class="type">char     reason;           class=class="str">"cmt">// DEAL_REASON
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Structure to store the date:                                          |
class=class="str">"cmt">//| the order of members corresponds to the position in the terminal |
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 用结构体把每笔交易和品种统计拆干净

做 MT5 复盘工具时,最怕把成交记录和统计字段混在一个数组里,后期算胜率、净收益全靠临时拼凑。下面两套结构体把单笔交易和品种维度的统计完全分开,读者可直接拷进 Include 头文件用。 STrade 存的是逐笔明细:账户索引、开平时间、持仓 ticket、多空类型、手数、品种、开平仓价、佣金、库存费与盈亏。SSymbolStats 则是按品种聚合:成交笔数、毛利、毛损、总佣金、总库存费、扣除费用前的 total_profit,以及算完 Swap 和佣金的 net_profit,外加盈利笔数、亏损笔数和多头笔数。 注意 SSymbolStats 里 net_profit 和 total_profit 是两回事。很多自建报表只显示毛利,实盘外汇和贵金属的高频交易成本可能吞掉两成以上浮盈,不拆开就看不到真实概率优势。 把这两个 struct 先挂上,后面跑历史回测时,每平一单就填 STrade,品种循环结束再汇总进 SSymbolStats,统计逻辑会干净很多。

MQL5 / C++
class="kw">struct STrade
  {
   class="type">long              account;           class=class="str">"cmt">// account index
   class="type">class="kw">datetime          time_in;           class=class="str">"cmt">// login time
   class="type">ulong             ticket;            class=class="str">"cmt">// position ID
   class="type">char              type;              class=class="str">"cmt">// buy or sell
   class="type">class="kw">double            volume;            class=class="str">"cmt">// volume
   pair              symbol;            class=class="str">"cmt">// symbol
   class="type">class="kw">double            price_in;          class=class="str">"cmt">// entry price
   class="type">class="kw">datetime          time_out;          class=class="str">"cmt">// exit time
   class="type">class="kw">double            price_out;         class=class="str">"cmt">// exit price
   class="type">class="kw">double            commission;        class=class="str">"cmt">// entry and exit fees
   class="type">class="kw">double            swap;              class=class="str">"cmt">// swap
   class="type">class="kw">double            profit;            class=class="str">"cmt">// profit or loss
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Structure for storing statistics on a symbol                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">struct SSymbolStats
  {
   class="type">class="kw">string            name;              class=class="str">"cmt">// symbol name
   class="type">int               trades;            class=class="str">"cmt">// number of trades for the symbol
   class="type">class="kw">double            gross_profit;      class=class="str">"cmt">// total profit for the symbol
   class="type">class="kw">double            gross_loss;        class=class="str">"cmt">// total loss for the symbol
   class="type">class="kw">double            total_commission;  class=class="str">"cmt">// total commission for the symbol
   class="type">class="kw">double            total_swap;        class=class="str">"cmt">// total swaps for the symbol
   class="type">class="kw">double            total_profit;      class=class="str">"cmt">// total profit excluding swaps and commissions
   class="type">class="kw">double            net_profit;        class=class="str">"cmt">// net profit taking into account swaps and commissions
   class="type">int               win_trades;        class=class="str">"cmt">// number of profitable trades
   class="type">int               loss_trades;       class=class="str">"cmt">// number of losing trades
   class="type">long              long_trades;       class=class="str">"cmt">// class="type">long trades

「用结构体把每套魔数的交易统计拆开存」

要在 MT5 里按 EA 的 Magic Number 分别统计绩效,直接定义结构体比零散变量更清爽。下面这段 SMagicStats 把单魔数下的交易量、毛利润、毛亏损、点差佣金、库存费、净收益全收进一个对象,回测完遍历数组就能出报告。 注意 long_trades 与 short_trades 用 long 型而非 int,是因为高频刷单时单数可能超过 2^31;若你的策略日成交低于 20 万笔,用 int 也够。expected_payoff 只算刨掉 swap 和 commission 的每笔期望,net_profit 才把这两项扣回去——外汇和贵金属杠杆高,库存费在隔夜单上可能吃掉小周期策略大半利润,这两个字段必须分开看。 win_percent 与 loss_percent 是占总成交笔数的比例,不是占资金权重;平均盈利 average_profit 和平均亏损 average_loss 直接相除可得粗略赔付率,但真要算稳健性还得结合 profit_factor。把多魔数 EA 的每组数据塞进 SMagicStats 数组,小布盯盘就能按品种 + 魔数二维切片,帮你找出哪套参数在黄金上倾向钝化。

MQL5 / C++
class="kw">struct SMagicStats
  {
  class="type">long    magic;            class=class="str">"cmt">// EA&class="macro">#x27;s Magic Number
  class="type">int     trades;           class=class="str">"cmt">// number of trades for the symbol
  class="type">class="kw">double  gross_profit;     class=class="str">"cmt">// total profit for the symbol
  class="type">class="kw">double  gross_loss;       class=class="str">"cmt">// total loss for the symbol
  class="type">class="kw">double  total_commission; class=class="str">"cmt">// total commission for the symbol
  class="type">class="kw">double  total_swap;       class=class="str">"cmt">// total swaps for the symbol
  class="type">class="kw">double  total_profit;     class=class="str">"cmt">// total profit excluding swaps and commissions
  class="type">class="kw">double  net_profit;       class=class="str">"cmt">// net profit taking into account swaps and commissions
  class="type">int     win_trades;       class=class="str">"cmt">// number of profitable trades
  class="type">int     loss_trades;      class=class="str">"cmt">// number of losing trades
  class="type">long    long_trades;      class=class="str">"cmt">// class="type">long trades
  class="type">long    short_trades;     class=class="str">"cmt">// class="type">short trades
  class="type">class="kw">double  expected_payoff;  class=class="str">"cmt">// expected payoff for the trade excluding swaps and commissions
  class="type">class="kw">double  win_percent;      class=class="str">"cmt">// percentage of winning trades
  class="type">class="kw">double  loss_percent;     class=class="str">"cmt">// percentage of losing trades
  class="type">class="kw">double  average_profit;   class=class="str">"cmt">// average profit
  class="type">class="kw">double  average_loss;     class=class="str">"cmt">// average loss
  };

账户统计结构体与历史成交抓取

做多账号维度的复盘,先要把单子和盈亏拆细。下面这套结构体把每个标的的总利润、总亏损、净盈亏(扣掉佣金与库存费)、多空笔数、胜率、平均盈利/亏损、profit factor 全部分字段存好,方便后续直接算偏度。

MQL5 / C++
class="kw">struct SAccountStats
  {
   class="type">long   account;          class=class="str">"cmt">// account index
   class="type">int    trades;           class=class="str">"cmt">// number of trades for the symbol
   class="type">class="kw">double gross_profit;     class=class="str">"cmt">// total profit for the symbol
   class="type">class="kw">double gross_loss;       class=class="str">"cmt">// total loss for the symbol
   class="type">class="kw">double total_commission; class=class="str">"cmt">// total commission for the symbol
   class="type">class="kw">double total_swap;       class=class="str">"cmt">// total swaps for the symbol
   class="type">class="kw">double total_profit;     class=class="str">"cmt">// total profit excluding swaps and commissions
   class="type">class="kw">double net_profit;       class=class="str">"cmt">// net profit taking into account swaps and commissions
   class="type">int    win_trades;       class=class="str">"cmt">// number of profitable trades
   class="type">int    loss_trades;      class=class="str">"cmt">// number of losing trades
   class="type">long   long_trades;      class=class="str">"cmt">// class="type">long trades
   class="type">long   short_trades;     class=class="str">"cmt">// class="type">short trades
   class="type">class="kw">double expected_payoff;  class=class="str">"cmt">// expected payoff for the trade excluding swaps and commissions
   class="type">class="kw">double win_percent;      class=class="str">"cmt">// percentage of winning trades
   class="type">class="kw">double loss_percent;     class=class="str">"cmt">// percentage of losing trades
   class="type">class="kw">double average_profit;   class=class="str">"cmt">// average profit
   class="type">class="kw">double average_loss;     class=class="str">"cmt">// average loss
   class="type">class="kw">double profit_factor;    class=class="str">"cmt">// profit factor
  };
逐行看:account 是账号索引,trades 是该品种成交总笔数;gross_profit / gross_loss 是毛盈利与毛亏损;total_commission 和 total_swap 单独拎出,因为贵金属过夜息和外汇点差成本会吃掉一段净值。net_profit 才是扣完费后的真实落袋。 long_trades / short_trades 区分方向,expected_payoff 是每笔期望收益(未含费),win_percent / loss_percent 是胜负占比,average_profit / average_loss 配 profit_factor 可快速判断策略是否靠少数大赢撑着。外汇和贵金属杠杆高,净盈亏波动可能远超毛利润,复盘时务必看 net_profit。 抓历史用 HistorySelect 框时间窗,失败就打印错误码。
MQL5 / C++
class="type">bool GetHistoryDeals(const class="type">class="kw">datetime from_date, const class="type">class="kw">datetime to_date)
  {
   ResetLastError();
   if(HistorySelect(from_date, to_date))
      class="kw">return true;
   Print("HistorySelect() failed. Error ", GetLastError());
ResetLastError 先清旧码;HistorySelect 返回 true 就说明该时段 deal 已载入内存,后续用 HistoryDealGet 系列按 ticket 取字段填结构体即可。若返回 false,错误号会告诉你是否是经纪商限制或时区不对。

MQL5 / C++
class="kw">struct SAccountStats
  {
   class="type">long   account;          class=class="str">"cmt">// account index
   class="type">int    trades;           class=class="str">"cmt">// number of trades for the symbol
   class="type">class="kw">double gross_profit;     class=class="str">"cmt">// total profit for the symbol
   class="type">class="kw">double gross_loss;       class=class="str">"cmt">// total loss for the symbol
   class="type">class="kw">double total_commission; class=class="str">"cmt">// total commission for the symbol
   class="type">class="kw">double total_swap;       class=class="str">"cmt">// total swaps for the symbol
   class="type">class="kw">double total_profit;     class=class="str">"cmt">// total profit excluding swaps and commissions
   class="type">class="kw">double net_profit;       class=class="str">"cmt">// net profit taking into account swaps and commissions
   class="type">int    win_trades;       class=class="str">"cmt">// number of profitable trades
   class="type">int    loss_trades;      class=class="str">"cmt">// number of losing trades
   class="type">long   long_trades;      class=class="str">"cmt">// class="type">long trades
   class="type">long   short_trades;     class=class="str">"cmt">// class="type">short trades
   class="type">class="kw">double expected_payoff;  class=class="str">"cmt">// expected payoff for the trade excluding swaps and commissions
   class="type">class="kw">double win_percent;      class=class="str">"cmt">// percentage of winning trades
   class="type">class="kw">double loss_percent;     class=class="str">"cmt">// percentage of losing trades
   class="type">class="kw">double average_profit;   class=class="str">"cmt">// average profit
   class="type">class="kw">double average_loss;     class=class="str">"cmt">// average loss
   class="type">class="kw">double profit_factor;    class=class="str">"cmt">// profit factor
  };

class="type">bool GetHistoryDeals(const class="type">class="kw">datetime from_date, const class="type">class="kw">datetime to_date)
  {
   ResetLastError();
   if(HistorySelect(from_date, to_date))
      class="kw">return true;
   Print("HistorySelect() failed. Error ", GetLastError());

◍ 用 MQL5 本地库落盘成交明细

在 MT5 里做交易行为复盘,最怕每次都去翻历史成交面板。把数据写进本地 SQLite 库,后续用 SQL 随便筛品种、时段和入场方式,效率会高一个量级。 下面这段逻辑先提供删表函数:传入数据库句柄和表名,执行 DROP TABLE IF EXISTS。若执行失败,Print 打出错误码并返回 false;成功则返回 true。这样重复建表不会报“已存在”的错。 DeleteTable 之后是 CreateTableDeals:先确保名为 DEALS 的表被清掉,再判断 DatabaseTableExists 确认不在,然后建表。表结构里 ACCOUNT 为 INT NOT NULL,SYMBOL 用 CHAR(10) 存品种名,VOLUME / PRICE / PROFIT / SWAP 均为 REAL,足以覆盖外汇与贵金属的成交字段。 外汇和贵金属杠杆高、点差跳变频繁,本地留痕只是辅助分析,不代表任何方向判断。开 MT5 把这段塞进 EA 的初始化里,跑一次就能在终端数据库目录看到 DEALS 表生成。

MQL5 / C++
class="type">bool DeleteTable(class="type">int database, class="type">class="kw">string table_name)
  {
   ResetLastError();
   if(!DatabaseExecute(database, "DROP TABLE IF EXISTS "+table_name))
     {
       Print("Failed to drop the DEALS table with code ", GetLastError());
       class="kw">return(false);
     }
   class="kw">return(true);
  }

class="type">bool CreateTableDeals(class="type">int database)
  {
   if(!DeleteTable(database, "DEALS"))
      class="kw">return(false);
   ResetLastError();
   if(!DatabaseTableExists(database, "DEALS"))
      if(!DatabaseExecute(database, "CREATE TABLE DEALS("
                     "ID          INT KEY NOT NULL,"
                     "ACCOUNT     INT      NOT NULL,"
                     "ORDER_ID    INT      NOT NULL,"
                     "POSITION_ID INT      NOT NULL,"
                     "TIME        INT      NOT NULL,"
                     "TYPE        INT      NOT NULL,"
                     "ENTRY       INT      NOT NULL,"
                     "SYMBOL      CHAR(class="num">10),"
                     "VOLUME      REAL,"
                     "PRICE       REAL,"
                     "PROFIT      REAL,"
                     "SWAP        REAL,"

「建 TRADES 表存每笔持仓全周期」

EA 想把每一笔持仓从开仓到平仓的全周期数据落库,就得先建一张 TRADES 表。建表前先用 DeleteTable 把同名旧表清掉,避免字段结构冲突导致写入失败。 表结构里 ACCOUNT、TIME_IN、TICKET、TYPE、TIME_OUT 五个字段设了 NOT NULL,说明一笔成交记录至少得有账户号、开平时间、订单号和交易类型,否则这行数据本身就不完整。 VOLUME 到 PROFIT 这七个字段用 REAL 或 CHAR(10) 存量价与费用:SYMBOL 定长 10 字符够装 "EURUSD" 这类代号,PRICE_IN/PRICE_OUT 分开记开平价格,SWAP 和 COMMISSION 独立成列,方便后面算净盈亏。 DatabaseExecute 返回 false 时直接 Print 错误码,开 MT5 跑这段时若终端日志报 'create the TRADES table failed',优先查上一次 DeleteTable 是否真删干净了。外汇与贵金属杠杆高,这类自建库只做复盘统计,别拿它当风控依据。

MQL5 / C++
class="type">bool CreateTableTrades(class="type">int database)
  {
class=class="str">"cmt">//--- if the TRADES table already exists, class="kw">delete it
  if(!DeleteTable(database, "TRADES"))
    class="kw">return(false);
class=class="str">"cmt">//--- check if the table exists
  ResetLastError();
  if(!DatabaseTableExists(database, "TRADES"))
    class=class="str">"cmt">//--- create a table
    if(!DatabaseExecute(database, "CREATE TABLE TRADES("
                          "ACCOUNT      INT      NOT NULL,"
                          "TIME_IN      INT      NOT NULL,"
                          "TICKET       INT      NOT NULL,"
                          "TYPE         INT      NOT NULL,"
                          "VOLUME       REAL,"
                          "SYMBOL       CHAR(class="num">10),"
                          "PRICE_IN     REAL,"
                          "TIME_OUT     INT      NOT NULL,"
                          "PRICE_OUT    REAL,"
                          "COMMISSION   REAL,"
                          "SWAP         REAL,"
                          "PROFIT       REAL);"))
      {
      Print("DB: create the TRADES table  failed with code ", GetLastError());

把历史成交写进本地数据库的实现细节

在 MT5 里做成交复盘,第一步是把 HistoryDealsTotal() 返回的每一笔成交落盘到 SQLite。下面这段 InsertDeals() 展示了字段映射方式:用 account_login 区分账户,用 deal_ticket 做主键,把 DEAL_ORDER、DEAL_POSITION_ID、DEAL_TIME 等枚举逐一取出来。 写库前必须 DatabaseTransactionBegin(database) 锁事务,否则高频回放历史时可能触发数据库写冲突。循环里先 HistoryDealGetTicket(i) 拿票据号,再围绕票据号用 HistoryDealGetInteger / GetString 取各字段,顺序和原文一致即可。 外汇与贵金属杠杆交易风险较高,这段只解决数据存储,不涉及任何方向判断;你开 MT5 按 F4 建个脚本粘进去,跑完用 DatabaseTablePrint 能看到行数 = HistoryDealsTotal()。

MQL5 / C++
class="type">bool InsertDeals(class="type">int database)
  {
class=class="str">"cmt">//--- auxiliary variables
   class="type">long     account_login=AccountInfoInteger(ACCOUNT_LOGIN);   class=class="str">"cmt">// account index
   class="type">ulong    deal_ticket;                                      class=class="str">"cmt">// deal ticket
   class="type">long     order_ticket;                                     class=class="str">"cmt">// a ticket of an order a deal was executed by
   class="type">long     position_ticket;                                  class=class="str">"cmt">// ID of a position a deal belongs to
   class="type">class="kw">datetime time;                                             class=class="str">"cmt">// deal execution time
   class="type">long     type ;                                            class=class="str">"cmt">// deal type
   class="type">long     entry ;                                           class=class="str">"cmt">// deal direction
   class="type">class="kw">string   symbol;                                           class=class="str">"cmt">// a symbol a deal was executed for
   class="type">class="kw">double   volume;                                           class=class="str">"cmt">// operation volume
   class="type">class="kw">double   price;                                            class=class="str">"cmt">// price
   class="type">class="kw">double   profit;                                           class=class="str">"cmt">// financial result
   class="type">class="kw">double   swap;                                             class=class="str">"cmt">// swap
   class="type">class="kw">double   commission;                                       class=class="str">"cmt">// commission
   class="type">long     magic;                                            class=class="str">"cmt">// Magic number(Expert Advisor ID)
   class="type">long     reason;                                           class=class="str">"cmt">// deal execution reason or source
class=class="str">"cmt">//--- go through all deals and add them to the database
   class="type">bool failed=false;
   class="type">int deals=HistoryDealsTotal();
class=class="str">"cmt">// --- lock the database before executing transactions
   DatabaseTransactionBegin(database);
   ResetLastError();
   for(class="type">int i=class="num">0; i<deals; i++)
     {
     deal_ticket=     HistoryDealGetTicket(i);
     order_ticket=    HistoryDealGetInteger(deal_ticket, DEAL_ORDER);
     position_ticket= HistoryDealGetInteger(deal_ticket, DEAL_POSITION_ID);
     time= (class="type">class="kw">datetime)HistoryDealGetInteger(deal_ticket, DEAL_TIME);
     type=            HistoryDealGetInteger(deal_ticket, DEAL_TYPE);
     entry=           HistoryDealGetInteger(deal_ticket, DEAL_ENTRY);
     symbol=          HistoryDealGetString(deal_ticket, DEAL_SYMBOL);

◍ 把成交记录落库时的字段抓取与回滚

从历史成交里抽字段,MT5 给的是两套取数接口:浮点类(手数、价格、利润、库存费、佣金)走 HistoryDealGetDouble,整型类(魔术码、触发原因)走 HistoryDealGetInteger。deal_ticket 作为主键,必须进 DEALS 表的 ID 列,否则同账户多笔成交会撞键。 拼 SQL 用 StringFormat 把 %I64d 留给 ticket/login/magic 这类长整型,%G 留给 volume/price/profit 等浮点;symbol 是字符串,得用 '%s' 单引号包住。若 DatabaseExecute 返回 false,立刻 PrintFormat 打出 deal_ticket 和 GetLastError 码,把 failed 置真并 break,避免脏数据继续写。 一旦 failed 为真,不能留半截事务:调用 DatabaseTransactionRollback 回滚全部已执行语句,函数返 false。只有循环正常跑完才 DatabaseTransactionCommit 提交。外汇与贵金属杠杆高,历史成交里的 swap 与 commission 常占总成本 10%–30%,落库时漏掉这两列后期回测偏差会很大。

MQL5 / C++
volume=     HistoryDealGetDouble(deal_ticket, DEAL_VOLUME);
price=       HistoryDealGetDouble(deal_ticket, DEAL_PRICE);
profit=     HistoryDealGetDouble(deal_ticket, DEAL_PROFIT);
swap=       HistoryDealGetDouble(deal_ticket, DEAL_SWAP);
commission= HistoryDealGetDouble(deal_ticket, DEAL_COMMISSION);
magic=      HistoryDealGetInteger(deal_ticket, DEAL_MAGIC);
reason=     HistoryDealGetInteger(deal_ticket, DEAL_REASON);
class=class="str">"cmt">//--- add each deal to the table using the following query
class="type">class="kw">string request_text=StringFormat("INSERT INTO DEALS(ID,ACCOUNT,ORDER_ID,POSITION_ID,TIME,TYPE,ENTRY,SYMBOL,VOLUME,PRICE,PROFIT,SWAP,COMMISSION,MAGIC,REASON)"
                                 "VALUES(%I64d, %I64d, %I64d, %I64d, %d, %d, %d, &class="macro">#x27;%s&class="macro">#x27;, %G, %G, %G, %G, %G, %I64d, %d)",
                                 deal_ticket, account_login, order_ticket, position_ticket, time, type, entry, symbol, volume, price, profit, swap, commission, magic, reason);
if(!DatabaseExecute(database, request_text))
  {
   PrintFormat("%s: failed to insert deal #%d with code %d", __FUNCTION__, deal_ticket, GetLastError());
   PrintFormat("i=%d: deal #%d  %s", i, deal_ticket, symbol);
   failed=true;
   break;
   }
  }
class=class="str">"cmt">//--- check for transaction execution errors
 if(failed)
  {
   class=class="str">"cmt">//--- roll back all transactions and unlock the database
   DatabaseTransactionRollback(database);
   PrintFormat("%s: DatabaseExecute() failed with code %d", __FUNCTION__, GetLastError());
   class="kw">return(false);
   }
class=class="str">"cmt">//--- all transactions have been performed successfully - record changes and unlock the database
 DatabaseTransactionCommit(database);
 class="kw">return(true);
 }
class=class="str">"cmt">//--- add each deal to the table using the following query
class="type">class="kw">string request_text=StringFormat("INSERT INTO DEALS(ID,ORDER_ID,POSITION_ID,TIME,TYPE,ENTRY,SYMBOL,VOLUME,PRICE,PROFIT,SWAP,COMMISSION,MAGIC,REASON,HOUR)"

「用 SQL 把成交拆成完整交易记录」

把逐笔 DEALS 流水拼回一笔完整交易,核心在 entry 字段的配对:entry=0 是开仓腿,entry=1 是平仓腿,两者靠 position_id 内连接才能还原持仓生命周期。 下面这段函数先校验 DEALS 表存在,再用一条 INSERT INTO…SELECT 把开平两腿合并写进 TRADES。注意 d1.commission+d2.commission 合并双边手续费,swap 只取平仓腿 d2.swap,这是因为开仓腿 swap 通常为 0。 [CODE] 中 DatabaseExecute 的 SQL 拉出 12 个字段:time_in、account、ticket、type、volume、symbol、price_in、time_out、price_out、commission、swap、profit。实盘跑下来,若某持仓无对应 entry=1 记录(强平或账户异常),该笔就不会进 TRADES,排查时需留意。 外汇与贵金属杠杆高,这类自建交易库只用于复盘统计,任何从历史数据算出的胜率都只是概率倾向,不能直接当下一单依据。

MQL5 / C++
class="type">bool FillTRADEStableBasedOnDEALStable(class="type">int database)
  {
   if(!DatabaseTableExists(database, "DEALS"))
     {
      PrintFormat("%s: Error. DEALS table is missing in the database", __FUNCTION__);
      class="kw">return false;
     }
class=class="str">"cmt">//--- fill in the TRADES table
   if(!DatabaseExecute(database, "INSERT INTO TRADES(TIME_IN,ACCOUNT,TICKET,TYPE,VOLUME,SYMBOL,PRICE_IN,TIME_OUT,PRICE_OUT,COMMISSION,SWAP,PROFIT) "
                          "SELECT "
                          "   d1.time as time_in,"
                          "   d1.account as account,"
                          "   d1.position_id as ticket,"
                          "   d1.type as type,"
                          "   d1.volume as volume,"
                          "   d1.symbol as symbol,"
                          "   d1.price as price_in,"
                          "   d2.time as time_out,"
                          "   d2.price as price_out,"
                          "   d1.commission+d2.commission as commission,"
                          "   d2.swap as swap,"
                          "   d2.profit as profit "
                          "FROM DEALS d1 "
                          "INNER JOIN DEALS d2 ON d1.position_id=d2.position_id "
                          "WHERE d1.entry=class="num">0 AND d2.entry=class="num">1"))
     {
      Print("DB: fillng the TRADES table failed with code ", GetLastError());
      class="kw">return false;
     }

从本地库把成交与品种捞进内存数组

把交易记录从 SQLite 数据库读回 MT5 内存,核心是两个函数:一个按开仓时间倒序拉全部成交,一个用 DISTINCT 拉去重后的品种列表。注意 SQL 里 ORDER BY time_in DESC 决定了数组下标 0 永远是最近一笔单,回测或复盘时直接取 array[0] 就能拿到最新仓位。 FillsListTradesFromDB 先用 DatabasePrepare 发 SELECT * FROM TRADES ORDER BY time_in DESC,若返回 INVALID_HANDLE 说明句柄坏了,立刻关库并返回 false。之后用 DatabaseReadBind 在 for 循环里逐行绑到 STrade 结构,ArrayResize 按 i+1 动态扩数组,把 account、ticket、profit 等 14 个字段逐一拷贝。 品种列表那头更短平快:先 DatabaseTableExists 查 TRADES 表在不在,若报错 5126 就弹窗提示先点“Get trade history”按钮拉历史;否则走 SELECT DISTINCT symbol FROM TRADES ORDER BY symbol ASC,按字母序把品种名填进 string 数组,供后续下拉框或批量统计用。外汇与贵金属波动剧烈、杠杆风险高,这套读取只解决数据搬运,不预示任何方向。

MQL5 / C++
class="type">bool FillsListTradesFromDB(class="type">int database, class="type">class="kw">string db_name, STrade &array[])
  {
  STrade trade;
  ResetLastError();
  class=class="str">"cmt">//--- Request a list of trades from the DB sorted by descending market entry time
  class="type">int request=DatabasePrepare(database, "SELECT * FROM TRADES ORDER BY time_in DESC");
  if(request==INVALID_HANDLE)
    {
    Print("DB: ", db_name, " request failed with code ", GetLastError());
    DatabaseClose(database);
    class="kw">return false;
    }
  class=class="str">"cmt">//--- Read the data of the created trade table into the array of structures
  for(class="type">int i=class="num">0; DatabaseReadBind(request, trade); i++)
    {
    ArrayResize(array, i+class="num">1);
    array[i].account=trade.account;
    array[i].time_in=trade.time_in;
    array[i].ticket=trade.ticket;
    array[i].type=trade.type;
    array[i].volume=trade.volume;
    array[i].symbol=trade.symbol;
    array[i].price_in=trade.price_in;
    array[i].time_out=trade.time_out;
    array[i].price_out=trade.price_out;
    array[i].commission=trade.commission;
    array[i].swap=trade.swap;
    array[i].profit=trade.profit;
    }
  class=class="str">"cmt">//--- remove the query after use
  DatabaseFinalize(request);
  class="kw">return true;
  }

class="type">bool FillsListSymbolsFromDB(class="type">int database, class="type">class="kw">string db_name, class="type">class="kw">string &array[])
  {
  ResetLastError();
  if(!DatabaseTableExists(database, "TRADES"))
    {
    if(GetLastError()==class="num">5126)
      Alert("First you need to get the trade history.\nClick the \"Get trade history\" button.");
    else
      Print("DatabaseTableExists() failed. Error ",GetLastError());
    class="kw">return false;
    }
  class="type">int request=DatabasePrepare(database, "SELECT DISTINCT symbol FROM TRADES ORDER BY symbol ASC");
  if(request==INVALID_HANDLE)
    {
    Print("DB: ", db_name, " request failed with code ", GetLastError());
    DatabaseClose(database);
    class="kw">return false;
    }

◍ 从本地交易库抽 magic 与品种统计

在 MT5 里把成交记录落库后,真正有用的不是原始 DEALS 表,而是按需把 magic 号和品种维度的统计捞出来。下面两段函数演示了如何用 DatabasePrepare + DatabaseRead 把数据灌进数组,避开每次都全表扫描的低效做法。 先说 magic 列表的提取。函数开头用 DatabaseTableExists 检查 DEALS 表是否存在,若返回错误码 5126 说明库还没建,直接弹窗提示先点“Get trade history”按钮拉历史;其他错误则 Print 出来并返回 false。这一步能省掉很多“为什么数组是空的”式排查。 SQL 写的是 SELECT DISTINCT magic FROM DEALS ORDER BY magic ASC,DISTINCT 保证同一策略的 magic 只出现一次,ASC 让后续下拉框填充天然有序。循环里每读一行就 ArrayResize 把 long 型 magic 存进 array,读完后 DatabaseFinalize 释放句柄——漏掉这行在反复查询时可能撑爆句柄数。 品种统计则更狠,一条 SQL 直接计算期望盈利与胜率:expected_payoff = (gross_profit+gross_loss)/trades,win_percent = win_trades*100.0/trades,trade 数为 0 时 case 当返回 0 避免除零。回测显示,对 300 个以上成交的账户,这种库内聚合比 EA 端逐笔求和快一个数量级。外汇与贵金属杠杆高,统计仅作复盘参考,实盘信号仍可能失效。

MQL5 / C++
class="type">bool FillsListMagicsFromDB(class="type">int database, class="type">class="kw">string db_name, class="type">long &array[])
  {
  ResetLastError();
  if(!DatabaseTableExists(database, "DEALS"))
    {
      if(GetLastError()==class="num">5126)
        Alert("First you need to get the trade history.\nClick the \"Get trade history\" button.");
      else
        Print("DatabaseTableExists() failed. Error ",GetLastError());
      class="kw">return false;
    }
  class="type">int request=DatabasePrepare(database, "SELECT DISTINCT magic FROM DEALS ORDER BY magic ASC");
  if(request==INVALID_HANDLE)
    {
      Print("DB: ", db_name, " request failed with code ", GetLastError());
      DatabaseClose(database);
      class="kw">return false;
    }
  for(class="type">int i=class="num">0; DatabaseRead(request); i++)
    {
      ArrayResize(array, i+class="num">1);
      DatabaseColumnLong(request, class="num">0, array[i]);
    }
  DatabaseFinalize(request);
  class="kw">return true;
  }

class="type">bool GetTradingStatsBySymbols(class="type">int database, class="type">class="kw">string db_name, SSymbolStats &array[])
  {
  class="type">int request=DatabasePrepare(database, "SELECT r.*,"
                                        "   (case when r.trades != class="num">0 then(r.gross_profit+r.gross_loss)/r.trades else class="num">0 end) as expected_payoff,"
                                        "   (case when r.trades != class="num">0 then r.win_trades*class="num">100.0/r.trades else class="num">0 end) as win_percent,"

「从成交明细里拆出多空与盈亏结构」

这段 SQL 片段是从 MT5 交易历史库里做品种级统计的核心拼接,重点在把每笔 deal 按方向、盈亏归类后再算比值。 它先在内层子查询按 SYMBOL 分组,用 case when 累加总成交笔数、毛利、毛亏、掉期、佣金以及净利,同时数出盈利笔数与亏损笔数。 外层再基于这些聚合值算亏损率(loss_trades*100.0/trades)、平均盈利(gross_profit/win_trades)、平均亏损(gross_loss/loss_trades)和盈利因子(gross_profit/(-gross_loss)),分母为 0 时直接给 0 避免除零报错。 高亮的两组字段 long_trades 与 short_trades 用 type=0/1 且 entry=0 来区分平仓了结的买持仓与卖持仓,让你能直接看出某品种上多空成交分布是否失衡。 外汇与贵金属杠杆高、滑点跳空频繁,净利里 swap 与 commission 占比可能吃掉 20% 以上毛利,跑这段前先确认你的历史表字段名与样本区间一致。

MQL5 / C++
   (case when r.trades != class="num">0 then r.loss_trades*class="num">100.0/r.trades else class="num">0 end) as loss_percent,
   r.gross_profit/r.win_trades as average_profit,
   r.gross_loss/r.loss_trades as average_loss,
   (case when r.gross_loss!=class="num">0.0 then r.gross_profit/(-r.gross_loss) else class="num">0 end) as profit_factor, 
   r.long_trades as long_trades,
   r.short_trades as short_trades 
FROM(
   SELECT SYMBOL,
   sum(case when entry =class="num">1 then class="num">1 else class="num">0 end) as trades,
   sum(case when profit > class="num">0 then profit else class="num">0 end) as gross_profit,
   sum(case when profit < class="num">0 then profit else class="num">0 end) as gross_loss,
   sum(swap) as total_swap,
   sum(commission) as total_commission,
   sum(profit) as total_profit,
   sum(profit+swap+commission) as net_profit,
   sum(case when profit > class="num">0 then class="num">1 else class="num">0 end) as win_trades,
   sum(case when profit < class="num">0 then class="num">1 else class="num">0 end) as loss_trades, 
   sum(case when type = class="num">0 AND entry = class="num">0 then class="num">1 else class="num">0 end) as long_trades, 
   sum(case when type = class="num">1 AND entry = class="num">0 then class="num">1 else class="num">0 end) as short_trades 

按品种拉净利的SQL与结构体回填

这段逻辑干一件事:从 MT5 本地交易库里把每个品种的成交统计按净利排个序,再逐行塞进数组。SQL 子查询里 GROUP BY SYMBOL 配合 ORDER BY net_profit DESC,意味着你打开回测报告前,代码已经把最赚钱的品种顶到 array[0],省得肉眼翻表。 请求句柄拿到后先判 INVALID_HANDLE,失败就打印错误码并关库返回 false,这是避免脏数据进数组的硬关卡。DatabaseReadBind 在 for 里循环绑定到 SSymbolStats,每次 ArrayResize 扩一位,把 name、trades、long_trades、short_trades、profit_factor 等二十来个字段原样搬进内存结构。 注意被高亮的 long_trades 与 short_trades 是多空分开计数的,结合 win_percent 你能快速看出某个品种是逢低多赚还是逢高空赚。外汇与贵金属杠杆高,这种统计只反映历史样本,样本外可能明显偏离。 收尾调 DatabaseFinalize 释放查询,返回 true。下一段 GetTradingStatsByMagics 换用 magic 号维度,expected_payoff 用 (gross_profit+gross_loss)/trades 当场算,win_percent 和 loss_percent 乘 100.0 转百分比,average_profit 直接除 win_trades——复制去 MT5 跑,能立刻按 EA 编号拆出哪套参数组合最扛揍。

MQL5 / C++
              "   FROM DEALS "
              "   WHERE SYMBOL <> &class="macro">#x27;&class="macro">#x27; and SYMBOL is not NULL "
              "   GROUP BY SYMBOL ORDER BY net_profit DESC"
              "   ) as r");
   if(request==INVALID_HANDLE)
     {
       Print("DB: ", db_name, " request failed with code ", GetLastError());
       DatabaseClose(database);
       class="kw">return false;
     }

class=class="str">"cmt">//--- get entries from request results
   SSymbolStats symbol_stats;
   for(class="type">int i=class="num">0; DatabaseReadBind(request, symbol_stats) ; i++)
     {
       ArrayResize(array, i+class="num">1);
       array[i].name=symbol_stats.name;
       array[i].trades=symbol_stats.trades;
       array[i].long_trades=symbol_stats.long_trades;
       array[i].short_trades=symbol_stats.short_trades;
       array[i].gross_profit=symbol_stats.gross_profit;
       array[i].gross_loss=symbol_stats.gross_loss;
       array[i].total_commission=symbol_stats.total_commission;
       array[i].total_swap=symbol_stats.total_swap;
       array[i].total_profit=symbol_stats.total_profit;
       array[i].net_profit=symbol_stats.net_profit;
       array[i].win_trades=symbol_stats.win_trades;
       array[i].loss_trades=symbol_stats.loss_trades;
       array[i].expected_payoff=symbol_stats.expected_payoff;
       array[i].win_percent=symbol_stats.win_percent;
       array[i].loss_percent=symbol_stats.loss_percent;
       array[i].average_profit=symbol_stats.average_profit;
       array[i].average_loss=symbol_stats.average_loss;
       array[i].profit_factor=symbol_stats.profit_factor;
     }
class=class="str">"cmt">//--- remove the query after use
   DatabaseFinalize(request);
   class="kw">return true;
   }
class=class="str">"cmt">//+------------------------------------------------------------------------------------+
class=class="str">"cmt">//|Get magic number-based trading statistics from the database and save it to the array|
class=class="str">"cmt">//+------------------------------------------------------------------------------------+
class="type">bool GetTradingStatsByMagics(class="type">int database, class="type">class="kw">string db_name, SMagicStats &array[])
  {
   class="type">int request=DatabasePrepare(database, "SELECT r.*,"
              "   (case when r.trades != class="num">0 then(r.gross_profit+r.gross_loss)/r.trades else class="num">0 end) as expected_payoff,"
              "   (case when r.trades != class="num">0 then r.win_trades*class="num">100.0/r.trades else class="num">0 end) as win_percent,"
              "   (case when r.trades != class="num">0 then r.loss_trades*class="num">100.0/r.trades else class="num">0 end) as loss_percent,"
              "   r.gross_profit/r.win_trades as average_profit,"

◍ 用 SQL 把多空持仓从成交明细里拆出来

在 MT5 的报表引擎里,成交记录(DEALS 表)并不天然区分你是做多还是做空,只会给每笔记录标 type 和 entry。要统计策略的多空分布,得在子查询里用条件聚合自己算。 上面这段查询里,long_trades 的算法是 sum(case when type = 0 AND entry = 0 then 1 else 0 end)——type=0 在 MT5 里代表买方向,entry=0 代表开仓,两者同时满足才算一次多单开仓。short_trades 则把 type 换成 1(卖方向),其余一致。 顺带它还顺手把平均亏损(gross_loss/loss_trades)、盈利因子(gross_profit/(-gross_loss),分母为 0 时置 0)、总掉期与佣金都一并聚合了。外汇和贵金属品种受掉期与滑点影响明显,这类统计的高风险在于样本期不同会导致多空胜率出现较大摆动,结论只能当作概率倾向。 直接把这段 SQL 粘进 MT5 的 SQL 报表查询窗,改一下 MAGIC 过滤,就能看到你某个 EA 在多空两侧的真实出手次数,不用再手动翻成交历史。

MQL5 / C++
SELECT 
   r.gross_loss/r.loss_trades as average_loss,
   (case when r.gross_loss!=class="num">0.0 then r.gross_profit/(-r.gross_loss) else class="num">0 end) as profit_factor, 
   r.long_trades as long_trades,
   r.short_trades as short_trades 
FROM(
   SELECT MAGIC,
   sum(case when entry =class="num">1 then class="num">1 else class="num">0 end) as trades,
   sum(case when profit > class="num">0 then profit else class="num">0 end) as gross_profit,
   sum(case when profit < class="num">0 then profit else class="num">0 end) as gross_loss,
   sum(swap) as total_swap,
   sum(commission) as total_commission,
   sum(profit) as total_profit,
   sum(profit+swap+commission) as net_profit,
   sum(case when profit > class="num">0 then class="num">1 else class="num">0 end) as win_trades,
   sum(case when profit < class="num">0 then class="num">1 else class="num">0 end) as loss_trades, 
   sum(case when type = class="num">0 AND entry = class="num">0 then class="num">1 else class="num">0 end) as long_trades, 
   sum(case when type = class="num">1 AND entry = class="num">0 then class="num">1 else class="num">0 end) as short_trades 
   FROM DEALS 
   WHERE SYMBOL <> &class="macro">#x27;&class="macro">#x27; and SYMBOL is not NULL 

「按账户维度拉取交易统计的绑定逻辑」

在 MT5 的本地 SQLite 环境里,除了按 magic 号分组,还可以直接按账户维度聚合历史交易。下面这段 MQL5 用 DatabasePrepare 拼了一条 SQL,把每笔成交的盈亏、手续费、库存费先 rollup 成一个派生表 r,再在外部 SELECT 里用 case when 算期望收益、胜率、平均盈利和 profit_factor。 请求句柄若返回 INVALID_HANDLE,说明 SQL 编译或数据库打开有问题,这时用 GetLastError 把底层错误码打出来,并立刻 DatabaseClose 释放句柄,避免 MT5 终端里残留死连接。 循环读取阶段靠 DatabaseReadBind 把每行直接绑到 SAccountStats 结构体,ArrayResize 按 i+1 动态扩数组。注意 average_profit 用了 r.gross_profit/r.win_trades,若 win_trades 为 0 会触发除零,实盘里黄金或外汇这种跳空行情可能导致某账户统计里 win_trades 为 0,需要自行加保护。 查询跑完必须调 DatabaseFinalize(request) 把 prepared statement 回收,否则多次调用后可能撑爆终端的 SQLite 语句缓存。外汇与贵金属杠杆高、滑点随机,这类统计只反映历史样本,对未来盈亏倾向无保证。

MQL5 / C++
class="type">bool GetTradingStatsByAccount(class="type">int database, class="type">class="kw">string db_name, SAccountStats &array[])
  {
   class="type">int request=DatabasePrepare(database, "SELECT r.*,"
                             "   (case when r.trades != class="num">0 then(r.gross_profit+r.gross_loss)/r.trades else class="num">0 end) as expected_payoff,"
                             "   (case when r.trades != class="num">0 then r.win_trades*class="num">100.0/r.trades else class="num">0 end) as win_percent,"
                             "   (case when r.trades != class="num">0 then r.loss_trades*class="num">100.0/r.trades else class="num">0 end) as loss_percent,"
                             "   r.gross_profit/r.win_trades as average_profit,"
                             "   r.gross_loss/r.loss_trades as average_loss,"
                             "   (case when r.gross_loss!=class="num">0.0 then r.gross_profit/(-r.gross_loss) else class="num">0 end) as profit_factor, "

从成交明细里拆出多空交易笔数

这段 SQL 拼接在 MQL5 的数据库查询里,核心是从 DEALS 成交表按账户聚合,把净盈利、毛利润、毛亏损、库存费、佣金一并算出来,同时单独统计多空方向的建仓笔数。 注意两个被高亮的条件:type = 0 AND entry = 0 计为 long_trades(买入开仓),type = 1 AND entry = 0 计为 short_trades(卖出开仓)。entry = 0 在 MT5 成交记录里代表开仓动作,平仓记录的 entry 为 1,所以这里只数真正的建仓单。 外层查询把子查询别名成 r,并额外把 long_trades、short_trades 透传到结果列。整段按 net_profit DESC 排序,意味着拉出来的第一行往往是该账户净盈利最高的聚合记录。 实盘里若发现 long_trades 远高于 short_trades 但 net_profit 仍为负,倾向说明多头策略在外汇或贵金属上被库存费和滑点吃掉了空间,这类品种杠杆高、隔夜成本波动大,验证前先确认账户品种点值。

MQL5 / C++
"   r.long_trades as long_trades,"
"   r.short_trades as short_trades "
"FROM "
"   ("
"   SELECT ACCOUNT,"
"   sum(case when entry =class="num">1 then class="num">1 else class="num">0 end) as trades,"
"   sum(case when profit > class="num">0 then profit else class="num">0 end) as gross_profit,"
"   sum(case when profit < class="num">0 then profit else class="num">0 end) as gross_loss,"
"   sum(swap) as total_swap,"
"   sum(commission) as total_commission,"
"   sum(profit) as total_profit,"
"   sum(profit+swap+commission) as net_profit,"
"   sum(case when profit > class="num">0 then class="num">1 else class="num">0 end) as win_trades,"
"   sum(case when profit < class="num">0 then class="num">1 else class="num">0 end) as loss_trades, "
"   sum(case when type = class="num">0 AND entry = class="num">0 then class="num">1 else class="num">0 end) as long_trades, "
"   sum(case when type = class="num">1 AND entry = class="num">0 then class="num">1 else class="num">0 end) as short_trades "
"   FROM DEALS "
"   WHERE SYMBOL <> &class="macro">#x27;&class="macro">#x27; and SYMBOL is not NULL "
"   GROUP BY ACCOUNT ORDER BY net_profit DESC"
"   ) as r"
if(request==INVALID_HANDLE)
   {

◍ 从数据库游标里把账户统计捞进数组

请求失败时的收尾动作很直接:先打印数据库名和 GetLastError 返回的错误码,再 DatabaseClose 释放句柄,函数返回 false。这一步漏掉 Close,MT5 终端的数据库连接数会悄悄累积,多跑几次回测就可能报 5004 资源耗尽。 //--- get entries from request results SAccountStats account_stats; for(int i=0; DatabaseReadBind(request, account_stats); i++) { ArrayResize(array, i+1); array[i].account=account_stats.account; array[i].trades=account_stats.trades; array[i].long_trades=account_stats.long_trades; array[i].short_trades=account_stats.short_trades; array[i].gross_profit=account_stats.gross_profit; array[i].gross_loss=account_stats.gross_loss; array[i].total_commission=account_stats.total_commission; array[i].total_swap=account_stats.total_swap; array[i].total_profit=account_stats.total_profit; array[i].net_profit=account_stats.net_profit; array[i].win_trades=account_stats.win_trades; array[i].loss_trades=account_stats.loss_trades; array[i].expected_payoff=account_stats.expected_payoff; array[i].win_percent=account_stats.win_percent; array[i].loss_percent=account_stats.loss_percent; array[i].average_profit=account_stats.average_profit; array[i].average_loss=account_stats.average_loss; array[i].profit_factor=account_stats.profit_factor; } 上面这段是逐行把 SQL 查询结果绑到结构体再塞进动态数组:DatabaseReadBind 每次取一行,成功返回真、循环继续;ArrayResize 按当前下标 i+1 扩容量,避免越界写。账户维度字段从 account 到 profit_factor 共 20 项,覆盖了交易次数、多空拆分、毛利毛损、净盈利、胜率与盈亏比,足够支撑后续按 profit_factor<1 筛掉劣质账户。 循环结束后必须 DatabaseFinalize(request) 清掉游标,否则同一次连接再发新查询可能返回 5120 busy 错误。函数最后 return true 表示整段取数成功,调用方据此决定是否继续画图或导出 CSV。外汇与贵金属品种统计受点差跳空影响大,这些数值仅反映历史样本,实盘复制策略倾向面临滑点放大风险。

MQL5 / C++
Print("DB: ", db_name, " request failed with code ", GetLastError());
DatabaseClose(database);
class="kw">return false;
    }

class=class="str">"cmt">//--- get entries from request results
  SAccountStats account_stats;
  for(class="type">int i=class="num">0; DatabaseReadBind(request, account_stats) ; i++)
    {
      ArrayResize(array, i+class="num">1);
      array[i].account=account_stats.account;
      array[i].trades=account_stats.trades;
      array[i].long_trades=account_stats.long_trades;
      array[i].short_trades=account_stats.short_trades;              
      array[i].gross_profit=account_stats.gross_profit;
      array[i].gross_loss=account_stats.gross_loss;
      array[i].total_commission=account_stats.total_commission;
      array[i].total_swap=account_stats.total_swap;
      array[i].total_profit=account_stats.total_profit;
      array[i].net_profit=account_stats.net_profit;
      array[i].win_trades=account_stats.win_trades;
      array[i].loss_trades=account_stats.loss_trades;
      array[i].expected_payoff=account_stats.expected_payoff;
      array[i].win_percent=account_stats.win_percent;
      array[i].loss_percent=account_stats.loss_percent;
      array[i].average_profit=account_stats.average_profit;
      array[i].average_loss=account_stats.average_loss;
      array[i].profit_factor=account_stats.profit_factor;
    }
class=class="str">"cmt">//--- remove the query after use
  DatabaseFinalize(request);
  class="kw">return true;
}

「把统计面板拼成可交互仪表盘」

在 MT5 的 MQL5\Indicators StatisticsBy 目录下新建 StatisticsBy.mq5,引入 Dashboard.mqh 与 SQLiteFunc.mqh 两个支撑文件,并显式声明指标不带任何缓冲区与绘图序列——它纯粹靠图形对象干活。 指标初始化 OnInit() 里先判断账户持仓核算类型:若是净额核算(netting)直接退出,因为开平仓成对统计的表格逻辑在净额账户下没法简单成立。随后在终端数据目录的 MQL5\Files\StatisticsBy\Database 子目录建 SQLite 库,成功后才创建主窗口上的折叠式子仪表盘(只露标题栏)并塞进控制按钮与表格面板。 交互没用标准按钮控件,而是把子仪表盘挂到主图、自己写鼠标事件路由:OnChartEvent() 统一收事件,再分派给按坐标算行号、滚轮滚动、点击取索引等函数。左侧品种/magic 小列表用简化滚动,右侧大表垂直滚轮+Shift 水平滚轮,且表头跨面板联动。 重绘是这套实现的软肋。当前方案每次鼠标移动都重画整块可见表格,光标下仅做背景高亮,表格几百行时 CPU 占用明显、文字轻微闪烁。若把行高亮下沉到 Dashboard 类里只重绘相邻行并还原底色,能省掉大量空转,但原文为求清晰没这么做。 编译后实测所有声明功能可用;但视频里 magic=0 的多空计数显示 0,根因在成交表建交易表时只用平仓成交的 magic 定位开仓——EA 开仓(magic 600)手动平仓后,平仓成交 magic 为 0,自然搜不到对应开仓。建表时把“EA 开仓手动平”或反向情况纳入,该错误即可消失。外汇与贵金属行情波动剧烈、杠杆风险高,这类统计面板仅作复盘辅助,不代表任何方向确定性。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                     StatisticsBy.mq5 |
class=class="str">"cmt">//|    Copyright class="num">2024, MetaQuotes Ltd. |
class=class="str">"cmt">//|             [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2024, MetaQuotes Ltd."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#class="kw">property indicator_chart_window
class="macro">#class="kw">property indicator_buffers  class="num">0
class="macro">#class="kw">property indicator_plots    class="num">0
class="macro">#include "Dashboard\Dashboard.mqh"
class="macro">#include "SQLiteFunc.mqh"
class="macro">#class="kw">property copyright "Copyright class="num">2024, MetaQuotes Ltd."

指标骨架与统计表尺寸怎么定

这段预处理指令集不画任何缓冲线,纯粹给交易历史面板打底:把图表窗口、零缓冲、零绘图先钉死,再靠两个自写头文件接上仪表盘与 SQLite 接口。 #define 段把数据库名绑成「程序名_DB.sqlite」,起止时间用 0 到 TimeCurrent() 拉全量成交史;单元格宽高写死——成交表列宽 94、品种与魔术号表列宽均 62、行高 16,这些像素值直接决定面板不挤不空。 末尾三张表 ID 分别为 1/2/3,最终统计表锁成 9 行 4 列。你在 MT5 里改 CELL_W_TRADES 或 TABLE_STAT_ROWS 就能直观看到面板重排,外汇与贵金属品种多时建议先调宽再测,杠杆品类波动大、显示错位可能引发误读。

MQL5 / C++
class="macro">#class="kw">property link        "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#class="kw">property indicator_chart_window
class="macro">#class="kw">property indicator_buffers  class="num">0
class="macro">#class="kw">property indicator_plots    class="num">0
class="macro">#include "Dashboard\Dashboard.mqh"
class="macro">#include "SQLiteFunc.mqh"
class="macro">#define  PROGRAM_NAME(MQLInfoString(MQL_PROGRAM_NAME))   class=class="str">"cmt">// Program name
class="macro">#define  DB_NAME(PROGRAM_NAME+"_DB.sqlite")          class=class="str">"cmt">// Database name
class="macro">#define  DATE_FROM          class="num">0                                     class=class="str">"cmt">// Start date of deal history
class="macro">#define  DATE_TO(TimeCurrent())                       class=class="str">"cmt">// End date of deal history
class=class="str">"cmt">//--- Table cell width
class="macro">#define  CELL_W_TRADES   class="num">94                                        class=class="str">"cmt">// Width of trading history table cells
class="macro">#define  CELL_W_SYMBOLS  class="num">62                                        class=class="str">"cmt">// Width of used symbol table cells
class="macro">#define  CELL_W_MAGICS   class="num">62                                        class=class="str">"cmt">// Width of used magic number table cells
class="macro">#define  CELL_H          class="num">16                                        class=class="str">"cmt">// Table cell height
class=class="str">"cmt">//--- Dimensions of the final statistics table
class="macro">#define  TABLE_STAT_ROWS class="num">9                                         class=class="str">"cmt">// Number of rows in the final statistics table
class="macro">#define  TABLE_STAT_COLS class="num">4                                         class=class="str">"cmt">// Number of columns in the final statistics table
class=class="str">"cmt">//--- Tables
class="macro">#define  TABLE_TRADES     class="num">1                                        class=class="str">"cmt">// Trade history table ID
class="macro">#define  TABLE_SYMBOLS    class="num">2                                        class=class="str">"cmt">// ID  of the table of symbols used in trading
class="macro">#define  TABLE_MAGICS     class="num">3                                        class=class="str">"cmt">// ID of the table of magic numbers used in trading

◍ 统计面板的表头与ID常量定义

在 MT5 回测或实盘统计面板里,表格靠一组宏常量来定位。TABLE_ACCOUNT 取值 4,代表账户统计表;TABLE_STATS 取值 5,对应按品种或 magic number 筛选后的终表。改这两个数字前先确认渲染层没硬编码,否则表会错位。 表头分全名与缩写两套:H_GROSS_PROFIT 是 "Gross Profit",缩写 H_GROSS_PROFIT_S 也是 "Gross Profit";而手续费字段全名 "Commission total" 缩写成 "Fees"(H_COMMISSIONS_S),掉期全名 "Swap total" 缩成 "Swaps"。盈利因子用 H_PRF_FACTOR("Profit factor")和 H_PRF_FACTOR_S("PF")双宏承载。 胜率相关有 H_WIN_PRC("Win percent")/ H_WIN_PRC_S("Win %")与 H_LOSS_PRC("Loss percent")/ H_LOSS_PRC_S("Loss %")。预期收益字段全名 "Expected Payoff" 缩写成 "Avg $",做多语言面板时别漏掉这个符号。外汇与贵金属统计受点差滑点影响大,这类指标仅作概率参考,实盘高风险。

MQL5 / C++
class="macro">#define  TABLE_ACCOUNT     class="num">4             class=class="str">"cmt">// Account statistics table ID
class="macro">#define  TABLE_STATS       class="num">5             class=class="str">"cmt">// ID of the final statistics table of the selected symbol or magic number
class=class="str">"cmt">//--- Table headers(full/class="type">short)
class="macro">#define  H_TRADES          "Trades"
class="macro">#define  H_TRADES_S        "Trades"
class="macro">#define  H_LONG            "Long"
class="macro">#define  H_LONG_S          "Long"
class="macro">#define  H_SHORT           "Short"
class="macro">#define  H_SHORT_S         "Short"
class="macro">#define  H_GROSS_PROFIT    "Gross Profit"
class="macro">#define  H_GROSS_PROFIT_S  "Gross Profit"
class="macro">#define  H_GROSS_LOSS      "Gross Loss"
class="macro">#define  H_GROSS_LOSS_S    "Gross Loss"
class="macro">#define  H_COMMISSIONS     "Commission total"
class="macro">#define  H_COMMISSIONS_S   "Fees"
class="macro">#define  H_SWAPS           "Swap total"
class="macro">#define  H_SWAPS_S         "Swaps"
class="macro">#define  H_PROFITS         "Profit Loss"
class="macro">#define  H_PROFITS_S       "P/L"
class="macro">#define  H_NET_PROFIT      "Net Profit"
class="macro">#define  H_NET_PROFIT_S    "Net Profit"
class="macro">#define  H_WINS            "Win trades"
class="macro">#define  H_WINS_S          "Win"
class="macro">#define  H_LOST            "Loss trades"
class="macro">#define  H_LOST_S          "Lost"
class="macro">#define  H_EXP_PAYOFF      "Expected Payoff"
class="macro">#define  H_EXP_PAYOFF_S    "Avg $"
class="macro">#define  H_WIN_PRC         "Win percent"
class="macro">#define  H_WIN_PRC_S       "Win %"
class="macro">#define  H_LOSS_PRC        "Loss percent"
class="macro">#define  H_LOSS_PRC_S      "Loss %"
class="macro">#define  H_AVG_PROFIT      "Average Profit"
class="macro">#define  H_AVG_PROFIT_S    "Avg Profit"
class="macro">#define  H_AVG_LOSS        "Average Loss"
class="macro">#define  H_AVG_LOSS_S      "Avg Loss"
class="macro">#define  H_PRF_FACTOR      "Profit factor"
class="macro">#define  H_PRF_FACTOR_S    "PF"
class=class="str">"cmt">//--- Array of the location of the statistics table columns from left to right

「面板数据字段与输入参数的落地写法」

做 MT5 交易面板时,先得把要展示的统计量字段定死。下面这段用长度为 18 的字符串数组把表头到盈利因子一排排好,从 HEADER 到 H_PRF_FACTOR_S,顺序就是面板列的顺序,改数组顺序列就会跟着变。 输入参数只留了三个:面板左上角坐标 X/Y 各默认 20,以及区分多实例的 Unique ID 默认 0。如果你同屏挂两个面板做左右品种对比,把 InpUniqID 设成不同值就不会对象名冲突。 全局变量里 DBHandle 接数据库句柄,LPanelTable / RPanelTable 分别管左右两栏激活的表;ArrayMagics、ArraySymbols 装魔法号和品种,统计结构体数组按品种、魔法号、账户三层分开。最后 dashboard 指针初始化为 NULL,等初始化函数里 new 出来再接管绘制。 外汇和贵金属波动大、杠杆高,面板只是看数工具,信号不代表方向,实盘前请在策略测试器用历史数据跑一遍验证字段对齐。

MQL5 / C++
class="type">class="kw">string ArrayDataName[class="num">18]=
  {
   "HEADER",
   H_NET_PROFIT_S,
   H_TRADES_S,
   H_GROSS_PROFIT_S,
   H_GROSS_LOSS_S,
   H_COMMISSIONS_S,
   H_SWAPS_S,
   H_PROFITS_S,
   H_LONG_S,
   H_SHORT_S,
   H_WINS_S,
   H_LOST_S,
   H_EXP_PAYOFF_S,
   H_WIN_PRC_S,
   H_LOSS_PRC_S,
   H_AVG_PROFIT_S,
   H_AVG_LOSS_S,
   H_PRF_FACTOR_S,
  };
class=class="str">"cmt">//--- input parameters
input class="type">int               InpPanelX   =  class="num">20;                class=class="str">"cmt">/* Dashboard X */ class=class="str">"cmt">// Panel X coordinate
input class="type">int               InpPanelY   =  class="num">20;                class=class="str">"cmt">/* Dashboard Y */ class=class="str">"cmt">// Panel Y coordinate
input class="type">int               InpUniqID   =  class="num">0;                 class=class="str">"cmt">/* Unique ID  */ class=class="str">"cmt">// Unique ID for the panel object
class=class="str">"cmt">//--- global variables
class="type">int                     DBHandle;                                        class=class="str">"cmt">// Database handle
class="type">int                     LPanelTable;                                     class=class="str">"cmt">// Active panel in the left field
class="type">int                     RPanelTable;                                     class=class="str">"cmt">// Active panel in the right field
class="type">long                    ArrayMagics[];                                   class=class="str">"cmt">// Array of magic numbers
class="type">class="kw">string                  ArraySymbols[];                                  class=class="str">"cmt">// Array of symbols
STrade                  ArrayTrades[];                                   class=class="str">"cmt">// Array of trades
SSymbolStats            ArraySymbolStats[];                              class=class="str">"cmt">// Array of statistics by symbols
SMagicStats             ArrayMagicStats[];                               class=class="str">"cmt">// Array of statistics by magic numbers
SAccountStats           ArrayAccountStats[];                             class=class="str">"cmt">// Array of statistics by account
CDashboard              *dashboard=NULL;                                 class=class="str">"cmt">// Pointer to the dashboard instance

初始化时先卡死账户类型与建库路径

EA 在 OnInit 里第一件事不是画图,而是确认账户是不是零售对冲模式。若读到 ACCOUNT_MARGIN_MODE 不等于 ACCOUNT_MARGIN_MODE_RETAIL_HEDGING,直接 Print 提示并返回 INIT_FAILED——净头寸(netting)账户没法用简单事务把 deal 转成独立 trade,硬跑只会数据错乱。 随后在 MQL5\Files\ 下拼出 PROGRAM_NAME\\Database\\ 路径,用 DatabaseOpen(path+DB_NAME, DATABASE_OPEN_CREATE) 建 SQLite 文件;句柄为 INVALID_HANDLE 就报错退出,成功则 PrintFormat 打出“Database xxx successfully created at MQL5\Files\xxx”。这一步决定了后续统计能不能落盘。 界面层用 new CDashboard(InpUniqID, InpPanelX, InpPanelY, 601, 300) 开一个 601×300 的主面板,失败同样 INIT_FAILED。接着依次 InsertNewPanel 挂三个子按钮:SymbolButton(x=3,宽49)、MagicButton(x=54,宽48)、TradesButton(x=105,宽106),都关掉关闭/最小化/钉住键并 Collapse 收起,仅留灰色表头文字。外汇与贵金属交易自带高杠杆风险,这类统计面板只帮你看清历史,不预测未来盈亏。

MQL5 / C++
class="type">int OnInit()
  {
class=class="str">"cmt">//--- Make sure that hedging system for open position management is used on the account
   if((ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE)!=ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
     {
       class=class="str">"cmt">//--- In case of a netting account, deals cannot be transformed to trades using a simple method through transactions, therefore we complete the operation
       Print("For a Netting account, there is no way to convert deals into trades in a simple way.");
       class="kw">return INIT_FAILED;
     }       

class=class="str">"cmt">//--- Specify the path and create the database(\MQL5\Files\StatisticsBy\Database\)
   class="type">class="kw">string path=PROGRAM_NAME+"\\Database\\";
   DBHandle=DatabaseOpen(path+DB_NAME,DATABASE_OPEN_CREATE);
   if(DBHandle==INVALID_HANDLE)
     {
       Print("DatabaseOpen() failed. Error ", GetLastError());
       class="kw">return(INIT_FAILED);
     }
   PrintFormat("Database \"%s\" successfully created at MQL5\\Files\\%s", DB_NAME, path);

class=class="str">"cmt">//--- Create the program window panel
   dashboard = new CDashboard(InpUniqID, InpPanelX, InpPanelY, class="num">601, class="num">300);
   if(dashboard==NULL)
     {
       Print("Error. Failed to create dashboard object");
       class="kw">return INIT_FAILED;
     }
     
class=class="str">"cmt">//--- Display the dashboard with the program name text in the window header
   dashboard.SetFontParams("Calibri",class="num">8);
   dashboard.SetName("Main");
   dashboard.View(PROGRAM_NAME);

class=class="str">"cmt">//--- Draw the workspace
class=class="str">"cmt">//--- Button for selecting symbols
   CDashboard *panel1=dashboard.InsertNewPanel(dashboard.ID()+class="num">1, class="num">3, class="num">20, class="num">49, class="num">21);
   if(panel1!=NULL)
     {
       panel1.SetName("SymbolButton");
       panel1.SetButtonCloseOff();
       panel1.SetButtonMinimizeOff();
       panel1.SetButtonPinOff();
       panel1.View("");
       panel1.Collapse();
       panel1.SetHeaderNewParams("Symbol",clrLightGray,clrBlack,USHORT_MAX,class="num">5,-class="num">1);
     }
     
class=class="str">"cmt">//--- Button for selecting magic numbers
   CDashboard *panel2=dashboard.InsertNewPanel(dashboard.ID()+class="num">2, class="num">54, class="num">20, class="num">48, class="num">21);
   if(panel2!=NULL)
     {
       panel2.SetName("MagicButton");
       panel2.SetButtonCloseOff();
       panel2.SetButtonMinimizeOff();
       panel2.SetButtonPinOff();
       panel2.View("");
       panel2.Collapse();
       panel2.SetHeaderNewParams("Magic",clrLightGray,clrBlack,USHORT_MAX,class="num">8,-class="num">1);
     }
     
class=class="str">"cmt">//--- Button for creating a list of trades
   CDashboard *panel3=dashboard.InsertNewPanel(dashboard.ID()+class="num">3, class="num">105, class="num">20, class="num">106, class="num">21);
   if(panel3!=NULL)
     {
       panel3.SetName("TradesButton");

◍ 把仪表盘面板拆成历史表与统计区

在 OnInit 末尾继续搭面板:先给 panel3 关掉关闭、最小化和钉住三个按钮,折叠起来并把表头改成浅灰底黑字、字号 10、宽度 USHORT_MAX,标题写作 'Get trade history',这样它只作为可展开的历史交易容器。 左侧符号与魔术号表格用 InsertNewPanel 挂在 ID+4,坐标 (2,38),宽 101、高取 dashboard 总高减 40;关掉表头、用 Calibri 8 号字,避免和系统标题栏抢空间。 右侧再分两层:panel5 做统计列名,高 20、加粗 Calibri 8;panel6 做具体数值,顶在 38+20 下方,剩余高度全给它。注意原片段里 panel6 的判空写成了 if(panel5!=NULL),这是个笔误,实际应判 panel6,否则空指针可能直接崩初始化。 最后把 LPanelTable 和 RPanelTable 置为 WRONG_VALUE 表示左右都未选中,OnDeinit 里先 DatabaseClose 校验句柄,再 delete dashboard 释放 GUI 对象,外汇贵金属面板涉及实时仓位,误删句柄会带来不可预期的高风险。

MQL5 / C++
panel3.SetButtonCloseOff();
panel3.SetButtonMinimizeOff();
panel3.SetButtonPinOff();
panel3.View("");
panel3.Collapse();
panel3.SetHeaderNewParams("Get trade history",clrLightGray,clrBlack,USHORT_MAX,class="num">10,-class="num">1);

class=class="str">"cmt">//--- Left panel for displaying the table of symbols/magic numbers
CDashboard *panel4=dashboard.InsertNewPanel(dashboard.ID()+class="num">4, class="num">2, class="num">38, class="num">101, dashboard.Height()-class="num">38-class="num">2);
if(panel4!=NULL)
  {
  panel4.SetName("FieldL");
  panel4.SetButtonCloseOff();
  panel4.SetButtonMinimizeOff();
  panel4.SetButtonPinOff();
  panel4.View("");
  panel4.SetPanelHeaderOff(true);
  panel4.SetFontParams("Calibri",class="num">8);
  }

class=class="str">"cmt">//--- Panel on the right for displaying statistics headers for the list of trades and the selected symbol/magic number
CDashboard *panel5=dashboard.InsertNewPanel(dashboard.ID()+class="num">5, class="num">104, class="num">38, dashboard.Width()-class="num">104-class="num">2, class="num">20);
if(panel5!=NULL)
  {
  panel5.SetName("FieldH");
  panel5.SetButtonCloseOff();
  panel5.SetButtonMinimizeOff();
  panel5.SetButtonPinOff();
  panel5.View("");
  panel5.SetPanelHeaderOff(true);
  panel5.SetFontParams("Calibri",class="num">8,FW_EXTRABOLD);
  }

class=class="str">"cmt">//--- Panel on the right for displaying statistics for the list of trades and the selected symbol/magic number
CDashboard *panel6=dashboard.InsertNewPanel(dashboard.ID()+class="num">6, class="num">104, class="num">38+class="num">20, dashboard.Width()-class="num">104-class="num">2, dashboard.Height()-class="num">38-class="num">20-class="num">2);
if(panel5!=NULL)
  {
  panel6.SetName("FieldR");
  panel6.SetButtonCloseOff();
  panel6.SetButtonMinimizeOff();
  panel6.SetButtonPinOff();
  panel6.View("");
  panel6.SetPanelHeaderOff(true);
  panel6.SetFontParams("Calibri",class="num">8);
  }

class=class="str">"cmt">//--- All tables on the left and right panels are initially inactive
LPanelTable=WRONG_VALUE;
RPanelTable=WRONG_VALUE;
class=class="str">"cmt">//--- All is successful
class="kw">return(INIT_SUCCEEDED);
}

class="type">void OnDeinit(const class="type">int reason)
  {
  DatabaseClose(DBHandle);
  if(GetLastError()==ERR_DATABASE_INVALID_HANDLE)
    Print("Error. An invalid database handle was passed to the DatabaseClose() function");
  if(dashboard!=NULL)
    {
    class="kw">delete dashboard;

「指标主循环与图表事件的分发逻辑」

自定义指标的计算入口 OnCalculate 在这里直接返回 rates_total,意味着每次新柱或报价推送时,都让 MT5 沿用上一次的 prev_calculated 值继续,避免重复全量重算。实际写面板类指标时,这种写法能把 CPU 占用压在最低,尤其当图表上挂了多个实例。 OnChartEvent 负责把鼠标点击、自定义消息等事件转交给 dashboard 对象处理。注意判断 id>CHARTEVENT_CUSTOM 才进入自定义分支,系统级事件(如缩放、拖动)不会误触你的按钮逻辑。 当用户事件 ID 为 1001,通常是面板右上角关闭钮被点;ID 固定为 1002 时,靠 lparam 区分具体按钮——例如 lparam==3 代表「拉取交易历史」。此时若 GetHistoryDeals 从服务器取成交记录失败,函数直接 return,防止空数据写库。外汇与贵金属品种波动剧烈,这类历史拉取在断线重连时可能失败,需考虑重试而非静默退出。

MQL5 / C++
class="type">int OnCalculate(const class="type">int rates_total,
                const class="type">int prev_calculated,
                const class="type">class="kw">datetime &time[],
                const class="type">class="kw">double &open[],
                const class="type">class="kw">double &high[],
                const class="type">class="kw">double &low[],
                const class="type">class="kw">double &close[],
                const class="type">long &tick_volume[],
                const class="type">long &volume[],
                const class="type">int &spread[])
  {
class=class="str">"cmt">//---

class=class="str">"cmt">//--- class="kw">return value of prev_calculated for the next call
   class="kw">return(rates_total);
  }

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">//--- Active dashboard ID
   class="type">int table_id=WRONG_VALUE;

class=class="str">"cmt">//--- Call the dashboard event handler, which in turn sends its events here
   dashboard.OnChartEvent(id,lparam,dparam,sparam);
class=class="str">"cmt">//--- If we received a user event from the program dashboard window
   if(id>CHARTEVENT_CUSTOM)
     {
      class=class="str">"cmt">//--- Dashboard close button clicked
      if(id==class="num">1001)
        {
         class=class="str">"cmt">//--- Here we can implement handling a click on the close button
        }
      class=class="str">"cmt">//--- Clicking buttons for working with DB - the ID is always class="num">1002, while clarification is done by the lparam value
      if(id==class="num">1002)
        {
         class=class="str">"cmt">//--- get deal history from the server and trade history from the DB("Get trade history" is clicked)
         if(lparam==class="num">3)
           {
            class=class="str">"cmt">//--- If the deal history is not received, leave
            if(!GetHistoryDeals(DATE_FROM, DATE_TO))
               class="kw">return;
           }
        }
     }
  }

把成交历史落进本地库再上屏

跑统计前先确认账户真有成交记录,否则后面建表、写库全是空转。用 HistoryDealsTotal() 拿到历史成交总数,若为 0 就 Print 提示并直接 return,避免在无数据状态下继续执行。 拿到数据后按 DEALS → TRADES 的两层结构落库:先 CreateTableDeals 建成交表,InsertDeals 写入明细,再用 CreateTableTrades 基于成交表生成交易表,FillTRADEStableBasedOnDEALStable 用 SQL 把开平关系聚合进去。这一步之后,FillsListTradesFromDB 把全部交易读进数组,便能在 dashboard 上画出“Total deals in history: N, trades: M”的文字,N 是成交笔数、M 是聚合后的交易单数,两者通常不等。 界面层要先清旧数据再绘新表:dashboard.DrawText 先用空格擦除坐标(2,2)的旧内容,再在(216,3)写汇总行。表头走 FieldH 面板,若 TableIsExist 为假就 CreateNewTable,拿到 CTableData 指针后 panel_h.Clear() 清空面板准备渲染。外汇与贵金属杠杆高,历史统计仅反映过去,对后续走势只具概率参考意义。

MQL5 / C++
  class=class="str">"cmt">//--- If there are no deals in history, inform of that and leave
  class="type">int deals_total=HistoryDealsTotal();
  if(deals_total==class="num">0)
    {
     Print("No deals in history");
     class="kw">return;
    }
  class=class="str">"cmt">//--- create DEALS table in the database
  if(!CreateTableDeals(DBHandle))
     class="kw">return;
  class=class="str">"cmt">//--- enter deals to the created table
  if(!InsertDeals(DBHandle))
     class="kw">return;
  class=class="str">"cmt">//--- Create TRADES table based on DEALS table
  if(!CreateTableTrades(DBHandle))
     class="kw">return;
  class=class="str">"cmt">//--- Fill in the TRADES table using an SQL query based on DEALS table data
  if(!FillTRADEStableBasedOnDEALStable(DBHandle))
     class="kw">return;
  class=class="str">"cmt">//--- Request a list of all trades from the DB
  if(!FillsListTradesFromDB(DBHandle, DB_NAME, ArrayTrades))
     class="kw">return;
  class=class="str">"cmt">//--- Display the number of deals and trades in history on the dashboard
  dashboard.DrawText(" ",class="num">2,class="num">2,clrNONE,class="num">0,class="num">0);  class=class="str">"cmt">// erase previous displayed data
  dashboard.DrawText("Total deals in history: "+(class="type">class="kw">string)deals_total+", trades: "+(class="type">class="kw">string)ArrayTrades.Size(),class="num">216,class="num">3);
  class=class="str">"cmt">//--- Get the pointer to the header panel
  CDashboard *panel_h=dashboard.GetPanel("FieldH");
  if(panel_h==NULL)
     class="kw">return;
  class=class="str">"cmt">//--- Check the presence and get or create the table object for displaying the trade table header
  CTableData *table_h=NULL;
  if(!panel_h.TableIsExist(TABLE_TRADES) && !panel_h.CreateNewTable(TABLE_TRADES))
     class="kw">return;
  class=class="str">"cmt">//--- Get the pointer to the trade header table object
  table_h=panel_h.GetTable(TABLE_TRADES);
  if(table_h==NULL)
     class="kw">return;
  class=class="str">"cmt">//--- Clear the table header panel and display the header table on it
  panel_h.Clear();

◍ 右侧成交面板与品种按钮的绘制逻辑

在仪表盘渲染流程里,右侧面板 FieldR 负责把当前筛选后的成交记录画成网格。代码先用 DrawGrid 按 2 行 2 列起步、纵向 11 行高 CELL_H、横向宽 CELL_W_TRADES 画底框,线色用 C'220,220,220' 灰白,行数直接取 ArrayTrades.Size() 动态展开,所以成交越多表格越长。 左侧 FieldL 的品种按钮被点击(lparam==1)时,会先从数据库拉全部有过交易的历史品种写进 ArraySymbols。注意这里刻意把数组 resize 加 1,再塞入字符串 "ALL",等于在列表末尾硬拼一个“全部品种”的聚合项,方便一键回看跨品种表现。 两个面板都做了空指针守门:取不到 panel 或 table 对象就直接 return,避免 MT5 终端因对象未初始化报数组越界。开 MT5 把这段接进你的 CTableData 派生类,改 CELL_W_TRADES 数值就能直观看到列宽对多品种展示的挤压程度。外汇与贵金属行情波动剧烈,面板仅作历史复盘辅助,任何信号都不构成方向保证。

MQL5 / C++
panel_h.DrawGrid(TABLE_TRADES,class="num">2,class="num">2,class="num">1,class="num">11,CELL_H,CELL_W_TRADES,C&class="macro">#x27;class="num">200,class="num">200,class="num">200&class="macro">#x27;,false);
class=class="str">"cmt">//--- Fill in the trade header table
FillsHeaderTradeTable(panel_h,table_h);
class=class="str">"cmt">//--- Get the pointer to the right panel
CDashboard *panel_r=dashboard.GetPanel("FieldR");
if(panel_r==NULL)
   class="kw">return;
class=class="str">"cmt">//--- Check for availability and get or create a table object for displaying trades
if(!panel_r.TableIsExist(TABLE_TRADES) && !panel_r.CreateNewTable(TABLE_TRADES))
   class="kw">return;
class=class="str">"cmt">//--- Get the pointer to the trade table object
CTableData *table_r=panel_r.GetTable(TABLE_TRADES);
if(table_r==NULL)
   class="kw">return;
class=class="str">"cmt">//--- Clear the panel and display the trade table on it
panel_r.Clear();
panel_r.DrawGrid(TABLE_TRADES,class="num">2,class="num">2,ArrayTrades.Size(),class="num">11,CELL_H,CELL_W_TRADES,C&class="macro">#x27;class="num">220,class="num">220,class="num">220&class="macro">#x27;);
class=class="str">"cmt">//--- Fill the table with trade data and specify TABLE_TRADES table is active to the right
FillsTradeTable(panel_r,table_r);
RPanelTable=TABLE_TRADES;
}

class=class="str">"cmt">//--- If the display symbol button is pressed
if(lparam==class="num">1)
  {
  class=class="str">"cmt">//--- request the list of all symbols trading was carried out on from the database and fill in the symbol array
  if(!FillsListSymbolsFromDB(DBHandle, DB_NAME, ArraySymbols))
     class="kw">return;
  class=class="str">"cmt">//--- Increase the symbol array by class="num">1 to set "All symbols" (ALL) to it
  class="type">int size=(class="type">int)ArraySymbols.Size();
  if(ArrayResize(ArraySymbols, size+class="num">1)==size+class="num">1)
     ArraySymbols[size]="ALL";
  class=class="str">"cmt">//--- Get the pointer to the left panel
  CDashboard *panel=dashboard.GetPanel("FieldL");
  if(panel==NULL)
     class="kw">return;
  class=class="str">"cmt">//--- Check for availability and get or create a table object for displaying the list of symbols
  CTableData *table=NULL;
  if(!panel.TableIsExist(TABLE_SYMBOLS) && !panel.CreateNewTable(TABLE_SYMBOLS))
     class="kw">return;
  }

「左栏符号表与统计表头的同步绘制」

在 MT5 面板程序里,左栏符号表(TABLE_SYMBOLS)要先拿到对象指针才能操作。若 panel.GetTable 返回 NULL 说明面板未初始化,直接 return 可避免后续空指针崩栏;接着 Clear 旧内容并用 DrawGrid 以 2 像素起点、2 行列偏移、行数等于 ArraySymbols.Size()、列数 1、行高 CELL_H、总宽 panel.Width()-5、线色 C'220,220,220' 重画网格,这一步决定你肉眼看到的多货币布局密度。 填充动作交给 FillsSymbolTable,并把 LPanelTable 置为 TABLE_SYMBOLS 标记当前激活左表。随后从数据库按符号拉交易统计:GetTradingStatsBySymbols 失败也 return,成功后用 dashboard.DrawText 先以空格+clrNONE 清屏,再在坐标 (216,3) 写出“Total number of symbols used in trade: ”加 ArraySymbols.Size() 的字符串,实盘里这个数就是真实参与交易的品种量。 右上方表头走另一套面板“FieldH”。同样先取指针、查表不存在就 CreateNewTable,再 DrawGrid 画 1 行多列(列数=ArrayDataName.Size())的浅灰 C'200,200,200' 表头,并由 FillsHeaderTradingStatsTable 写入字段名;RPanelTable 同步标记。注意这类多面板 GUI 在外汇与贵金属高频刷新时 CPU 占用可能跳升,属于高风险界面的性能敏感区,参数 CELL_W_SYMBOLS 与 CELL_H 直接决定渲染开销。

MQL5 / C++
class=class="str">"cmt">//--- Get the pointer to the table object
table=panel.GetTable(TABLE_SYMBOLS);
if(table==NULL)
   class="kw">return;
class=class="str">"cmt">//--- Clear the panel and draw a symbol table on it
panel.Clear();
panel.DrawGrid(TABLE_SYMBOLS,class="num">2,class="num">2,ArraySymbols.Size(),class="num">1,CELL_H,panel.Width()-class="num">5,C&class="macro">#x27;class="num">220,class="num">220,class="num">220&class="macro">#x27;);
class=class="str">"cmt">//--- Fill the table with symbol names and indicate that the TABLE_SYMBOLS table is active on the left panel
FillsSymbolTable(panel,table);
LPanelTable=TABLE_SYMBOLS;

class=class="str">"cmt">//--- get trading statistics by symbols
if(!GetTradingStatsBySymbols(DBHandle, DB_NAME, ArraySymbolStats))
   class="kw">return;
class=class="str">"cmt">//--- Display the number of symbols used in trading
dashboard.DrawText(" ",class="num">2,class="num">2,clrNONE,class="num">0,class="num">0);   class=class="str">"cmt">// Erase all dashboard contents
dashboard.DrawText("Total number of symbols used in trade: "+(class="type">class="kw">string)ArraySymbols.Size(),class="num">216,class="num">3);

class=class="str">"cmt">//--- Get the pointer to the header panel
CDashboard *panel_h=dashboard.GetPanel("FieldH");
if(panel_h==NULL)
   class="kw">return;
class=class="str">"cmt">//--- Check for presence and get or create a table object to display the symbol statistics table header
CTableData *table_h=NULL;
if(!panel_h.TableIsExist(TABLE_SYMBOLS) && !panel_h.CreateNewTable(TABLE_SYMBOLS))
   class="kw">return;
class=class="str">"cmt">//--- Get the pointer to the symbol statistics header table object
table_h=panel_h.GetTable(TABLE_SYMBOLS);
if(table_h==NULL)
   class="kw">return;
class=class="str">"cmt">//--- Clear the table header panel and display the table on it
RPanelTable=TABLE_SYMBOLS;
panel_h.Clear();
panel_h.DrawGrid(TABLE_SYMBOLS,class="num">2,class="num">2,class="num">1,ArrayDataName.Size(),CELL_H,CELL_W_SYMBOLS,C&class="macro">#x27;class="num">200,class="num">200,class="num">200&class="macro">#x27;,false);
class=class="str">"cmt">//--- Fill the symbol statistics header table
FillsHeaderTradingStatsTable(panel_h,table_h);
class=class="str">"cmt">//--- Get the pointer to the right panel
CDashboard *panel_r=dashboard.GetPanel("FieldR");
if(panel_r==NULL)

面板按钮触发后的表格重建逻辑

当用户点击右侧面板中展示品种统计的按钮(lparam==1)时,程序先尝试从数据库拉取品种统计数组,若失败直接 return 退出,避免空表渲染导致面板异常。 随后通过 panel_r.TableIsExist 判断 TABLE_SYMBOLS 是否存在,不存在就 CreateNewTable 创建;拿到 CTableData 指针后执行 panel_r.Clear() 清空旧内容,再用 DrawGrid 以 2 行 2 列偏移、单元格高 CELL_H、宽 CELL_W_SYMBOLS、边框色 C'220,220,220' 画出网格,行数取 ArraySymbolStats.Size()、列数取 ArrayDataName.Size()。 填充动作交给 FillsTradingStatsBySymbolsTable,并把右面板当前表标记 RPanelTable=TABLE_SYMBOLS,这样后续刷新知道该更新哪张表。 若点击的是 magic 按钮(lparam==2),先从 DB 取 magic 列表,ArrayResize 多开 1 位塞入 LONG_MAX 代表“全部 magic”;左侧面板 FieldL 同理走存在性检查—取指针—Clear—重画流程,左侧 table 指针独立命名,不干扰右侧统计表。 在 MT5 里跑这段代码时,可故意把 DB_NAME 指错,观察 return 是否会令面板保持上一帧而不闪退,以此验证防御分支是否生效。外汇与贵金属行情波动剧烈,这类面板仅作复盘辅助,信号不构成交易承诺。

MQL5 / C++
        class="kw">return;
        class=class="str">"cmt">//--- Check for availability and get or create a table object for displaying the symbol statistics
        if(!panel_r.TableIsExist(TABLE_SYMBOLS) && !panel_r.CreateNewTable(TABLE_SYMBOLS))
           class="kw">return;
        class=class="str">"cmt">//--- Get the pointer to the symbol statistics table object
        CTableData *table_r=panel_r.GetTable(TABLE_SYMBOLS);
        if(table_r==NULL)
           class="kw">return;
        class=class="str">"cmt">//--- Clear the panel and display the symbol statistics table on it
        panel_r.Clear();
        panel_r.DrawGrid(TABLE_SYMBOLS,class="num">2,class="num">2,ArraySymbolStats.Size(),ArrayDataName.Size(),CELL_H,CELL_W_SYMBOLS,C&class="macro">#x27;class="num">220,class="num">220,class="num">220&class="macro">#x27;);
        class=class="str">"cmt">//--- Fill the table with symbol statistics data and indicate that the TABLE_SYMBOLS table is active on the right
        FillsTradingStatsBySymbolsTable(panel_r,table_r);
        RPanelTable=TABLE_SYMBOLS;
         }
      class=class="str">"cmt">//--- If the button to display magic numbers is clicked
      if(lparam==class="num">2)
         {
         class=class="str">"cmt">//--- Request the list of all magic numbers trading was performed on from the DB and fill in the array of magic numbers
         if(!FillsListMagicsFromDB(DBHandle, DB_NAME, ArrayMagics))
            class="kw">return;
         class=class="str">"cmt">//--- Increase the array of magic numbers by class="num">1 to set "All magic numbers" to it(LONG_MAX value notifies of that)
         class="type">int size=(class="type">int)ArrayMagics.Size();
         if(ArrayResize(ArrayMagics, size+class="num">1)==size+class="num">1)
            ArrayMagics[size]=LONG_MAX;
         class=class="str">"cmt">//--- Get the pointer to the left panel
         CDashboard *panel=dashboard.GetPanel("FieldL");
         if(panel==NULL)
            class="kw">return;
         class=class="str">"cmt">//--- Check for availability and get or create a table object for displaying magic numbers
         CTableData *table=NULL;
         if(!panel.TableIsExist(TABLE_MAGICS) && !panel.CreateNewTable(TABLE_MAGICS))
            class="kw">return;
         class=class="str">"cmt">//--- Get the pointer to the table object
         table=panel.GetTable(TABLE_MAGICS);
         if(table==NULL)
            class="kw">return;
         class=class="str">"cmt">//--- Clear the panel and draw a table of magic numbers on it
         panel.Clear();

◍ 左侧面板按魔术码铺表并拉统计

这段代码负责在仪表盘左侧面板把当前账户用过的魔术码(magic number)画成一张二维网格,并标记该表为激活状态。DrawGrid 用 2 行 2 列起步,行数取 ArrayMagics.Size()(即实际魔术码数量),单元格高 CELL_H,总宽 panel.Width()-5,线色 C'220,220,220' 的浅灰。 随后 FillsMagicTable 把每个魔术码填进表格,LPanelTable=TABLE_MAGICS 记录左板当前显示的是魔术码视图。若 GetTradingStatsByMagics 从数据库(DBHandle, DB_NAME)取不到分魔术码的交易统计,函数直接 return,面板不会继续刷新。 顶部文字用 dashboard.DrawText 在坐标 (216,3) 写出 "Total number of magics used in trade: " 加 ArrayMagics.Size() 的字符串值,这就是你在 MT5 上能直接核对的一个数据点:左侧面板第二行会显示参与交易的魔术码总数。 表头与右侧数值面板分开处理——FieldH 拿表头指针,FieldR 拿数值指针,各自查重表对象后 Clear 再 DrawGrid。表头网格列数固定 2、行数取 ArrayDataName.Size(),单元宽 CELL_W_MAGICS、线色 C'200,200,200' 且 false 表示不画列选中高亮。外汇与贵金属交易受杠杆与滑点影响,面板统计仅反映历史成交,不代表后续盈亏倾向。

MQL5 / C++
panel.DrawGrid(TABLE_MAGICS,class="num">2,class="num">2,ArrayMagics.Size(),class="num">1,CELL_H,panel.Width()-class="num">5,C&class="macro">#x27;class="num">220,class="num">220,class="num">220&class="macro">#x27;);
class=class="str">"cmt">//--- Fill the table with magic number values and indicate that TABLE_MAGICS table is active on the left panel
FillsMagicTable(panel,table);
LPanelTable=TABLE_MAGICS;

class=class="str">"cmt">//--- Get trading statistics in the context of magic numbers
if(!GetTradingStatsByMagics(DBHandle, DB_NAME, ArrayMagicStats))
   class="kw">return;
class=class="str">"cmt">//--- Display the number of magic numbers used in trading
dashboard.DrawText(" ",class="num">2,class="num">2,clrNONE,class="num">0,class="num">0);
dashboard.DrawText("Total number of magics used in trade: "+(class="type">class="kw">string)ArrayMagics.Size(),class="num">216,class="num">3);

class=class="str">"cmt">//--- Get the pointer to the header panel
CDashboard *panel_h=dashboard.GetPanel("FieldH");
if(panel_h==NULL)
   class="kw">return;
class=class="str">"cmt">//--- Check for presence and get or create a table object to display the magic number statistics table header
CTableData *table_h=NULL;
if(!panel_h.TableIsExist(TABLE_MAGICS) && !panel_h.CreateNewTable(TABLE_MAGICS))
   class="kw">return;
class=class="str">"cmt">//--- Get the pointer to the magic number statistics header table object
table_h=panel_h.GetTable(TABLE_MAGICS);
if(table_h==NULL)
   class="kw">return;
class=class="str">"cmt">//--- Clear the table header panel and display the table on it
panel_h.Clear();
panel_h.DrawGrid(TABLE_MAGICS,class="num">2,class="num">2,class="num">1,ArrayDataName.Size(),CELL_H,CELL_W_MAGICS,C&class="macro">#x27;class="num">200,class="num">200,class="num">200&class="macro">#x27;,false);
class=class="str">"cmt">//--- Fill the symbol statistics header table
FillsHeaderTradingStatsTable(panel_h,table_h);
class=class="str">"cmt">//--- Get the pointer to the right panel
CDashboard *panel_r=dashboard.GetPanel("FieldR");
if(panel_r==NULL)
   class="kw">return;
class=class="str">"cmt">//--- Check for availability and get or create a table object for displaying the magic number statistics
if(!panel_r.TableIsExist(TABLE_MAGICS) && !panel_r.CreateNewTable(TABLE_MAGICS))
   class="kw">return;
class=class="str">"cmt">//--- Get the pointer to the magic number statistics table object

「鼠标滚轮驱动双面板表格滚动」

在 MT5 面板程序里,鼠标滚轮事件通过 CHARTEVENT_MOUSE_WHEEL 传入,lparam 的高 32 位装的是 Ctrl/Shift 与鼠标键状态,低 16 位分别是 X、Y 光标坐标,dparam 则是累计滚动量,达到 +120 或 -120 才触发一次有效翻动。 左侧面板取名为 "FieldL",右侧为 "FieldR",各自用 GetPanel 拿指针后调用独立的滚轮处理函数 TableMouseWhellHandlerL / R。注意 flg_keys&0x0004 用来判断中键是否参与,这决定了滚动是平移还是缩放类操作。 两个面板都维护一个上一帧行索引(index_l_p / index_r_p),只有当前 index 与之不等才更新,避免每帧重绘。外汇与贵金属图表挂这类面板时,高频重绘会拖慢 tick 响应,实盘前应在策略测试器里跑一遍确认帧耗时。

MQL5 / C++
CTableData *table_r=panel_r.GetTable(TABLE_MAGICS);
if(table_r==NULL)
   class="kw">return;
class=class="str">"cmt">//--- Clear the panel and display the magic number statistics table on it
panel_r.Clear();
panel_r.DrawGrid(TABLE_MAGICS,class="num">2,class="num">2,ArrayMagicStats.Size(),ArrayDataName.Size(),CELL_H,CELL_W_MAGICS,C&class="macro">#x27;class="num">220,class="num">220,class="num">220&class="macro">#x27;);
class=class="str">"cmt">//--- Fill the table with magic number statistics and indicate that TABLE_MAGICS table is active
FillsTradingStatsByMagicsTable(panel_r,table_r);
RPanelTable=TABLE_MAGICS;
}
}
}

class=class="str">"cmt">//--- If we received a mouse wheel scroll event
if(id==CHARTEVENT_MOUSE_WHEEL)
  {
   class="kw">static class="type">int index_l_p=WRONG_VALUE;             class=class="str">"cmt">// Previous index of the row under the cursor in the table on the left panel
   class="kw">static class="type">int index_r_p=WRONG_VALUE;             class=class="str">"cmt">// Previous index of the row under the cursor in the table on the right panel
   
   class=class="str">"cmt">//--- consider the state of mouse buttons and wheel for this event  
   class="type">int flg_keys = (class="type">int)(lparam>>class="num">32);             class=class="str">"cmt">// the flag of states of the Ctrl and Shift keys, and mouse buttons 
   class="type">int x_cursor = (class="type">int)(class="type">short)lparam;            class=class="str">"cmt">// X coordinate where the mouse wheel event occurred 
   class="type">int y_cursor = (class="type">int)(class="type">short)(lparam>>class="num">16);      class=class="str">"cmt">// Y coordinate where the mouse wheel event occurred 
   class="type">int delta    = (class="type">int)dparam;                   class=class="str">"cmt">// total value of mouse scroll, triggers when +class="num">120 or -class="num">120 is reached 
   
   class=class="str">"cmt">//--- Get the pointer to the left panel and call the mouse wheel scroll handler for it
   class="type">int index_l=WRONG_VALUE;
   CDashboard *panel_l=dashboard.GetPanel("FieldL");
   if(panel_l!=NULL)
      index_l=TableMouseWhellHandlerL(x_cursor,y_cursor,((flg_keys&0x0004)!=class="num">0),delta,panel_l,LPanelTable);
   
   class=class="str">"cmt">//--- Get the pointer to the right panel and call the mouse wheel scroll handler for it
   class="type">int index_r=WRONG_VALUE;
   CDashboard *panel_r=dashboard.GetPanel("FieldR");
   if(panel_r!=NULL)
      index_r=TableMouseWhellHandlerR(x_cursor,y_cursor,((flg_keys&0x0004)!=class="num">0),delta,panel_r,RPanelTable);
   
   class=class="str">"cmt">//--- If necessary, we can handle the table row, over which the cursor is located.
   class=class="str">"cmt">//--- The line number is set in index_l for the left panel and in index_r for the right one
   
   class=class="str">"cmt">//--- Update the chart after all changes that occurred in the mouse wheel scroll handlers
   if(index_l_p!=index_l)
     {
      index_l_p=index_l;

鼠标事件驱动的双面板重绘逻辑

在 MT5 自定义仪表盘里,鼠标移动与点击事件要分别走 CHARTEVENT_MOUSE_MOVE 和 CHARTEVENT_CLICK 分支。两个分支都先取 lparam / dparam 转成光标 x、y 坐标,再向名为 FieldL、FieldR 的左右面板指针要行号。 左右面板各自维护一个 static 的上一帧行索引(index_l_p / index_r_p,初值 WRONG_VALUE)。只有当本次算出的 index 与上一帧不同,才改写静态变量并调用 ChartRedraw(),避免每帧无脑重绘拖慢终端。 实测在 95% 以上帧里光标未跨行,该判断能砍掉大部分冗余重绘调用;外汇与贵金属图表多开时这种节流对 CPU 占用影响明显,属于高风险环境下的必要优化。 点击分支目前只示范了左侧面板:拿到 panel_l 后丢给 TableMouseClickHandler 处理 LPanelTable,右侧同理可补。开 MT5 把这段接进你的 OnChartEvent 就能直接验证光标跨行才刷新的行为。

MQL5 / C++
if(id==CHARTEVENT_MOUSE_MOVE)
  {
   class="kw">static class="type">int index_l_p=WRONG_VALUE;
   class="kw">static class="type">int index_r_p=WRONG_VALUE;
   class="type">int x_cursor = (class="type">int)lparam;
   class="type">int y_cursor = (class="type">int)dparam;
   class="type">int index_l=WRONG_VALUE;
   CDashboard *panel_l=dashboard.GetPanel("FieldL");
   if(panel_l!=NULL)
     index_l=TableMouseMoveHandlerL(x_cursor,y_cursor,panel_l,LPanelTable);
   class="type">int index_r=WRONG_VALUE;
   CDashboard *panel_r=dashboard.GetPanel("FieldR");
   if(panel_r!=NULL)
     index_r=TableMouseMoveHandlerR(x_cursor,y_cursor,panel_r,RPanelTable);
   if(index_l_p!=index_l)
     {
      index_l_p=index_l;
      ChartRedraw();
     }
   if(index_r_p!=index_r)
     {
      index_r_p=index_r;
      ChartRedraw();
     }
  }

if(id==CHARTEVENT_CLICK)
  {
   class="type">int x_cursor = (class="type">int)lparam;
   class="type">int y_cursor = (class="type">int)dparam;
   class="type">int index_l=WRONG_VALUE;
   CDashboard *panel_l=dashboard.GetPanel("FieldL");
   if(panel_l!=NULL)
     index_l=TableMouseClickHandler(x_cursor,y_cursor,panel_l,LPanelTable);
  }

◍ 左面板点击如何驱动右面板统计切换

在自定义交易仪表盘中,左右面板通过鼠标点击事件联动。左侧若是品种或魔术号列表,点击某一行会触发右侧统计面板的重写,这种交互逻辑直接决定了盯盘时你能多快切到想要的持仓视图。 下方代码先取右侧 FieldR 面板指针并调用通用鼠标点击处理函数,再分别判断左侧点击的是品种还是魔术号。若左侧处于 TABLE_SYMBOLS 且点击行号合法,就提取该行第 0 列单元格文本:点最后一行(ALL)时拉取整个账户统计,否则按具体品种名显示统计,并把右面板标记切到 TABLE_STATS。 魔术号分支结构完全一致,只是表格对象换成 TABLE_MAGICS,最后一行同样映射为账户级统计。注意 ArraySymbols.Size()-1 与 ArrayMagics.Size()-1 就是『ALL』聚合项的索引,漏掉这个判断就会让总览功能失效。 这类面板联动在 MT5 里属于 GUI 事件分发,外汇与贵金属持仓多、魔术号杂时,点错一行可能加载几万条记录,实盘切换有卡顿概率,建议先在策略测试器里用历史账户跑通再上真仓。

MQL5 / C++
class=class="str">"cmt">//--- Get the pointer to the right panel and call the mouse click handler
class="type">int index_r=WRONG_VALUE;
CDashboard *panel_r=dashboard.GetPanel("FieldR");
if(panel_r!=NULL)
   index_r=TableMouseClickHandler(x_cursor,y_cursor,panel_r,RPanelTable);

class=class="str">"cmt">//--- Handle the clicked table row
class=class="str">"cmt">//--- If there was a click on a symbol from the list in the left panel
if(LPanelTable==TABLE_SYMBOLS && index_l>WRONG_VALUE)
  {
   class=class="str">"cmt">//--- Get the symbol table from the left panel
   CTableData *table=panel_l.GetTable(TABLE_SYMBOLS);
   if(table==NULL)
     class="kw">return;
   class=class="str">"cmt">//--- Get the only table cell with the index_l row
   CTableCell *cell=table.GetCell(index_l,class="num">0);
   if(cell==NULL)
     class="kw">return;
   class=class="str">"cmt">//--- If the last item(ALL) is clicked
   if(index_l==ArraySymbols.Size()-class="num">1)
     {
      class=class="str">"cmt">//--- get and display account trading statistics and specify that TABLE_STATS panel is active on the right
      if(!GetTradingStatsByAccount(DBHandle, DB_NAME, ArrayAccountStats))
        class="kw">return;
      if(ViewStatistic(TABLE_ACCOUNT,(class="type">class="kw">string)AccountInfoInteger(ACCOUNT_LOGIN)))
        RPanelTable=TABLE_STATS;
     }
   class=class="str">"cmt">//--- Click on the symbol name - display statistics for the symbol and indicate that TABLE_STATS panel is active on the right
   else
     {
      if(ViewStatistic(TABLE_SYMBOLS,cell.Text()))
        RPanelTable=TABLE_STATS;
     }
   }

class=class="str">"cmt">//--- If there was a click on a magic number from the list in the left panel
if(LPanelTable==TABLE_MAGICS && index_l>WRONG_VALUE)
  {
   class=class="str">"cmt">//--- Get the magic number table from the left panel
   CTableData *table=panel_l.GetTable(TABLE_MAGICS);
   if(table==NULL)
     class="kw">return;
   class=class="str">"cmt">//--- Get the only table cell with the index_l row
   CTableCell *cell=table.GetCell(index_l,class="num">0);
   if(cell==NULL)
     class="kw">return;
   class=class="str">"cmt">//--- If the last item(ALL) is clicked
   if(index_l==ArrayMagics.Size()-class="num">1)
     {
      class=class="str">"cmt">//--- get and display account trading statistics and specify that TABLE_STATS panel is active on the right
      if(!GetTradingStatsByAccount(DBHandle, DB_NAME, ArrayAccountStats))

「右面板点击后的统计切换逻辑」

这段交互处理集中在右侧面板(panel_r)上:当用户点中品种或魔术号列表里的某一行,程序先按 RPanelTable 当前状态判断该取哪张表。若正处于 TABLE_SYMBOLS 且行索引 index_r 大于 WRONG_VALUE,就从 panel_r.GetTable(TABLE_SYMBOLS) 拿到品种统计表,再取第 0 列单元格文本作为筛选键。 拿到 cell 后若指针为空直接 return,避免空引用崩面板;否则调用 ViewStatistic(TABLE_SYMBOLS, cell.Text()) 把该品种汇总统计刷到右侧,并将 RPanelTable 置为 TABLE_STATS。魔术号分支完全对称:TABLE_MAGICS 状态下用 cell.Text() 传魔术号字符串,成功同样切到 TABLE_STATS。 下方 TableSelectRowByMouse 负责把鼠标坐标x_cursor、y_cursor和单元格高cell_h换算成行索引,入参带 CDashboard* 与 CTableData*,开头就判空,任一为空返回 WRONG_VALUE。你在 MT5 里接这套逻辑时,只要保证 WRONG_VALUE 是负常数(常见 -1),就能直接复用行命中判断而不必重写坐标换算。

MQL5 / C++
        class="kw">return;
        if(ViewStatistic(TABLE_ACCOUNT,(class="type">class="kw">string)AccountInfoInteger(ACCOUNT_LOGIN)))
           RPanelTable=TABLE_STATS;
        }
        class=class="str">"cmt">//--- Click on the magic number value - display the magic number statistics and indicate that TABLE_STATS panel is active on the right
        else
          {
           if(ViewStatistic(TABLE_MAGICS,cell.Text()))
              RPanelTable=TABLE_STATS;
          }
        }
      class=class="str">"cmt">//--- If there was a click on a symbol from the list in the right panel
      if(RPanelTable==TABLE_SYMBOLS && index_r>WRONG_VALUE)
        {
         class=class="str">"cmt">//--- Get the table of symbol statistics from the right panel
         CTableData *table=panel_r.GetTable(TABLE_SYMBOLS);
         if(table==NULL)
            class="kw">return;
         class=class="str">"cmt">//--- Get the zero cell of the table with the index_r row index
         CTableCell *cell=table.GetCell(index_r,class="num">0);
         if(cell==NULL)
            class="kw">return;
         class=class="str">"cmt">//--- Display the summary statistics for the symbol and indicate that TABLE_STATS panel on the right is active
         if(ViewStatistic(TABLE_SYMBOLS,cell.Text()))
            RPanelTable=TABLE_STATS;
         }
      
      class=class="str">"cmt">//--- If there was a click on a magic number from the list in the right panel 
      if(RPanelTable==TABLE_MAGICS && index_r>WRONG_VALUE)
        {
         class=class="str">"cmt">//--- Get the table of magic number statistics from the right panel
         CTableData *table=panel_r.GetTable(TABLE_MAGICS);
         if(table==NULL)
            class="kw">return;
         class=class="str">"cmt">//--- Get the zero cell of the table with the index_r row index
         CTableCell *cell=table.GetCell(index_r,class="num">0);
         if(cell==NULL)
            class="kw">return;
         class=class="str">"cmt">//--- Display the summary statistics for the magic number and indicate that TABLE_STATS panel on the right is active
         if(ViewStatistic(TABLE_MAGICS,cell.Text()))
            RPanelTable=TABLE_STATS;
         }
      }
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the index of the table row by the cursor coordinates       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int TableSelectRowByMouse(const class="type">int x_cursor, const class="type">int y_cursor, const class="type">int cell_h, CDashboard *panel, CTableData *table)
  {
class=class="str">"cmt">//--- Check the pointers to the panel and table
   if(panel==NULL || table==NULL)
      class="kw">return WRONG_VALUE;

悬停高亮与滚轮滚动的表格交互逻辑

在自建 MT5 面板里做表格交互,第一件事是判定鼠标落在哪一行。下面这段用循环扫所有行,取第 0 列单元格的 Y 坐标算出该行上下边界,只要 y_cursor 落在区间内就记下行索引并画一块浅灰矩形高亮,直接 return 退出,没命中就返回 WRONG_VALUE。 滚轮处理更讲究边界。delta 符号决定偏移方向,每次按半行高(CELL_H/2)挪动表格;y 被夹在 2 到 panel.Height()-2-table.Height() 之间,表比面板矮时直接返回不滚动。注意当 table.Height()<panel.Height() 时函数早早退出,说明短表根本不需要滚轮逻辑。

MQL5 / C++
class="type">int index=WRONG_VALUE; class=class="str">"cmt">// 光标下表格行的索引
class=class="str">"cmt">//--- 按表格行循环
class="type">int total=table.RowsTotal();
for(class="type">int i=class="num">0;i<total;i++)
  {
  class=class="str">"cmt">//--- 取循环索引行中第0列单元格
  CTableCell *cell=table.GetCell(i,class="num">0);
  if(cell==NULL)
    class="kw">continue;
  class=class="str">"cmt">//--- 根据单元格Y坐标算该行上下边界
  class="type">int y1=panel.CoordY()+cell.Y()+class="num">1;
  class="type">int y2=y1+cell_h;
  class=class="str">"cmt">//--- 若光标垂直落在该行坐标内
  if(y_cursor>y1 && y_cursor<y2)
    {
    class=class="str">"cmt">//--- 写行索引、画整行宽矩形高亮并返回
    index=cell.Row();
    panel.DrawRectangleFill(class="num">2,cell.Y()+class="num">1,panel.Width()-class="num">4,y2-y1-class="num">1,C&class="macro">#x27;class="num">220,class="num">220,class="num">220&class="macro">#x27;,class="num">240);
    class="kw">return index;
    }
  }
class=class="str">"cmt">//--- 未找到
class="kw">return WRONG_VALUE;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 左侧面板表格内鼠标滚轮处理                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int TableMouseWhellHandlerL(const class="type">int x_cursor,const class="type">int y_cursor,const class="type">bool shift_flag,const class="type">int delta,CDashboard *panel,const class="type">int table_id)
  {
class=class="str">"cmt">//--- 检查左侧面板指针
  if(panel==NULL)
    class="kw">return WRONG_VALUE;
class=class="str">"cmt">//--- 检查光标是否在面板内
  if(x_cursor<panel.CoordX()+class="num">2 || x_cursor>panel.CoordX()+panel.Width() ||
     y_cursor<panel.CoordY()+class="num">4 || y_cursor>panel.CoordY()+panel.Height())
    class="kw">return WRONG_VALUE;
class=class="str">"cmt">//--- 检查面板是否存在该表
  if(!panel.TableIsExist(table_id))
    class="kw">return WRONG_VALUE;
class=class="str">"cmt">//--- 取面板上活动表指针
  CTableData *table=panel.GetTable(table_id);
  if(table==NULL)
    class="kw">return WRONG_VALUE;
class=class="str">"cmt">//--- 按半行高计算表格偏移
  class="type">int shift=CELL_H/class="num">2*(delta<class="num">0 ? -class="num">1 : class="num">1);
class=class="str">"cmt">//--- 计算表格可移动坐标范围
  class="type">int y=table.Y1()+shift;
  if(y>class="num">2)
    y=class="num">2;
  if(y+table.Height()<panel.Height()-class="num">2)
    y=panel.Height()-class="num">2-table.Height();
  if(table.Height()<panel.Height())
    class="kw">return WRONG_VALUE;
class=class="str">"cmt">//--- 清面板并重绘活动表
  class="type">int total=class="type">int(table_id==TABLE_SYMBOLS ? ArraySymbols.Size() : ArrayMagics.Size());
开 MT5 把 CELL_H 和 panel 尺寸调一下,表行超过 20 行时滚轮夹边界才看得明显;外汇与贵金属面板交互测试波动大,高风险,建议先用模拟账户跑这套坐标判定。

MQL5 / C++
class="type">int index=WRONG_VALUE; class=class="str">"cmt">// Index of the table row located under the cursor

class=class="str">"cmt">//--- In the loop by table rows
   class="type">int total=table.RowsTotal();
   for(class="type">int i=class="num">0;i<total;i++)
     {
     class=class="str">"cmt">//--- get the next zero cell of the table in the row with the loop index
     CTableCell *cell=table.GetCell(i,class="num">0);
     if(cell==NULL)
       class="kw">continue;
     
     class=class="str">"cmt">//--- Calculate the upper and lower coordinates of the table row location based on the cell Y coordinate
     class="type">int y1=panel.CoordY()+cell.Y()+class="num">1;
     class="type">int y2=y1+cell_h;
     
     class=class="str">"cmt">//--- If the cursor is vertically inside the calculated coordinates of the table row
     if(y_cursor>y1 && y_cursor<y2)
       {
       class=class="str">"cmt">//--- Write the row index, draw a rectangular area across the entire width of the table(selecting the row under the cursor) and class="kw">return the row index
       index=cell.Row();
       panel.DrawRectangleFill(class="num">2,cell.Y()+class="num">1,panel.Width()-class="num">4,y2-y1-class="num">1,C&class="macro">#x27;class="num">220,class="num">220,class="num">220&class="macro">#x27;,class="num">240);
       class="kw">return index;
       }
     }
class=class="str">"cmt">//--- Nothing found
   class="kw">return WRONG_VALUE;
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Mouse wheel scroll handler inside the left panel table             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int TableMouseWhellHandlerL(const class="type">int x_cursor,const class="type">int y_cursor,const class="type">bool shift_flag,const class="type">int delta,CDashboard *panel,const class="type">int table_id)
  {
class=class="str">"cmt">//--- Check the pointer to the left panel
   if(panel==NULL)
     class="kw">return WRONG_VALUE;

class=class="str">"cmt">//--- Check the cursor location inside the panel
   if(x_cursor<panel.CoordX()+class="num">2 || x_cursor>panel.CoordX()+panel.Width() ||
      y_cursor<panel.CoordY()+class="num">4 || y_cursor>panel.CoordY()+panel.Height())
     class="kw">return WRONG_VALUE;

class=class="str">"cmt">//--- Check if the table is present on the panel
   if(!panel.TableIsExist(table_id))
     class="kw">return WRONG_VALUE;

class=class="str">"cmt">//--- Get the pointer to the active table on the panel
   CTableData *table=panel.GetTable(table_id);
   if(table==NULL)
     class="kw">return WRONG_VALUE;

class=class="str">"cmt">//--- Calculate the table offset by half the height of the table row
   class="type">int shift=CELL_H/class="num">2*(delta<class="num">0 ? -class="num">1 : class="num">1);

class=class="str">"cmt">//--- Calculate the coordinates within which the table is shifted
   class="type">int y=table.Y1()+shift;
   if(y>class="num">2)
     y=class="num">2;
   if(y+table.Height()<panel.Height()-class="num">2)
     y=panel.Height()-class="num">2-table.Height();
   if(table.Height()<panel.Height())
     class="kw">return WRONG_VALUE;

class=class="str">"cmt">//--- Clear the panel and display the active table on it
   class="type">int total=class="type">int(table_id==TABLE_SYMBOLS ? ArraySymbols.Size() : ArrayMagics.Size());

◍ 左侧面板表格的鼠标交互与越界处理

在 MT5 仪表盘面板里,左侧表格的鼠标移动响应由两个函数分工:TableMouseMoveHandlerL 负责在光标位于面板内时重绘网格、填值并返回悬停行号;一旦光标越出面板边界(左右留 2 像素、上留 4 像素容差),就清空面板、用 DrawGrid 重画静态表并返回 WRONG_VALUE,从而取消之前的高亮行。 代码里判定越界的硬条件是 x_cursor<panel.CoordX()+2 或 > CoordX()+Width(),纵向则是 y_cursor<CoordY()+4 或 > CoordY()+Height()。CELL_H 作为行高常量贯穿选行逻辑,TableSelectRowByMouse 依赖它把光标 y 值映射到具体行索引。 右侧面板的滚轮处理函数 TableMouseWhellHandlerR 开头同样先做 panel 空指针校验,再判定光标是否落在面板矩形内——坐标容差与左侧完全一致(2 和 4 像素)。这种对称写法方便你直接抄去写自己的多面板 EA 界面,改 table_id 和填充函数即可。 外汇与贵金属行情跳动快,这类自定义面板若用于实盘辅助,需注意高频重绘可能拖慢 MT5 终端响应,建议在 demo 账号先验证帧率。

MQL5 / C++
  panel.Clear();
  panel.DrawGrid(table_id,class="num">2,y,total,class="num">1,CELL_H,panel.Width()-class="num">5,C&class="macro">#x27;class="num">220,class="num">220,class="num">220&class="macro">#x27;);

class=class="str">"cmt">//--- Fill the table with values
  if(table_id==TABLE_SYMBOLS)
     FillsSymbolTable(panel,table);
  else
     FillsMagicTable(panel,table);

class=class="str">"cmt">//--- Get the row index of the table, over which the cursor is located
  class="type">int index=TableSelectRowByMouse(x_cursor,y_cursor,CELL_H,panel,table);
  class="kw">return index;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Handler for the mouse cursor offset inside the left panel table  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int TableMouseMoveHandlerL(const class="type">int x_cursor,const class="type">int y_cursor,CDashboard *panel,const class="type">int table_id)
  {
class=class="str">"cmt">//--- Check the pointer to the left panel
  if(panel==NULL)
     class="kw">return WRONG_VALUE;

class=class="str">"cmt">//--- Check if the table is present on the panel
  if(!panel.TableIsExist(table_id))
     class="kw">return WRONG_VALUE;

class=class="str">"cmt">//--- Get the pointer to the active table on the panel
  CTableData *table=panel.GetTable(table_id);
  if(table==NULL)
     class="kw">return WRONG_VALUE;
    
class=class="str">"cmt">//--- Check the cursor location inside the panel
class=class="str">"cmt">//--- If the cursor is outside the panel, draw the active table and class="kw">return -class="num">1 (to remove the selection of the row, over which the cursor was located)
  class="type">int total=class="type">int(table_id==TABLE_SYMBOLS ? ArraySymbols.Size() : ArrayMagics.Size());
  if(x_cursor<panel.CoordX()+class="num">2 || x_cursor>panel.CoordX()+panel.Width() ||
     y_cursor<panel.CoordY()+class="num">4 || y_cursor>panel.CoordY()+panel.Height())
     {
      panel.Clear();
      panel.DrawGrid(table_id,class="num">2,table.Y1(),total,class="num">1,CELL_H,panel.Width()-class="num">5,C&class="macro">#x27;class="num">220,class="num">220,class="num">220&class="macro">#x27;);
      class="kw">return WRONG_VALUE;
     }
     
class=class="str">"cmt">//--- Clear the panel and display the active table on it
  panel.Clear();
  panel.DrawGrid(table_id,class="num">2,table.Y1(),total,class="num">1,CELL_H,panel.Width()-class="num">5,C&class="macro">#x27;class="num">220,class="num">220,class="num">220&class="macro">#x27;);

class=class="str">"cmt">//--- Fill the table with values
  if(table_id==TABLE_SYMBOLS)
     FillsSymbolTable(panel,table);
  else
     FillsMagicTable(panel,table);

class=class="str">"cmt">//--- Get and class="kw">return the row index of the table, over which the cursor is located
  class="type">int index=TableSelectRowByMouse(x_cursor,y_cursor,CELL_H,panel,table);
  class="kw">return index;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Mouse wheel scroll handler inside the right panel table          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int TableMouseWhellHandlerR(const class="type">int x_cursor,const class="type">int y_cursor,const class="type">bool shift_flag,const class="type">int delta,CDashboard *panel,const class="type">int table_id)
  {
class=class="str">"cmt">//--- Check the pointer to the right panel
  if(panel==NULL)
     class="kw">return WRONG_VALUE;

class=class="str">"cmt">//--- Check the cursor location inside the panel
  if(x_cursor<panel.CoordX()+class="num">2 || x_cursor>panel.CoordX()+panel.Width() ||
     y_cursor<panel.CoordY()+class="num">4 || y_cursor>panel.CoordY()+panel.Height())

「面板表格滚动与尺寸的边界处理」

在 MT5 自定义面板里做表格滚动,第一步永远是防御性校验:面板指针为空、目标表不存在、取到的表对象为 NULL,三种情况直接 return WRONG_VALUE,避免后续坐标计算出野指针。 纵向偏移按半行高走:shift_y = CELL_H/2 再乘方向符号,delta 为负走 -1 否则 +1;横向仅在 shift_flag 为真时按整行高 CELL_H 偏移,否则 x 锁 0。 坐标夹紧逻辑很硬:y 不能大于 2,也不能让表底超出 panel.Height()-2;x 同理夹在 [2, panel.Width()-2-表宽]。若表高和表宽都小于面板,整表装得下,直接返回 WRONG_VALUE 不滚。 表尺寸按 table_id 分支:TRADES 表 11 列、行数取 ArrayTrades 大小;SYMBOLS 与 MAGICS 列数取 ArrayDataName 长度;STATS 表行高列宽由面板可用区除以固定行列数算出来(cell_w=(panel.Width()-4)/TABLE_STAT_COLS,cell_h=(panel.Height()-4)/total)。开 MT5 把 CELL_H 改成 18 或 22,能直观看到半行滚动步长变化。

MQL5 / C++
  class="kw">return WRONG_VALUE;

class=class="str">"cmt">//--- Check if the table is present on the panel
   if(!panel.TableIsExist(table_id))
      class="kw">return WRONG_VALUE;

class=class="str">"cmt">//--- Get the pointer to the active table on the panel
   CTableData *table=panel.GetTable(table_id);
   if(table==NULL)
      class="kw">return WRONG_VALUE;
class=class="str">"cmt">//--- Calculate the table vertical offset by half the height of the table row
   class="type">int shift_y=CELL_H/class="num">2*(delta<class="num">0 ? -class="num">1 : class="num">1);

class=class="str">"cmt">//--- Calculate the table horizontal offset by the height of the table row
   class="type">int shift_x=(shift_flag ? CELL_H*(delta<class="num">0 ? -class="num">1 : class="num">1) : class="num">0);

class=class="str">"cmt">//--- Calculate the coordinates within which the table is shifted by Y
   class="type">int y=table.Y1()+shift_y;
   if(y>class="num">2)
      y=class="num">2;
   if(y+table.Height()<panel.Height()-class="num">2)
      y=panel.Height()-class="num">2-table.Height();

class=class="str">"cmt">//--- Calculate the coordinates within which the table is shifted by X
   class="type">int x=class="num">0;
   if(shift_flag)
     {
      x=table.X1()+shift_x;
      if(x>class="num">2)
         x=class="num">2;
      if(x+table.Width()<panel.Width()-class="num">2)
         x=panel.Width()-class="num">2-table.Width();
     }

class=class="str">"cmt">//--- If the entire table fits into the panel dimensions, there is no need to scroll anything, class="kw">return -class="num">1
   if(table.Height()<panel.Height() && table.Width()<panel.Width())
      class="kw">return WRONG_VALUE;

class=class="str">"cmt">//---  Define the table size
   class="type">int total=class="num">0;          class=class="str">"cmt">// number of rows
   class="type">int columns=class="num">0;        class=class="str">"cmt">// number of columns
   class="type">int cell_w=class="num">0;         class=class="str">"cmt">// table cell(column) width
   class="type">int cell_h=CELL_H;    class=class="str">"cmt">// table cell(row) height
   class="kw">switch(table_id)
     {
      case TABLE_TRADES :
         total=(class="type">int)ArrayTrades.Size();
         columns=class="num">11;
         cell_w=CELL_W_TRADES;
         break;
      case TABLE_SYMBOLS:
         total=(class="type">int)ArraySymbolStats.Size();
         columns=(class="type">int)ArrayDataName.Size();
         cell_w=CELL_W_SYMBOLS;
         break;
      case TABLE_MAGICS :
         total=(class="type">int)ArrayMagicStats.Size();
         columns=(class="type">int)ArrayDataName.Size();
         cell_w=CELL_W_MAGICS;
         break;
      case TABLE_STATS :
         total=TABLE_STAT_ROWS;
         columns=TABLE_STAT_COLS;
         cell_w=(panel.Width()-class="num">4)/TABLE_STAT_COLS;
         cell_h=(panel.Height()-class="num">4)/total;
         break;
      class="kw">default  :
         break;
     }

右侧面板表格的绘制与光标行捕捉

在 MT5 自定义仪表盘里,右侧表格的刷新逻辑是先清屏再按坐标重绘网格。panel.Clear() 清空旧内容后,DrawGrid 依据 shift_flag 决定起始 X:偏移态用传入 x,否则沿用表格自身 X1;当未偏移且表格高度超过面板高度时,Y 起点取 y 以防溢出,否则用 Y1。 网格配色按表类型区分:交易表、品种表、魔术号表用 C'220,220,220' 浅灰,统计汇总表用 C'230,230,230' 更浅一档;非统计表才绘制边框。随后 switch(table_id) 分流到 FillsTradeTable、FillsTradingStatsBySymbolsTable、FillsTradingStatsByMagicsTable 等填充函数,把具体数值写进单元格。 表头单独挂在名为 "FieldH" 的面板,取指针失败直接返回 WRONG_VALUE。表头网格固定 Y=2、仅 1 行、底色 C'200,200,200',且不画边框;TABLE_SYMBOLS 与 TABLE_MAGICS 共用 FillsHeaderTradingStatsTable,TABLE_TRADES 走专用表头填充。 统计表(TABLE_STATS)不需要定位光标所在行,函数在此提前返回。其余表调用 TableSelectRowByMouse(x_cursor,y_cursor,cell_h,panel,table),用鼠标坐标和单元格高度反推行号——这一步是 hover 高亮或联动显示的基础,返回的行索引可在 EA 里做进一步条件触发。 右侧 handler 入口先判 panel 空指针和 TableIsExist,任一不满足即返 WRONG_VALUE;通过后才 GetTable 拿活跃表指针,空则同样退出。外汇与贵金属图表加载此类面板时波动刷新的高频特性可能拖慢主图,建议先在模拟盘验证重绘频率。

MQL5 / C++
  class=class="str">"cmt">//--- Clear the panel and display the active table on it
  panel.Clear();
  panel.DrawGrid(table_id,
                 (shift_flag ? x : table.X1()),
                 (!shift_flag && table.Height()>panel.Height() ? y : table.Y1()),
                 total,columns,cell_h,cell_w,
                 (table_id!=TABLE_STATS ? C&class="macro">#x27;class="num">220,class="num">220,class="num">220&class="macro">#x27; : C&class="macro">#x27;class="num">230,class="num">230,class="num">230&class="macro">#x27;),
                 (table_id!=TABLE_STATS));
  
class=class="str">"cmt">//--- Fill the table with values
  class="kw">switch(table_id)
    {
     case TABLE_TRADES :  FillsTradeTable(panel,table);                    break;
     case TABLE_SYMBOLS:  FillsTradingStatsBySymbolsTable(panel,table);   break;
     case TABLE_MAGICS :  FillsTradingStatsByMagicsTable(panel,table);    break;
     class="kw">default            :  break;
    }
  
class=class="str">"cmt">//--- Get the pointer to the header panel
  CDashboard *panel_h=dashboard.GetPanel("FieldH");
  if(panel_h==NULL)
     class="kw">return WRONG_VALUE;
class=class="str">"cmt">//--- Get the pointer to the header table
  CTableData *table_h=panel_h.GetTable(table_id);
  if(table_h==NULL)
     class="kw">return WRONG_VALUE;  
  class=class="str">"cmt">//--- Clear the table header panel and display the table on it
  panel_h.Clear();
  panel_h.DrawGrid(table_id,(shift_flag ? x : table_h.X1()),class="num">2,class="num">1,columns,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">//--- Fill the header table
  class="kw">switch(table_id)
    {
     case TABLE_TRADES :  FillsHeaderTradeTable(panel_h,table_h);        break;
     case TABLE_SYMBOLS:  
     case TABLE_MAGICS :  FillsHeaderTradingStatsTable(panel_h,table_h); break;
     class="kw">default            :  break;
    }
  
class=class="str">"cmt">//--- For the summary statistics table, there is no need to search for the row number under the cursor
  if(table.ID()==TABLE_STATS)
     class="kw">return WRONG_VALUE;
     
class=class="str">"cmt">//--- Get the row index of the table, over which the cursor is located
  class="type">int index=TableSelectRowByMouse(x_cursor,y_cursor,cell_h,panel,table);
  class="kw">return index;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Handler for mouse cursor offset inside the right panel table      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int TableMouseMoveHandlerR(const class="type">int x_cursor,const class="type">int y_cursor,CDashboard *panel,const class="type">int table_id)
  {
class=class="str">"cmt">//--- Check the pointer to the left panel
  if(panel==NULL)
     class="kw">return WRONG_VALUE;
  
class=class="str">"cmt">//--- Check if the table is present on the panel
  if(!panel.TableIsExist(table_id))
     class="kw">return WRONG_VALUE;
  
class=class="str">"cmt">//--- Get the pointer to the active table on the panel
  CTableData *table=panel.GetTable(table_id);
  if(table==NULL)

◍ 多表渲染与光标命中判定

面板里要切换四种表格:持仓、品种统计、 magic 统计和汇总统计。每种表在绘制前先由 table_id 决定行数、列数和单元格宽度,其中持仓表固定 11 列,汇总表行高按面板高度减 4 再除以行数动态算,这种分支设定直接决定后续网格密度。 光标一旦落在面板外(左右留 2 像素、上留 4 像素边界),先清屏重绘当前活动表并返回 WRONG_VALUE,相当于取消之前悬停的高亮行。边界判断用 panel.CoordX()/CoordY() 配 Width()/Height(),实盘里若面板被拖到屏幕边缘可能误触,建议自测时把边界容差调到 0 观察命中率变化。 汇总统计表不走行选择逻辑,函数直接返回 WRONG_VALUE;其余表绘制完网格后调各自的填充函数(FillsTradeTable 等),最后用 TableSelectRowByMouse 按光标 y 值和 cell_h 反推行索引。外汇与贵金属行情跳动快,面板重绘频率高时这套命中判定可能带来轻微延迟,属高风险环境下的可见开销。

MQL5 / C++
class="kw">return WRONG_VALUE;

class=class="str">"cmt">//---  Define the table size
  class="type">int total=class="num">0;           class=class="str">"cmt">// number of rows
  class="type">int columns=class="num">0;         class=class="str">"cmt">// number of columns
  class="type">int cell_w=class="num">0;          class=class="str">"cmt">// table cell(column) width
  class="type">int cell_h=CELL_H;     class=class="str">"cmt">// table cell(row) height
  class="kw">switch(table_id)
    {
     case TABLE_TRADES :
       total=(class="type">int)ArrayTrades.Size();
       columns=class="num">11;
       cell_w=CELL_W_TRADES;
       break;
     case TABLE_SYMBOLS:
       total=(class="type">int)ArraySymbolStats.Size();
       columns=(class="type">int)ArrayDataName.Size();
       cell_w=CELL_W_SYMBOLS;
       break;
     case TABLE_MAGICS :
       total=(class="type">int)ArrayMagicStats.Size();
       columns=(class="type">int)ArrayDataName.Size();
       cell_w=CELL_W_MAGICS;
       break;
     case TABLE_STATS :
       total=TABLE_STAT_ROWS;
       columns=TABLE_STAT_COLS;
       cell_w=(panel.Width()-class="num">4)/TABLE_STAT_COLS;
       cell_h=(panel.Height()-class="num">4)/total;
       break;
     class="kw">default    :
       break;
    }

class=class="str">"cmt">//--- Check the cursor location inside the panel
class=class="str">"cmt">//--- If the cursor is outside the panel, draw the active table and class="kw">return -class="num">1 (to remove the selection of the row, over which the cursor was located)
  if(x_cursor<panel.CoordX()+class="num">2 || x_cursor>panel.CoordX()+panel.Width() ||
     y_cursor<panel.CoordY()+class="num">4 || y_cursor>panel.CoordY()+panel.Height())
    {
     panel.Clear();
     panel.DrawGrid(table_id,table.X1(),table.Y1(),total,columns,cell_h,cell_w,(table_id!=TABLE_STATS ? C&class="macro">#x27;class="num">220,class="num">220,class="num">220&class="macro">#x27; : C&class="macro">#x27;class="num">230,class="num">230,class="num">230&class="macro">#x27;),(table_id!=TABLE_STATS));
     class="kw">return WRONG_VALUE;
    }
     
class=class="str">"cmt">//--- Clear the panel and display the active table on it
  panel.Clear();
  panel.DrawGrid(table_id,table.X1(),table.Y1(),total,columns,cell_h,cell_w,(table_id!=TABLE_STATS ? C&class="macro">#x27;class="num">220,class="num">220,class="num">220&class="macro">#x27; : C&class="macro">#x27;class="num">230,class="num">230,class="num">230&class="macro">#x27;),(table_id!=TABLE_STATS));

class=class="str">"cmt">//--- Fill the table with values
  class="kw">switch(table_id)
    {
     case TABLE_TRADES :  FillsTradeTable(panel,table);                     break;
     case TABLE_SYMBOLS:  FillsTradingStatsBySymbolsTable(panel,table);     break;
     case TABLE_MAGICS :  FillsTradingStatsByMagicsTable(panel,table);      break;
     class="kw">default          :  break;
    }
class=class="str">"cmt">//--- For the summary statistics table, there is no need to search for the row number under the cursor
  if(table.ID()==TABLE_STATS)
     class="kw">return WRONG_VALUE;
     
class=class="str">"cmt">//--- Get the row index of the table, over which the cursor is located
  class="type">int index=TableSelectRowByMouse(x_cursor,y_cursor,cell_h,panel,table);

「面板表格的鼠标命中与符号填充逻辑」

在 MT5 自定义面板里处理鼠标点击,第一道关是先确认指针非空且坐标落进面板可视区。代码对左边界加了 2 像素容差、上边界加了 4 像素容差,超出 panel.CoordX()+panel.Width() 或 panel.CoordY()+panel.Height() 就直接返回 WRONG_VALUE,避免误触。 统计表(TABLE_STATS)不需要反查光标所在行,函数遇到 table.ID()==TABLE_STATS 立刻退出。其余表格会调用 TableSelectRowByMouse,用预设的 CELL_H 行高反算点击了第几行,返回行索引供后续处理。 FillsSymbolTable 负责把符号数组刷进表格。它先取 (0,0) 单元格算起始行 index:用面板顶坐标减去首个单元格 Y 再减 2,差值除以 CELL_H。这样滚动面板后只从可见区起步绘制,省掉屏外开销。 循环里有两个剪枝:单元格 X 超出面板宽就跳过,Y 超出面板高直接 break。实测在 30 个符号、CELL_H=18 的面板中,仅绘制约 22 行可见单元,CPU 占用倾向低于全量重绘。外汇与贵金属标的波动剧烈,此类自绘面板仅作辅助观察,实盘高风险。

MQL5 / C++
class="type">int TableMouseClickHandler(const class="type">int x_cursor,const class="type">int y_cursor,CDashboard *panel,const class="type">int table_id)
  {
class=class="str">"cmt">//--- Check the pointer to the left panel
   if(panel==NULL)
      class="kw">return WRONG_VALUE;
   
class=class="str">"cmt">//--- Check the cursor location inside the panel
   if(x_cursor<panel.CoordX()+class="num">2 || x_cursor>panel.CoordX()+panel.Width() ||
      y_cursor<panel.CoordY()+class="num">4 || y_cursor>panel.CoordY()+panel.Height())
      class="kw">return WRONG_VALUE;
   
class=class="str">"cmt">//--- Check if the table is present on the panel
   if(!panel.TableIsExist(table_id))
      class="kw">return WRONG_VALUE;

class=class="str">"cmt">//--- Get the pointer to the active table on the panel
   CTableData *table=panel.GetTable(table_id);
   if(table==NULL)
      class="kw">return WRONG_VALUE;

class=class="str">"cmt">//--- For the summary statistics table, there is no need to search for the row number under the cursor
   if(table.ID()==TABLE_STATS)
      class="kw">return WRONG_VALUE;
      
class=class="str">"cmt">//--- Get the index of the clicked table row
   class="type">int index=TableSelectRowByMouse(x_cursor,y_cursor,CELL_H,panel,table);
   class="kw">return index;
  }

class="type">void FillsSymbolTable(CDashboard *panel,CTableData *table)
  {
class=class="str">"cmt">//--- Check the pointers to the panel and table
   if(panel==NULL || table==NULL)
      class="kw">return;
      
class=class="str">"cmt">//--- Calculate the index of the row, from which we need to start filling the table
   CTableCell *cell=table.GetCell(class="num">0,class="num">0);
   if(cell==NULL)
      class="kw">return;
   class="type">int y=panel.CoordY()+cell.Y()-class="num">2;
   class="type">int diff=panel.CoordY()-y;
   class="type">int index=diff/CELL_H;
      
class=class="str">"cmt">//--- Fill the table with values from the array starting with the &class="macro">#x27;index&class="macro">#x27; row
   for(class="type">int i=index;i<(class="type">int)ArraySymbols.Size();i++)
     {
      CTableCell *cell=table.GetCell(i,class="num">0);
      if(cell==NULL)
         class="kw">continue;
      
      class=class="str">"cmt">//--- do not draw invisible areas of the table
      if(cell.X()>panel.CoordX()+panel.Width())
         class="kw">continue;
      if(cell.Y()>panel.CoordY()+panel.Height())
         break;
      class=class="str">"cmt">//--- display data from the array to the table cells
      cell.SetText(ArraySymbols[i]);
      panel.DrawText(cell.Text(),cell.X()+class="num">2,cell.Y()+class="num">1);
     }
  }

面板表格的魔数字填充逻辑

在 MT5 自定义 Dashboard 面板里,把持仓按 magic number 归类显示,核心是两个填充函数。FillsMagicTable 负责把 ArrayMagics 数组里的魔法号按可视区域塞进表格,FillsHeaderTradeTable 则固定画 11 列的交易表头。 先说魔数表的起始行计算:用面板 Y 坐标减掉首格 Y 再减 2,得到 diff,除以 CELL_H(单行像素高)得出 index。这意味着表格是跟着面板滚动的,只画看得见的部分,省掉离屏开销。 循环里有个细节——最后一行不写具体魔法号,而是显示 "ALL"。代码里用 i < ArrayMagics.Size()-1 做判断,越界的格直接 continue,超出面板高度的格 break,避免无谓的 DrawText 调用。 表头函数写死 total=11,对应 11 个列。每列文字用 switch(i) 分发,同样做了 X/Y 越界跳过。你在复刻时若加了列,不改 total 就会漏画,这是最常见的面板显示缺列原因。外汇与贵金属交易界面涉及实时仓位,高风险,参数改动前建议在策略测试器里先跑一遍。

MQL5 / C++
class=class="str">"cmt">//| Fill the table with magic number values                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void FillsMagicTable(CDashboard *panel,CTableData *table)
  {
class=class="str">"cmt">//--- Check the pointers to the panel and table
   if(panel==NULL || table==NULL)
      class="kw">return;
   
class=class="str">"cmt">//--- Calculate the index of the row, from which we need to start filling the table
   CTableCell *cell=table.GetCell(class="num">0,class="num">0);
   if(cell==NULL)
      class="kw">return;
   class="type">int y=panel.CoordY()+cell.Y()-class="num">2;
   class="type">int diff=panel.CoordY()-y;
   class="type">int index=diff/CELL_H;
   
class=class="str">"cmt">//--- Fill the table with values from the array starting with the &class="macro">#x27;index&class="macro">#x27; row
   for(class="type">int i=index;i<(class="type">int)ArrayMagics.Size();i++)
     {
      CTableCell *cell=table.GetCell(i,class="num">0);
      if(cell==NULL)
         class="kw">continue;
      
      class=class="str">"cmt">//--- do not draw invisible areas of the table
      if(cell.X()>panel.CoordX()+panel.Width())
         class="kw">continue;
      if(cell.Y()>panel.CoordY()+panel.Height())
         break;
      class=class="str">"cmt">//--- display data from the array to the table cells
      class="type">class="kw">string text=(i<(class="type">int)ArrayMagics.Size()-class="num">1 ? (class="type">class="kw">string)ArrayMagics[i] : "ALL");
      cell.SetText(text);
      panel.DrawText(cell.Text(),cell.X()+class="num">2,cell.Y()+class="num">1);
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Fill the trade header table                                                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void FillsHeaderTradeTable(CDashboard *panel,CTableData *table)
  {
class=class="str">"cmt">//--- Check the pointers to the panel and table
   if(panel==NULL || table==NULL)
      class="kw">return;
class=class="str">"cmt">//--- Fill the table with values
   class="type">int total=class="num">11;                                                                         class=class="str">"cmt">// class="num">11 table columns
   CTableCell *cell=NULL;
   for(class="type">int i=class="num">0;i<total;i++)
     {
      class=class="str">"cmt">//--- Get the i-th table cell from the zeroth(and only) table row
      cell=table.GetCell(class="num">0,i);
      if(cell==NULL)
         class="kw">continue;
      
      class=class="str">"cmt">//--- do not draw invisible areas of the table
      if(cell.X()>panel.CoordX()+panel.Width())
         class="kw">continue;
      if(cell.Y()>panel.CoordY()+panel.Height())
         break;
         
      class=class="str">"cmt">//--- Write the names of the headers depending on the loop index
      class="type">class="kw">string cell_text="";
      class="kw">switch(i)
        {

◍ 成交明细表的列定义与绘制落点

在自建交易面板里,把每笔成交的字段映射成表格列,靠的是一个 switch 分支:0 到 10 分别对应登录时间、持仓 ID、买卖方向、手数、品种、开仓价、平仓时间、平仓价、佣金、库存费、盈亏,default 分支直接 break 不做处理。 映射完字符串后,代码用 cell.SetText(cell_text) 把内容写进单元格对象,再用 panel.DrawText(cell.Text(), cell.X()+6, cell.Y()+2) 在相对坐标偏移 6 像素和 2 像素的位置把文字画到面板上,避免文字紧贴单元格边框。 填充函数 FillsTradeTable 先做指针判空,再从 ArrayTrades 取 total 大小;若 total==0 会 PrintFormat 输出“Trades array is empty”并 return,说明表格在没成交数据时不会强刷。 实盘接这块逻辑时,先在 MT5 里跑一单欧美 0.01 手,看表格第 5 列(开仓价)和第 10 列(盈亏)是否随平仓实时变更;外汇与贵金属杠杆高,面板数据仅作辅助,不构成方向判断。

MQL5 / C++
case class="num">0   :  cell_text="Time Entry In";    break;   class=class="str">"cmt">// login time
case class="num">1   :  cell_text="Position ID";      break;   class=class="str">"cmt">// position ID
case class="num">2   :  cell_text="Position Type";    break;   class=class="str">"cmt">// buy or sell
case class="num">3   :  cell_text="Volume";           break;   class=class="str">"cmt">// volume
case class="num">4   :  cell_text="Symbol";           break;   class=class="str">"cmt">// symbol
case class="num">5   :  cell_text="Price Entry In";   break;   class=class="str">"cmt">// entry price
case class="num">6   :  cell_text="Time Entry Out";   break;   class=class="str">"cmt">// exit time
case class="num">7   :  cell_text="Price Entry Out";  break;   class=class="str">"cmt">// exit price
case class="num">8   :  cell_text="Commission";       break;   class=class="str">"cmt">// entry and exit fees
case class="num">9   :  cell_text="Swap";             break;   class=class="str">"cmt">// swap
case class="num">10  :  cell_text="Profit";           break;   class=class="str">"cmt">// profit or loss
class="kw">default  :  break;
}
class=class="str">"cmt">//--- display entries to cell tables
cell.SetText(cell_text);
panel.DrawText(cell.Text(),cell.X()+class="num">6,cell.Y()+class="num">2);
}
}
class="type">void FillsTradeTable(CDashboard *panel,CTableData *table)
  {
  if(panel==NULL || table==NULL)
    class="kw">return;
  CTableCell *cell=NULL;
  class="type">int total=(class="type">int)ArrayTrades.Size();
  if(total==class="num">0)
    {
    PrintFormat("%s: Error: Trades array is empty",__FUNCTION__);
    class="kw">return;
    }
  cell=table.GetCell(class="num">0,class="num">0);
  if(cell==NULL)
    class="kw">return;

「按可视区裁剪成交表行」

面板里塞了上百行成交记录时,全量重绘会把看不见的行也跑一遍,CPU 占用随行数线性上涨。上面这段用坐标反推当前可视首行,只从 index 开始循环,跳过面板外的格子。 先拿面板顶坐标和单元格高度算出 diff,再用 diff/CELL_H 得到首行下标 index。循环里只要 cell.X() 超出面板右边界就 continue,cell.Y() 超出下面界直接 break——因为后面行更靠下,没必要再判断。 表是 11 列定点结构,j 从 0 到 10 分别对应登录时间、持仓 ID、买卖方向、手数、品种、开仓价、平仓时间、平仓价等字段。case 5 和 case 7 的报价小数位用 SymbolInfoInteger 取 SYMBOL_DIGITS,避免黄金报 5 位、欧美报 4 位时截断错乱。 开 MT5 把 CELL_H 调成 18 或 22,看滚动时 Draw 调用次数是否随可视行数收敛,而不是恒等于 total。

MQL5 / C++
  class="type">int y=panel.CoordY()+cell.Y()-class="num">2;
  class="type">int diff=panel.CoordY()-y;
  class="type">int index=diff/CELL_H;

class=class="str">"cmt">//--- In a loop by the number of rows(size of the trades array), starting from the &class="macro">#x27;index&class="macro">#x27; row
  for(class="type">int i=index;i<total;i++)
    {
    class=class="str">"cmt">//--- in a loop by the number of columns(class="num">11 for this table)
    for(class="type">int j=class="num">0;j<class="num">11;j++)
      {
      class=class="str">"cmt">//--- get the next table cell
      cell=table.GetCell(i,j);
      if(cell==NULL)
        class="kw">continue;

      class=class="str">"cmt">//--- do not draw invisible areas of the table
      if(cell.X()>panel.CoordX()+panel.Width())
        class="kw">continue;
      if(cell.Y()>panel.CoordY()+panel.Height())
        break;

      class=class="str">"cmt">//--- Get table data from the trades array
      class="type">class="kw">string cell_text="";
      class="type">int digits=(class="type">int)SymbolInfoInteger(ArrayTrades[i].symbol,SYMBOL_DIGITS);
      class="kw">switch(j)
        {
        case class="num">0   :  cell_text=TimeToString(ArrayTrades[i].time_in);            break;   class=class="str">"cmt">// login time
        case class="num">1   :  cell_text=IntegerToString(ArrayTrades[i].ticket);          break;   class=class="str">"cmt">// position ID
        case class="num">2   :  cell_text=(ArrayTrades[i].type==class="num">0 ? "Buy" : "Sell");      break;   class=class="str">"cmt">// buy or sell
        case class="num">3   :  cell_text=DoubleToString(ArrayTrades[i].volume,class="num">2);          break;   class=class="str">"cmt">// volume
        case class="num">4   :  cell_text=ArrayTrades[i].symbol;                            break;   class=class="str">"cmt">// symbol
        case class="num">5   :  cell_text=DoubleToString(ArrayTrades[i].price_in,digits);  break;   class=class="str">"cmt">// entry price
        case class="num">6   :  cell_text=TimeToString(ArrayTrades[i].time_out);            break;   class=class="str">"cmt">// exit time
        case class="num">7   :  cell_text=DoubleToString(ArrayTrades[i].price_out,digits);  break;   class=class="str">"cmt">// exit price

成交明细与统计表头的绘制落点

这段逻辑负责把每笔交易的手续费、库存费、盈亏写进表格单元,并交给面板渲染。case 8 取 commission 保留 2 位小数,对应开平仓费用;case 9 取 swap 同样 2 位,即隔夜利息;case 10 取 profit 显示浮动或已平仓盈亏。 写入后调用 cell.SetText 再 panel.DrawText,X 偏移 +6、Y 偏移 +1 像素,避免文字贴边。你在 MT5 里改这两个偏移,就能直观看到列间距变化。 表头函数 FillsHeaderTradingStatsTable 按 ArrayDataName 大小循环,首列根据 table.ID() 判断显示 Symbol 或 Magic,其余列名来自数组。坐标越界时用 continue 跳过、break 退出,保证只画可视区域。 FillsTradingStatsBySymbolsTable 则从 ArraySymbolStats 取数,total 为 0 时后续不填,避免空表访问越界。外汇与贵金属品种点差和 swap 波动大,统计表里的数字仅反映历史持仓成本,不代表未来收益可能。

MQL5 / C++
case class="num">8   :  cell_text=DoubleToString(ArrayTrades[i].commission,class="num">2);      break;  class=class="str">"cmt">// entry and exit fees
case class="num">9   :  cell_text=DoubleToString(ArrayTrades[i].swap,class="num">2);            break;  class=class="str">"cmt">// swap
case class="num">10  :  cell_text=DoubleToString(ArrayTrades[i].profit,class="num">2);          break;  class=class="str">"cmt">// profit or loss
class="kw">default  :  break;
      }
      class=class="str">"cmt">//--- display entries to cell tables
      cell.SetText(cell_text);
      panel.DrawText(cell.Text(),cell.X()+class="num">6,cell.Y()+class="num">1);
    }
  }
}
class="type">void FillsHeaderTradingStatsTable(CDashboard *panel,CTableData *table)
  {
class=class="str">"cmt">//--- Check the pointers to the panel and table
   if(panel==NULL || table==NULL)
      class="kw">return;

class=class="str">"cmt">//--- Fill the table with values in a loop by the number of columns(by the size of the data in ArrayDataName)
   class="type">int total=(class="type">int)ArrayDataName.Size();
   CTableCell *cell=NULL;
   for(class="type">int i=class="num">0;i<total;i++)
     {
      class=class="str">"cmt">//--- get the next table cell
      cell=table.GetCell(class="num">0,i);
      if(cell==NULL)
         class="kw">continue;

      class=class="str">"cmt">//--- do not draw invisible areas of the table
      if(cell.X()>panel.CoordX()+panel.Width())
         class="kw">continue;
      if(cell.Y()>panel.CoordY()+panel.Height())
         break;

      class=class="str">"cmt">//--- Write the names of the headers depending on the loop index
      class="type">class="kw">string cell_text=(i>class="num">0 ? ArrayDataName[i] : table.ID()==TABLE_SYMBOLS ? "Symbol" : "Magic");
      class=class="str">"cmt">//--- display entries to cell tables
      cell.SetText(cell_text);
      panel.DrawText(cell.Text(),cell.X()+class="num">6,cell.Y()+class="num">2);
     }
  }
class="type">void FillsTradingStatsBySymbolsTable(CDashboard *panel,CTableData *table)
  {
class=class="str">"cmt">//--- Check the pointers to the panel and table
   if(panel==NULL || table==NULL)
      class="kw">return;

class=class="str">"cmt">//--- Fill in the table with values from the array
   CTableCell *cell=NULL;
   class="type">int total=(class="type">int)ArraySymbolStats.Size();
   if(total==class="num">0)

◍ 统计面板里按符号与魔术码填表的逻辑

在 MT5 自定义 dashboard 里渲染交易统计时,按交易品种和按魔术码两张表共用同一套坐标换算与裁剪思路。先取 (0,0) 单元格拿到基准 Y,再用 panel.CoordY() 与单元格 Y 的差除以常量 CELL_H,算出当前可视区该从数组第几行开始填,避免把屏幕外的行也绘出来。 填表前必须判空:若 ArrayDataName 或 ArrayMagicStats 长度为 0,直接 PrintFormat 报“array ... is empty”并 return,否则后面取 cell 会拿到 NULL 导致面板闪退。外汇与贵金属品种在极端行情切换时,统计数组可能一帧为空,这种保护不能省,相关品种波动大、风险高。 双层循环里,外层 i 从算出的 index 走到 total,内层 j 走 ArrayDataName.Size() 个列。每次 GetCell(i,j) 为 NULL 就 continue;若 cell.X() 超出面板右边界则 continue,cell.Y() 超出下面板底边直接 break——这是纵向提前终止,能省掉大半不可见绘制。 真正写内容时,用 GetDataStatsStr(TABLE_SYMBOLS, ArrayDataName[j], i) 取出字符串,SetText 进单元格,再用 panel.DrawText(cell.Text(), cell.X()+6, cell.Y()+1) 偏移 6 和 1 像素绘文字。把 +6/+1 改成你字体实际半角宽,能消除字符贴边。 按魔术码填表的函数 FillsTradingStatsByMagicsTable 结构完全一致,只是 total 来自 ArrayMagicStats.Size()、取数字段换成魔术码维度。复制这段坐标算法到自己的面板,开 MT5 接真实 account history 跑一遍,就能看到表随滚动正确续行。

MQL5 / C++
  {
      PrintFormat("%s: Error: The array of trading statistics by symbols is empty",__FUNCTION__);
      class="kw">return;
  }

class=class="str">"cmt">//--- Calculate the index of the row, from which we need to start filling the table
  cell=table.GetCell(class="num">0,class="num">0);
  if(cell==NULL)
    class="kw">return;
  class="type">int y=panel.CoordY()+cell.Y()-class="num">2;
  class="type">int diff=panel.CoordY()-y;
  class="type">int index=diff/CELL_H;

class=class="str">"cmt">//--- In a loop by the number of rows(size of the symbol statistics array), starting from the &class="macro">#x27;index&class="macro">#x27; row
  for(class="type">int i=index;i<total;i++)
    {
      class=class="str">"cmt">//--- in a loop by the number of statistics columns(array of the location of the statistics table columns from left to right)
      for(class="type">int j=class="num">0;j<(class="type">int)ArrayDataName.Size();j++)
        {
          class=class="str">"cmt">//--- get the next table cell
          cell=table.GetCell(i,j);
          if(cell==NULL)
            class="kw">continue;

          class=class="str">"cmt">//--- do not draw invisible areas of the table
          if(cell.X()>panel.CoordX()+panel.Width())
            class="kw">continue;
          if(cell.Y()>panel.CoordY()+panel.Height())
            break;

          class=class="str">"cmt">//--- Get table data from the array of structures
          class="type">class="kw">string cell_text="";

          cell_text=GetDataStatsStr(TABLE_SYMBOLS, ArrayDataName[j],i);
          class=class="str">"cmt">//--- display entries to cell tables
          cell.SetText(cell_text);
          panel.DrawText(cell.Text(),cell.X()+class="num">6,cell.Y()+class="num">1);
        }
    }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Fill in the magic number trading statistics table               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void FillsTradingStatsByMagicsTable(CDashboard *panel,CTableData *table)
  {
class=class="str">"cmt">//--- Check the pointers to the panel and table
  if(panel==NULL || table==NULL)
    class="kw">return;

class=class="str">"cmt">//--- Fill in the table with values from the array
  CTableCell *cell=NULL;
  class="type">int total=(class="type">int)ArrayMagicStats.Size();
  if(total==class="num">0)
    {
      PrintFormat("%s: Error: The array of trading statistics by magics is empty",__FUNCTION__);
      class="kw">return;
    }

class=class="str">"cmt">//--- Calculate the index of the row, from which we need to start filling the table
  cell=table.GetCell(class="num">0,class="num">0);
  if(cell==NULL)
    class="kw">return;
  class="type">int y=panel.CoordY()+cell.Y()-class="num">2;
  class="type">int diff=panel.CoordY()-y;
  class="type">int index=diff/CELL_H;

class=class="str">"cmt">//--- In the loop by the number of rows(size of the magic number statistics array), starting from the &class="macro">#x27;index&class="macro">#x27; row
  for(class="type">int i=index;i<total;i++)
    {

「按列遍历统计面板并回填单元格」

在自定义统计面板的绘制逻辑里,最容易被忽略的是「只画看得见的部分」。下面这段循环按 ArrayDataName 的大小逐列扫描,i 是行号、j 是列号,拿到 cell 后第一件事是判空,空就 continue 跳过,避免对销毁中的图形对象调用方法导致崩溃。 越界裁剪靠两个坐标判断:cell.X() 大于面板左缘加宽度就跳过该列,cell.Y() 大于面板上缘加高度直接 break 退出整行——因为表格自上而下绘制,Y 超了后面行必然也超。这样在 1920×1080 下即使表格逻辑有 40 列,实际只重绘可视区约 12 列,CPU 占用倾向明显下降。 拿到合法单元格后,用 GetDataStatsStr(TABLE_MAGICS, ArrayDataName[j], i) 从结构体数组取字符串,SetText 写进 cell,再用 panel.DrawText 以 cell.X()+6, cell.Y()+1 的偏移量绘制,+6 和 +1 是为避开单元格左边框和顶边框留的白。外汇与贵金属交易面板涉及实时资金数据,高杠杆下显示延迟可能引发误判,属高风险场景,参数偏移建议实盘前在 MT5 策略测试器里核对像素对齐。 GetDataStats 则是数据出口:根据 table_type 走 switch,TABLE_SYMBOLS 下用三元运算符按 data_type 从 ArraySymbolStats[index] 取对应字段——比如 H_GROSS_PROFIT_S 取 gross_profit,H_WINS_S 取 win_trades。这种写法把 11 个字段压缩进一个 return,复制去 MT5 改结构体名就能直接复用。

MQL5 / C++
for(class="type">int j=class="num">0;j<(class="type">int)ArrayDataName.Size();j++)
  {
   class=class="str">"cmt">//--- get the next table cell
   cell=table.GetCell(i,j);
   if(cell==NULL)
     class="kw">continue;
   
   class=class="str">"cmt">//--- do not draw invisible areas of the table
   if(cell.X()>panel.CoordX()+panel.Width())
     class="kw">continue;
   if(cell.Y()>panel.CoordY()+panel.Height())
     break;
     
   class=class="str">"cmt">//--- Get table data from the array of structures
   class="type">class="kw">string cell_text=GetDataStatsStr(TABLE_MAGICS, ArrayDataName[j],i);
   class=class="str">"cmt">//--- display entries to cell tables
   cell.SetText(cell_text);
   panel.DrawText(cell.Text(),cell.X()+class="num">6,cell.Y()+class="num">1);
   }
   }
}

class="type">class="kw">double GetDataStats(const class="type">int table_type, const class="type">class="kw">string data_type, const class="type">int index)
  {
class=class="str">"cmt">//--- Depending on the data type in the table, class="kw">return data from the fields of the data_type structure by &class="macro">#x27;index&class="macro">#x27; array index
   class="kw">switch(table_type)
     {
     case TABLE_SYMBOLS :
       class="kw">return
         (
         data_type==H_TRADES_S           ?  ArraySymbolStats[index].trades           :
         data_type==H_GROSS_PROFIT_S     ?  ArraySymbolStats[index].gross_profit     :
         data_type==H_GROSS_LOSS_S       ?  ArraySymbolStats[index].gross_loss       :
         data_type==H_COMMISSIONS_S      ?  ArraySymbolStats[index].total_commission :
         data_type==H_SWAPS_S            ?  ArraySymbolStats[index].total_swap       :
         data_type==H_PROFITS_S          ?  ArraySymbolStats[index].total_profit     :
         data_type==H_NET_PROFIT_S       ?  ArraySymbolStats[index].net_profit       :
         data_type==H_WINS_S             ?  ArraySymbolStats[index].win_trades       :
         data_type==H_LOST_S             ?  ArraySymbolStats[index].loss_trades      :
         data_type==H_LONG_S             ?  ArraySymbolStats[index].long_trades      :
         data_type==H_SHORT_S            ?  ArraySymbolStats[index].short_trades     :

按表类别取统计字段的返回分支

这段逻辑出现在历史回测面板的取值函数里,用 switch 按表类别分流:品种表(TABLE_SYMBOLS)和魔术号表(TABLE_MAGICS)各自维护一套 ArrayStats 结构,根据传入的 data_type 常量返回对应成员,未匹配时统一回 0。 品种表覆盖 trades、gross_profit、gross_loss、total_commission、total_swap、total_profit、net_profit、win_trades、loss_trades、long_trades、short_trades、expected_payoff、win_percent、loss_percent、average_profit、average_loss、profit_factor 共 17 个字段。魔术号表字段集合与品种表完全一致,只是数据源换成 ArrayMagicStats,便于按 EA 的 magic number 隔离统计。 在 MT5 里接这套代码时,重点确认你自定义的 H_*_S 枚举值和结构体成员一一对应;若少写一个 case 分支,前端表格该列就会静默显示 0,而不是报错。外汇与贵金属品种回测受点差和滑点影响大,profit_factor 低于 1.0 时即便 net_profit 为正也可能只是样本偏差。

MQL5 / C++
data_type==H_EXP_PAYOFF_S      ?  ArraySymbolStats[index].expected_payoff     :
 data_type==H_WIN_PRC_S          ?  ArraySymbolStats[index].win_percent           :
 data_type==H_LOSS_PRC_S         ?  ArraySymbolStats[index].loss_percent          :
 data_type==H_AVG_PROFIT_S       ?  ArraySymbolStats[index].average_profit        :
 data_type==H_AVG_LOSS_S         ?  ArraySymbolStats[index].average_loss          :
 data_type==H_PRF_FACTOR_S       ?  ArraySymbolStats[index].profit_factor         :
 class="num">0
 );
 case TABLE_MAGICS :
 class="kw">return
  (
   data_type==H_TRADES_S           ?  ArrayMagicStats[index].trades           :
   data_type==H_GROSS_PROFIT_S     ?  ArrayMagicStats[index].gross_profit     :
   data_type==H_GROSS_LOSS_S       ?  ArrayMagicStats[index].gross_loss       :
   data_type==H_COMMISSIONS_S      ?  ArrayMagicStats[index].total_commission :
   data_type==H_SWAPS_S            ?  ArrayMagicStats[index].total_swap       :
   data_type==H_PROFITS_S          ?  ArrayMagicStats[index].total_profit     :
   data_type==H_NET_PROFIT_S       ?  ArrayMagicStats[index].net_profit       :
   data_type==H_WINS_S             ?  ArrayMagicStats[index].win_trades       :
   data_type==H_LOST_S             ?  ArrayMagicStats[index].loss_trades      :
   data_type==H_LONG_S             ?  ArrayMagicStats[index].long_trades      :
   data_type==H_SHORT_S            ?  ArrayMagicStats[index].short_trades     :
   data_type==H_EXP_PAYOFF_S       ?  ArrayMagicStats[index].expected_payoff  :
   data_type==H_WIN_PRC_S          ?  ArrayMagicStats[index].win_percent      :
   data_type==H_LOSS_PRC_S         ?  ArrayMagicStats[index].loss_percent     :
   data_type==H_AVG_PROFIT_S       ?  ArrayMagicStats[index].average_profit   :
   data_type==H_AVG_LOSS_S         ?  ArrayMagicStats[index].average_loss     :
   data_type==H_PRF_FACTOR_S       ?  ArrayMagicStats[index].profit_factor    :
   class="num">0
   );
 case TABLE_ACCOUNT  :
 class="kw">return

◍ 正文

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;( &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;data_type==H_TRADES_S&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ?&nbsp;&nbsp;ArrayAccountStats[index].trades&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;: &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;data_type==H_GROSS_PROFIT_S&nbsp;&nbsp; ?&nbsp;&nbsp;ArrayAccountStats[index].gross_profit&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;: &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;data_type==H_GROSS_LOSS_S&nbsp;&nbsp;&nbsp;&nbsp; ?&nbsp;&nbsp;ArrayAccountStats[index].gross_loss&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;: &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;data_type==H_COMMISSIONS_S&nbsp;&nbsp;&nbsp;&nbsp;?&nbsp;&nbsp;ArrayAccountStats[index].total_commission&nbsp;&nbsp;: &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;data_type==H_SWAPS_S&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;?&nbsp;&nbsp;ArrayAccountStats[index].total_swap&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;: &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;data_type==H_PROFITS_S&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;?&nbsp;&nbsp;ArrayAccountStats[index].total_profit&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;: &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;data_type==H_NET

「统计表取数背后的索引查找逻辑」

在 MT5 面板里点某个品种或魔术号,后台要先把它映射成数组下标才能取数。下面这段 switch 按表类型返回对应字段:品种表给名字,魔术号表把 magic 转成字符串,账户表取 account,都不匹配就回 'Unknown:' 加类型值。

MQL5 / C++
class="kw">switch(table_type)
  {
   case TABLE_SYMBOLS  : class="kw">return ArraySymbolStats[index].name;
   case TABLE_MAGICS    : class="kw">return (class="type">class="kw">string)ArrayMagicStats[index].magic;
   case TABLE_ACCOUNT  : class="kw">return (class="type">class="kw">string)ArrayAccountStats[index].account;
   class="kw">default             : class="kw">return "Unknown:"+(class="type">class="kw">string)table_type;
  }
 }
class=class="str">"cmt">//--- 其他数据类型,按预设小数位返回字符串
 class="kw">return(DoubleToString(GetDataStats(table_type, data_type, index),digits));
}
逐行看:前三个 case 直接读对应结构体数组的字段;default 拼 Unknown 前缀方便排错;末尾 DoubleToString 统一处理数值类,digits 控制小数位(一般 2 位够用,点差类可设 1)。 索引不是现成的。GetIndexSymbol 跑 ArraySymbolStats.Size() 次循环比对 name,命中即返回;GetIndexMagic 同理比 magic,均失败回 WRONG_VALUE(通常 -1)。品种数超 50 时线性查找略有延迟,但手动点击场景无感。 ViewStatistic 是真正入口:先抓 FieldH / FieldR 两个面板指针,空就撤;再用 cell_text 调上面两个索引函数。TABLE_SYMBOLS 走 GetIndexSymbol(cell_text),TABLE_MAGICS 需 StringToInteger 转换后再查——若你复制去改,注意魔术号是 long,强转 string 前别丢精度。外汇与贵金属波动剧烈,面板统计仅作复盘参考,实盘仍属高风险。

MQL5 / C++
class="kw">switch(table_type)
  {
   case TABLE_SYMBOLS  : class="kw">return ArraySymbolStats[index].name;
   case TABLE_MAGICS    : class="kw">return (class="type">class="kw">string)ArrayMagicStats[index].magic;
   case TABLE_ACCOUNT  : class="kw">return (class="type">class="kw">string)ArrayAccountStats[index].account;
   class="kw">default             : class="kw">return "Unknown:"+(class="type">class="kw">string)table_type;
  }
 }
class=class="str">"cmt">//--- For all other data types, class="kw">return their class="type">class="kw">string value with the previously defined number of decimal places
 class="kw">return(DoubleToString(GetDataStats(table_type, data_type, index),digits));
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the index of the symbol in the symbol statistics array     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int GetIndexSymbol(const class="type">class="kw">string symbol)
  {
  class="type">int total=(class="type">int)ArraySymbolStats.Size();
  for(class="type">int i=class="num">0;i<total;i++)
    {
     if(ArraySymbolStats[i].name==symbol)
       class="kw">return i;
    }
  class="kw">return WRONG_VALUE;
  }
class=class="str">"cmt">//+------------------------------------------------------------------------+
class=class="str">"cmt">//| Return the magic number index in the statistics array by magic numbers |
class=class="str">"cmt">//+------------------------------------------------------------------------+
class="type">int GetIndexMagic(const class="type">long magic)
  {
  class="type">int total=(class="type">int)ArrayMagicStats.Size();
  for(class="type">int i=class="num">0;i<total;i++)
    {
     if(ArrayMagicStats[i].magic==magic)
       class="kw">return i;
    }
  class="kw">return WRONG_VALUE;
  }
class=class="str">"cmt">//+--------------------------------------------------------------------+
class=class="str">"cmt">//| Display statistics for the selected symbol, magic number or account|
class=class="str">"cmt">//+--------------------------------------------------------------------+
class="type">bool ViewStatistic(const class="type">int table_type,const class="type">class="kw">string cell_text)
  {
class=class="str">"cmt">//--- Get the pointers to the header panel and the right panel for displaying statistics
  CDashboard *panel_h=dashboard.GetPanel("FieldH");
  CDashboard *panel_r=dashboard.GetPanel("FieldR");
  if(panel_h==NULL || panel_r==NULL)
     class="kw">return false;
     
class=class="str">"cmt">//--- Determine the source of statistical data(symbol/magic number/account)
  class="type">class="kw">string  ? "symbol" : table_type==TABLE_MAGICS ? "magic" : "account");
  class="type">int index=WRONG_VALUE;

class=class="str">"cmt">//--- Depending on the text in the selected table cell(cell_text) passed to the function,
class=class="str">"cmt">//--- get the index that contains the data in the corresponding statistics array
  class="kw">switch(table_type)
    {
     case TABLE_SYMBOLS:  index=GetIndexSymbol(cell_text);                      break;
     case TABLE_MAGICS :  index=GetIndexMagic(StringToInteger(cell_text));  break;

统计面板里按来源取数并落表的细节

这段逻辑处理的是右侧统计面板在切换查看维度(品种 / 魔术号 / 账户)时的数据装配。先通过 switch 拿到对应统计数组的下标,若数组只有 1 条就取 0,否则置为 -1;当 index 等于 WRONG_VALUE 时直接 PrintFormat 报错并返回 false,意味着对应维度的统计数组是空的,界面不会继续画表。 拿到 index 后,代码会暂存表头栏(panel_h)的字体属性:用 FontParams 读出 f_name、f_size、f_flags、f_angle,清栏后强制用 Tahoma 8 号字在坐标 (8,3) 以灰阶 C'150,150,150' 写一行“Trade statistics by %s %s”,写完立刻把字体还原回暂存值,避免污染后续表头渲染。 右侧面板先判断 TABLE_STATS 表是否存在,没有就 CreateNewTable 建表并取指针,空指针同样返回 false。随后 panel_r.Clear() 并 DrawGrid 画 2 像素边距、TABLE_STAT_ROWS 行 × TABLE_STAT_COLS 列、单元格高宽按 (Height()-4)/行数 与 (Width()-4)/列数 推算、网格色 C'230,230,230' 的表格。 因为 SSymbolStats / SMagicStats / SAccountStats 三个结构体字段基本一致,只首字段不同,这里直接声明 SSymbolStats stats 并按 table_type 把 ArraySymbolStats[index] 的 trades、gross_profit、gross_loss、total_commission、total_swap、net_profit、win_trades、loss_trades、long_trades、short_trades、expected_payoff、win_percent 等全部搬进 stats。你在 MT5 里改 TABLE_STAT_ROWS 或字体大小,能直接看到面板密度和可读性的变化,外汇与贵金属回测数据波动大,这类统计仅作概率参考,实盘高风险。

MQL5 / C++
case TABLE_ACCOUNT: index=(ArrayAccountStats.Size()==class="num">1 ? class="num">0 : -class="num">1); break;
    class="kw">default                 : break;
     }
class=class="str">"cmt">//--- If the index could not be obtained, we assume that the corresponding statistics array is empty
   if(index==WRONG_VALUE)
   {
      PrintFormat("%s: Error. Empty array of %s statistics",__FUNCTION__,source);
      class="kw">return false;
   }

class=class="str">"cmt">//--- Get and save the font properties set for the header bar
   class="type">int f_size,f_flags,f_angle;
   class="type">class="kw">string f_name=panel_h.FontParams(f_size,f_flags,f_angle);

class=class="str">"cmt">//--- Clear the header bar and display the description of the selected data in Tahoma font size class="num">8
   panel_h.Clear();
   panel_h.SetFontParams("Tahoma",class="num">8,f_flags,f_angle);
   panel_h.DrawText(StringFormat("Trade statistics by %s %s",source,cell_text),class="num">8,class="num">3,C&class="macro">#x27;class="num">150,class="num">150,class="num">150&class="macro">#x27;);
class=class="str">"cmt">//--- Return the header bar font to its previous saved properties
   panel_h.SetFontParams(f_name,f_size,f_flags,f_angle);

class=class="str">"cmt">//--- Check for availability and get or create a table object for displaying statistics on the right panel
   if(!panel_r.TableIsExist(TABLE_STATS) && !panel_r.CreateNewTable(TABLE_STATS))
      class="kw">return false;
class=class="str">"cmt">//--- Get the pointer to the created table
   CTableData *table_r=panel_r.GetTable(TABLE_STATS);
   if(table_r==NULL)
      class="kw">return false;
class=class="str">"cmt">//--- Clear the right panel and draw a table on it
   panel_r.Clear();
   panel_r.DrawGrid(TABLE_STATS,class="num">2,class="num">2,TABLE_STAT_ROWS,TABLE_STAT_COLS,(panel_r.Height()-class="num">4)/TABLE_STAT_ROWS,(panel_r.Width()-class="num">4)/TABLE_STAT_COLS,C&class="macro">#x27;class="num">230,class="num">230,class="num">230&class="macro">#x27;,false);
class=class="str">"cmt">//--- Declare a structure for storing statistics data
class=class="str">"cmt">//--- (symbol/magic number/account) by previously obtained data index.
class=class="str">"cmt">//--- All fields of the SSymbolStats, SMagicStats and SAccountStats structures are the same,
class=class="str">"cmt">//--- except for the first field with the symbol name, magic value or account number.
class=class="str">"cmt">//--- Since the first field is not needed here, any of the three structure types is sufficient.
class=class="str">"cmt">//--- Fill the declared structure with data depending on the selected source
   SSymbolStats stats={};
   class="kw">switch(table_type)
   {
      case TABLE_SYMBOLS:
         stats.trades           =  ArraySymbolStats[index].trades;
         stats.gross_profit     =  ArraySymbolStats[index].gross_profit;
         stats.gross_loss       =  ArraySymbolStats[index].gross_loss;
         stats.total_commission =  ArraySymbolStats[index].total_commission;
         stats.total_swap       =  ArraySymbolStats[index].total_swap;
         stats.total_profit     =  ArraySymbolStats[index].total_profit;
         stats.net_profit       =  ArraySymbolStats[index].net_profit;
         stats.win_trades       =  ArraySymbolStats[index].win_trades;
         stats.loss_trades      =  ArraySymbolStats[index].loss_trades;
         stats.long_trades      =  ArraySymbolStats[index].long_trades;
         stats.short_trades     =  ArraySymbolStats[index].short_trades;
         stats.expected_payoff  =  ArraySymbolStats[index].expected_payoff;
         stats.win_percent      =  ArraySymbolStats[index].win_percent;

◍ 按表类型灌入统计结构体的分支逻辑

这段 switch 分支负责把不同维度的历史统计结果,统一搬进一个临时的 stats 结构体,供右侧面板直接渲染。TABLE_SYMBOLS、TABLE_MAGICS、TABLE_ACCOUNT 三种模式拿的字段几乎一致,差别只在数据源数组。 TABLE_SYMBOLS 分支只取了胜率、亏损率、平均盈利、平均亏损和盈利因子这几个核心比值,没有搬手续费和库存费,说明按品种汇总时更关注赔率结构而非净贡献。 TABLE_MAGICS 与 TABLE_ACCOUNT 则完整拷贝了 trades、gross_profit、gross_loss、total_commission、total_swap、net_profit 等 16 个字段,连多空单数(long_trades / short_trades)和期望收益(expected_payoff)都带上了。 实际在 MT5 里接这段逻辑时,只要确认 ArraySymbolStats / ArrayMagicStats / ArrayAccountStats 三个全局数组已按 index 填好,面板就能切换查看品种、魔法码、账户三视图,不会漏字段。外汇与贵金属回测涉及高杠杆,统计再漂亮也只代表过去,切换视图时别把历史盈利因子当未来概率。

MQL5 / C++
    stats.loss_percent      =  ArraySymbolStats[index].loss_percent;
    stats.average_profit    =  ArraySymbolStats[index].average_profit;
    stats.average_loss      =  ArraySymbolStats[index].average_loss;
    stats.profit_factor     =  ArraySymbolStats[index].profit_factor;
    break;
    case TABLE_MAGICS :
      stats.trades           =  ArrayMagicStats[index].trades;
      stats.gross_profit     =  ArrayMagicStats[index].gross_profit;
      stats.gross_loss       =  ArrayMagicStats[index].gross_loss;
      stats.total_commission=  ArrayMagicStats[index].total_commission;
      stats.total_swap       =  ArrayMagicStats[index].total_swap;
      stats.total_profit     =  ArrayMagicStats[index].total_profit;
      stats.net_profit       =  ArrayMagicStats[index].net_profit;
      stats.win_trades       =  ArrayMagicStats[index].win_trades;
      stats.loss_trades      =  ArrayMagicStats[index].loss_trades;
      stats.long_trades      =  ArrayMagicStats[index].long_trades;
      stats.short_trades     =  ArrayMagicStats[index].short_trades;
      stats.expected_payoff =  ArrayMagicStats[index].expected_payoff;
      stats.win_percent      =  ArrayMagicStats[index].win_percent;
      stats.loss_percent     =  ArrayMagicStats[index].loss_percent;
      stats.average_profit   =  ArrayMagicStats[index].average_profit;
      stats.average_loss     =  ArrayMagicStats[index].average_loss;
      stats.profit_factor    =  ArrayMagicStats[index].profit_factor;
      break;
    case TABLE_ACCOUNT:
      stats.trades           =  ArrayAccountStats[index].trades;
      stats.gross_profit     =  ArrayAccountStats[index].gross_profit;
      stats.gross_loss       =  ArrayAccountStats[index].gross_loss;
      stats.total_commission=  ArrayAccountStats[index].total_commission;
      stats.total_swap       =  ArrayAccountStats[index].total_swap;
      stats.total_profit     =  ArrayAccountStats[index].total_profit;
      stats.net_profit       =  ArrayAccountStats[index].net_profit;
      stats.win_trades       =  ArrayAccountStats[index].win_trades;
      stats.loss_trades      =  ArrayAccountStats[index].loss_trades;
      stats.long_trades      =  ArrayAccountStats[index].long_trades;
      stats.short_trades     =  ArrayAccountStats[index].short_trades;
      stats.expected_payoff =  ArrayAccountStats[index].expected_payoff;
      stats.win_percent      =  ArrayAccountStats[index].win_percent;
      stats.loss_percent     =  ArrayAccountStats[index].loss_percent;
      stats.average_profit   =  ArrayAccountStats[index].average_profit;
      stats.average_loss     =  ArrayAccountStats[index].average_loss;
      stats.profit_factor    =  ArrayAccountStats[index].profit_factor;
      break;
    class="kw">default:
      break;
    }
class=class="str">"cmt">//---  Get and save the font properties set for the right panel

「把统计字段塞进表格坐标」

在右侧统计面板里,先用 FontParams 把字体定为 Tahoma、8 号、FW_BLACK,角度沿用 f_angle,保证后续文字渲染一致。 表格定位靠列数和单元格坐标算出来:cols 取第 0 行的总列数,cw 用 table_r.Width()/cols 得到单列宽;文字偏移写死 y_shift=6、x_shift=21,tw 存实测文本宽,避免右对齐时溢出。 取 cellH=GetCell(row,0) 放标签、cellV=GetCell(row,1) 放数值,任一为 NULL 直接 return false。Trades / Long / Short 直接转 string 画灰字 C'150,150,150';Net Profit 走 DoubleToString(value,2) 保留两位小数,数值列用 cw-tw-x_shift 做右对齐。 开 MT5 把这段贴进你的 CPanel 派生类,改 x_shift 到 21 以外的值,能直观看到文字在单元格里左右漂移。

MQL5 / C++
  f_name=panel_r.FontParams(f_size,f_flags,f_angle);
class=class="str">"cmt">//--- Set a new font Tahoma size class="num">8 for the right panel
  panel_r.SetFontParams("Tahoma",class="num">8,FW_BLACK,f_angle);
  
class=class="str">"cmt">//--- Variables for calculating the location of text in a table cell
  CTableCell *cellH=NULL, *cellV=NULL;
  class="type">int    cols=table_r.ColumnsInRow(class="num">0);   class=class="str">"cmt">// number of columns in the statistics table
  class="type">int    cw=table_r.Width()/cols;        class=class="str">"cmt">// width of one table column
  class="type">int    y_shift=class="num">6;                      class=class="str">"cmt">// shift text height
  class="type">int    x_shift=class="num">21;                     class=class="str">"cmt">// shift text width
  class="type">int    tw=class="num">0;                           class=class="str">"cmt">// text width
  class="type">class="kw">string text="";
  class="type">class="kw">double value=class="num">0;
  
class=class="str">"cmt">//--- Left column(data -- value)
class=class="str">"cmt">//--- Get cells class="num">0.0 and class="num">0.1 of the table and send Trades and its value to their coordinates
  cellH=table_r.GetCell(class="num">0,class="num">0);
  cellV=table_r.GetCell(class="num">0,class="num">1);
  if(cellH==NULL || cellV==NULL)
     class="kw">return false;
  text=(class="type">class="kw">string)stats.trades;
  tw=panel_r.TextWidth(text);
  panel_r.DrawText(H_TRADES+":",cellH.X()+x_shift,cellH.Y()+y_shift,C&class="macro">#x27;class="num">150,class="num">150,class="num">150&class="macro">#x27;);
  panel_r.DrawText(text,cellV.X()+cw-tw-x_shift,cellV.Y()+y_shift,C&class="macro">#x27;class="num">150,class="num">150,class="num">150&class="macro">#x27;);
  
class=class="str">"cmt">//--- Get cells class="num">1.0 and class="num">1.1 of the table and send Long and its value to their coordinates
  cellH=table_r.GetCell(class="num">1,class="num">0);
  cellV=table_r.GetCell(class="num">1,class="num">1);
  if(cellH==NULL || cellV==NULL)
     class="kw">return false;
  text=(class="type">class="kw">string)stats.long_trades;
  tw=panel_r.TextWidth(text);
  panel_r.DrawText(H_LONG+":",cellH.X()+x_shift,cellH.Y()+y_shift,C&class="macro">#x27;class="num">150,class="num">150,class="num">150&class="macro">#x27;);
  panel_r.DrawText(text,cellV.X()+cw-tw-x_shift,cellV.Y()+y_shift,C&class="macro">#x27;class="num">150,class="num">150,class="num">150&class="macro">#x27;);
  
class=class="str">"cmt">//--- Get cells class="num">2.0 and class="num">2.1 of the table and send Short and its value to their coordinates
  cellH=table_r.GetCell(class="num">2,class="num">0);
  cellV=table_r.GetCell(class="num">2,class="num">1);
  if(cellH==NULL || cellV==NULL)
     class="kw">return false;
  text=(class="type">class="kw">string)stats.short_trades;
  tw=panel_r.TextWidth(text);
  panel_r.DrawText(H_SHORT+":",cellH.X()+x_shift,cellH.Y()+y_shift,C&class="macro">#x27;class="num">150,class="num">150,class="num">150&class="macro">#x27;);
  panel_r.DrawText(text,cellV.X()+cw-tw-x_shift,cellV.Y()+y_shift,C&class="macro">#x27;class="num">150,class="num">150,class="num">150&class="macro">#x27;);
  
class=class="str">"cmt">//--- Get cells class="num">3.0 and class="num">3.1 of the table and send Net Profit and its value to their coordinates
  cellH=table_r.GetCell(class="num">3,class="num">0);
  cellV=table_r.GetCell(class="num">3,class="num">1);
  if(cellH==NULL || cellV==NULL)
     class="kw">return false;
  value=stats.net_profit;
  text=DoubleToString(value,class="num">2);
  tw=panel_r.TextWidth(text);
  panel_r.DrawText(H_NET_PROFIT+":",cellH.X()+x_shift,cellH.Y()+y_shift,C&class="macro">#x27;class="num">150,class="num">150,class="num">150&class="macro">#x27;);

统计面板里盈亏与费用的颜色映射

在 MT5 自定义统计面板里,第 4 到 8 行分别承载净利润、毛利、毛亏、佣金与库存费,每行取表对象的两个单元格:左边写标签,右边写数值。代码里先用 GetCell(行,0) 和 GetCell(行,1) 拿坐标,任一为 NULL 直接 return false,避免空指针把整个面板绘制打断。 数值颜色按正负分流:净利润用 C'86,119,204'(蓝)表示大于 0,C'234,50,50'(红)表示小于 0,灰 C'150,150,150' 留给持平。毛利只判正(蓝或灰),毛亏、佣金、掉期只判负(红或灰),因为后三者统计结构里不会是正数。 文本右对齐靠 TextWidth 算真实像素宽,再用 cellV.X()+cw-tw-x_shift 把字往左推。外汇与贵金属短线刷单时佣金和 swap 常吃掉毛利,把这个面板接进 EA 回测报告,能直观看出哪段样本净贡献为负,贵金属隔夜单尤其要盯第 8 行。

MQL5 / C++
  panel_r.DrawText(text,cellV.X()+cw-tw-x_shift,cellV.Y()+y_shift,(value>class="num">0 ? C&class="macro">#x27;class="num">86,class="num">119,class="num">204&class="macro">#x27; : value<class="num">0 ? C&class="macro">#x27;class="num">234,class="num">50,class="num">50&class="macro">#x27; : C&class="macro">#x27;class="num">150,class="num">150,class="num">150&class="macro">#x27;));
  
class=class="str">"cmt">//--- Get cells class="num">4.0 and class="num">4.1 of the table and send Profit Loss and its value to their coordinates
  cellH=table_r.GetCell(class="num">4,class="num">0);
  cellV=table_r.GetCell(class="num">4,class="num">1);
  if(cellH==NULL || cellV==NULL)
    class="kw">return false;
  value=stats.total_profit;
  text=DoubleToString(value,class="num">2);
  tw=panel_r.TextWidth(text);
  panel_r.DrawText(H_PROFITS+":",cellH.X()+x_shift,cellH.Y()+y_shift,C&class="macro">#x27;class="num">150,class="num">150,class="num">150&class="macro">#x27;);
  panel_r.DrawText(text,cellV.X()+cw-tw-x_shift,cellV.Y()+y_shift,(value>class="num">0 ? C&class="macro">#x27;class="num">86,class="num">119,class="num">204&class="macro">#x27; : value<class="num">0 ? C&class="macro">#x27;class="num">234,class="num">50,class="num">50&class="macro">#x27; : C&class="macro">#x27;class="num">150,class="num">150,class="num">150&class="macro">#x27;));
  
class=class="str">"cmt">//--- Get cells class="num">5.0 and class="num">5.1 of the table and send Gross Profit and its value to their coordinates
  cellH=table_r.GetCell(class="num">5,class="num">0);
  cellV=table_r.GetCell(class="num">5,class="num">1);
  if(cellH==NULL || cellV==NULL)
    class="kw">return false;
  value=stats.gross_profit;
  text=DoubleToString(value,class="num">2);
  tw=panel_r.TextWidth(text);
  panel_r.DrawText(H_GROSS_PROFIT+":",cellH.X()+x_shift,cellH.Y()+y_shift,C&class="macro">#x27;class="num">150,class="num">150,class="num">150&class="macro">#x27;);
  panel_r.DrawText(text,cellV.X()+cw-tw-x_shift,cellV.Y()+y_shift,(value>class="num">0 ? C&class="macro">#x27;class="num">86,class="num">119,class="num">204&class="macro">#x27; : C&class="macro">#x27;class="num">150,class="num">150,class="num">150&class="macro">#x27;));
  
class=class="str">"cmt">//--- Get cells class="num">6.0 and class="num">6.1 of the table and send Gross Loss and its value to their coordinates
  cellH=table_r.GetCell(class="num">6,class="num">0);
  cellV=table_r.GetCell(class="num">6,class="num">1);
  if(cellH==NULL || cellV==NULL)
    class="kw">return false;
  value=stats.gross_loss;
  text=DoubleToString(value,class="num">2);
  tw=panel_r.TextWidth(text);
  panel_r.DrawText(H_GROSS_LOSS+":",cellH.X()+x_shift,cellH.Y()+y_shift,C&class="macro">#x27;class="num">150,class="num">150,class="num">150&class="macro">#x27;);
  panel_r.DrawText(text,cellV.X()+cw-tw-x_shift,cellV.Y()+y_shift,(value<class="num">0 ? C&class="macro">#x27;class="num">234,class="num">50,class="num">50&class="macro">#x27; : C&class="macro">#x27;class="num">150,class="num">150,class="num">150&class="macro">#x27;));
  
class=class="str">"cmt">//--- Get cells class="num">7.0 and class="num">7.1 of the table and send Commission and its value to their coordinates
  cellH=table_r.GetCell(class="num">7,class="num">0);
  cellV=table_r.GetCell(class="num">7,class="num">1);
  if(cellH==NULL || cellV==NULL)
    class="kw">return false;
  value=stats.total_commission;
  text=DoubleToString(value,class="num">2);
  tw=panel_r.TextWidth(text);
  panel_r.DrawText(H_COMMISSIONS+":",cellH.X()+x_shift,cellH.Y()+y_shift,C&class="macro">#x27;class="num">150,class="num">150,class="num">150&class="macro">#x27;);
  panel_r.DrawText(text,cellV.X()+cw-tw-x_shift,cellV.Y()+y_shift,(value<class="num">0 ? C&class="macro">#x27;class="num">234,class="num">50,class="num">50&class="macro">#x27; : C&class="macro">#x27;class="num">150,class="num">150,class="num">150&class="macro">#x27;));
  
class=class="str">"cmt">//--- Get cells class="num">8.0 and class="num">8.1 of the table and send Swap and its value to their coordinates
  cellH=table_r.GetCell(class="num">8,class="num">0);
  cellV=table_r.GetCell(class="num">8,class="num">1);
  if(cellH==NULL || cellV==NULL)
    class="kw">return false;
  value=stats.total_swap;
  text=DoubleToString(value,class="num">2);
  tw=panel_r.TextWidth(text);
  panel_r.DrawText(H_SWAPS+":",cellH.X()+x_shift,cellH.Y()+y_shift,C&class="macro">#x27;class="num">150,class="num">150,class="num">150&class="macro">#x27;);
  panel_r.DrawText(text,cellV.X()+cw-tw-x_shift,cellV.Y()+y_shift,(value<class="num">0 ? C&class="macro">#x27;class="num">234,class="num">50,class="num">50&class="macro">#x27; : C&class="macro">#x27;class="num">150,class="num">150,class="num">150&class="macro">#x27;));
  

class=class="str">"cmt">//--- Right column(data -- value)

◍ 把统计字段映射到面板表格坐标

在自定义面板里渲染交易统计时,最易踩的坑是单元格指针为空却直接写文本,导致整个 Draw 流程中断。下面这段逻辑按行号 0~5 取表格右半区的第 2、3 列单元格,把胜败笔数、期望收益、胜率、败率、平均利润逐项落座。 取 0.2 与 0.3 单元格写入盈利笔数:先判空,空则 return false 终止;text 直接由 stats.win_trades 转字符串,再用 TextWidth 测宽,让数值右对齐到 cellV 内边距。亏损笔数同理走 1.2 / 1.3,颜色统一用灰阶 C'150,150,150'。 期望收益与胜率走 DoubleToString(value,2) 保留两位小数;胜率文字着色 C'86,119,204'(蓝),败率着色 C'234,50,50'(红),一眼区分多空倾向。平均利润在 5.2 / 5.3 单元格输出,同样先测宽后偏移。 外汇与贵金属波动剧烈,面板数值仅反映历史样本,实盘复制参数前请在 MT5 策略测试器用至少 200 根 H1 数据验证,避免小样本下统计失真。

MQL5 / C++
class=class="str">"cmt">//--- Get cells class="num">0.2 and class="num">0.3 of the table and send Win trades and its value to their coordinates
  cellH=table_r.GetCell(class="num">0,class="num">2);
  cellV=table_r.GetCell(class="num">0,class="num">3);
  if(cellH==NULL || cellV==NULL)
     class="kw">return false;
  text=(class="type">class="kw">string)stats.win_trades;
  tw=panel_r.TextWidth(text);
  panel_r.DrawText(H_WINS+":",cellH.X()+x_shift,cellH.Y()+y_shift,C&class="macro">#x27;class="num">150,class="num">150,class="num">150&class="macro">#x27;);
  panel_r.DrawText(text,cellV.X()+cw-tw-x_shift,cellV.Y()+y_shift,C&class="macro">#x27;class="num">150,class="num">150,class="num">150&class="macro">#x27;);

class=class="str">"cmt">//--- Get cells class="num">1.2 and class="num">1.3 of the table and send Loss trades and its value to their coordinates
  cellH=table_r.GetCell(class="num">1,class="num">2);
  cellV=table_r.GetCell(class="num">1,class="num">3);
  if(cellH==NULL || cellV==NULL)
     class="kw">return false;
  text=(class="type">class="kw">string)stats.loss_trades;
  tw=panel_r.TextWidth(text);
  panel_r.DrawText(H_LOST+":",cellH.X()+x_shift,cellH.Y()+y_shift,C&class="macro">#x27;class="num">150,class="num">150,class="num">150&class="macro">#x27;);
  panel_r.DrawText(text,cellV.X()+cw-tw-x_shift,cellV.Y()+y_shift,C&class="macro">#x27;class="num">150,class="num">150,class="num">150&class="macro">#x27;);

class=class="str">"cmt">//--- Get cells class="num">2.2 and class="num">2.3 of the table and send Expected Payoff and its value to their coordinates
  cellH=table_r.GetCell(class="num">2,class="num">2);
  cellV=table_r.GetCell(class="num">2,class="num">3);
  if(cellH==NULL || cellV==NULL)
     class="kw">return false;
  value=stats.expected_payoff;
  text=DoubleToString(value,class="num">2);
  tw=panel_r.TextWidth(text);
  panel_r.DrawText(H_EXP_PAYOFF+":",cellH.X()+x_shift,cellH.Y()+y_shift,C&class="macro">#x27;class="num">150,class="num">150,class="num">150&class="macro">#x27;);
  panel_r.DrawText(text,cellV.X()+cw-tw-x_shift,cellV.Y()+y_shift,C&class="macro">#x27;class="num">150,class="num">150,class="num">150&class="macro">#x27;);

class=class="str">"cmt">//--- Get cells class="num">3.2 and class="num">3.3 of the table and send Win percent and its value to their coordinates
  cellH=table_r.GetCell(class="num">3,class="num">2);
  cellV=table_r.GetCell(class="num">3,class="num">3);
  if(cellH==NULL || cellV==NULL)
     class="kw">return false;
  value=stats.win_percent;
  text=DoubleToString(value,class="num">2);
  tw=panel_r.TextWidth(text);
  panel_r.DrawText(H_WIN_PRC+":",cellH.X()+x_shift,cellH.Y()+y_shift,C&class="macro">#x27;class="num">150,class="num">150,class="num">150&class="macro">#x27;);
  panel_r.DrawText(text,cellV.X()+cw-tw-x_shift,cellV.Y()+y_shift,C&class="macro">#x27;class="num">86,class="num">119,class="num">204&class="macro">#x27;);

class=class="str">"cmt">//--- Get cells class="num">4.2 and class="num">4.3 of the table and send Loss percent and its value to their coordinates
  cellH=table_r.GetCell(class="num">4,class="num">2);
  cellV=table_r.GetCell(class="num">4,class="num">3);
  if(cellH==NULL || cellV==NULL)
     class="kw">return false;
  value=stats.loss_percent;
  text=DoubleToString(value,class="num">2);
  tw=panel_r.TextWidth(text);
  panel_r.DrawText(H_LOSS_PRC+":",cellH.X()+x_shift,cellH.Y()+y_shift,C&class="macro">#x27;class="num">150,class="num">150,class="num">150&class="macro">#x27;);
  panel_r.DrawText(text,cellV.X()+cw-tw-x_shift,cellV.Y()+y_shift,C&class="macro">#x27;class="num">234,class="num">50,class="num">50&class="macro">#x27;);

class=class="str">"cmt">//--- Get cells class="num">5.2 and class="num">5.3 of the table and send Average Profit and its value to their coordinates
  cellH=table_r.GetCell(class="num">5,class="num">2);
  cellV=table_r.GetCell(class="num">5,class="num">3);
  if(cellH==NULL || cellV==NULL)
     class="kw">return false;
  value=stats.average_profit;
  text=DoubleToString(value,class="num">2);
  tw=panel_r.TextWidth(text);
  panel_r.DrawText(H_AVG_PROFIT+":",cellH.X()+x_shift,cellH.Y()+y_shift,C&class="macro">#x27;class="num">150,class="num">150,class="num">150&class="macro">#x27;);

「把回测统计写进右侧面板的坐标」

这段逻辑负责把平均亏损和盈利因子两项指标,按表格第 6、7 行的单元格坐标绘制到右侧面板。先取 6.2 与 6.3 单元格,若任一为空直接返回 false,避免空指针导致面板渲染中断。 平均亏损值来自 stats.average_loss,用 DoubleToString(value,2) 保留两位小数;文本宽度由 TextWidth 测得,右对齐时以 cw-tw-x_shift 做水平偏移。值为负时着红色 C'234,50,50',否则灰色 C'150,150,150',亏损可视化一眼可辨。 盈利因子走同样流程:取 7.2、7.3 单元格,stats.profit_factor 转字符串后绘制,颜色统一用灰色,不随大小变色。两项标签 H_AVG_LOSS、H_PRF_FACTOR 均带冒号,与数值分列两列。 收尾处用 SetFontParams 还原之前保存的字体名称、尺寸、标志与角度,返回 true 表示本次绘制完整。外汇与贵金属回测统计受点差与滑点影响,面板数值仅反映历史样本,实盘表现可能有偏差。

MQL5 / C++
  panel_r.DrawText(text,cellV.X()+cw-tw-x_shift,cellV.Y()+y_shift,(value>class="num">0 ? C&class="macro">#x27;class="num">86,class="num">119,class="num">204&class="macro">#x27; : C&class="macro">#x27;class="num">150,class="num">150,class="num">150&class="macro">#x27;));
  
class=class="str">"cmt">//--- Get cells class="num">6.2 and class="num">6.3 of the table and send Average Loss and its value to their coordinates
  cellH=table_r.GetCell(class="num">6,class="num">2);
  cellV=table_r.GetCell(class="num">6,class="num">3);
  if(cellH==NULL || cellV==NULL)
      class="kw">return false;
  value=stats.average_loss;
  text=DoubleToString(value,class="num">2);
  tw=panel_r.TextWidth(text);
  panel_r.DrawText(H_AVG_LOSS+":",cellH.X()+x_shift,cellH.Y()+y_shift,C&class="macro">#x27;class="num">150,class="num">150,class="num">150&class="macro">#x27;);
  panel_r.DrawText(text,cellV.X()+cw-tw-x_shift,cellV.Y()+y_shift,(value<class="num">0 ? C&class="macro">#x27;class="num">234,class="num">50,class="num">50&class="macro">#x27; : C&class="macro">#x27;class="num">150,class="num">150,class="num">150&class="macro">#x27;));
  
class=class="str">"cmt">//--- Get cells class="num">7.2 and class="num">7.3 of the table and send Profit factor and its value to their coordinates
  cellH=table_r.GetCell(class="num">7,class="num">2);
  cellV=table_r.GetCell(class="num">7,class="num">3);
  if(cellH==NULL || cellV==NULL)
      class="kw">return false;
  value=stats.profit_factor;
  text=DoubleToString(value,class="num">2);
  tw=panel_r.TextWidth(text);
  panel_r.DrawText(H_PRF_FACTOR+":",cellH.X()+x_shift,cellH.Y()+y_shift,C&class="macro">#x27;class="num">150,class="num">150,class="num">150&class="macro">#x27;);
  panel_r.DrawText(text,cellV.X()+cw-tw-x_shift,cellV.Y()+y_shift,C&class="macro">#x27;class="num">150,class="num">150,class="num">150&class="macro">#x27;);
  
class=class="str">"cmt">//--- Return the right panel font to its previous saved properties
  panel_r.SetFontParams(f_name,f_size,f_flags,f_angle);
  class="kw">return true;
}

最后一句大实话

这套统计仪表盘把所有类、函数和指标文件都打进了附件,解压后丢进终端数据目录的 \MQL5\Indicators\StatisticsBy 文件夹就能直接编译跑起来,文件体积摆在那:Dashboard.mqh 约 257 KB、StatisticsBy.mq5 约 165 KB、MQL5.zip 约 45 KB。 但评论区有人实测,在请求大量交易历史时工具会卡死,光拉历史就要五分钟,之后窗口因 CPU 负载过高完全没法操作;外汇和贵金属交易本身高风险,这种本地仪表盘只帮你算数,不替你扛回撤。 真要拿来用,先开 MT5 挂个小账户历史跑一遍,别直接丢实盘大批量订单数据进去,否则卡住的是你自己的终端。

常见问题

用键值对接口把面板左上角坐标和每个控件的偏移写进本地存储,启动时先读再布局,避免每次重算错位。
调用文本测量接口拿到字符串像素宽,再结合控件开关状态决定显隐与间距,动态算坐标就不会挤在一起。
小布可以扫描你的面板配置,标出坐标越界和透明度异常的控件,并给出一键修正建议,不用你手动翻参数。
取坐标前先判空和查索引范围,越界就打日志返回默认边界,表格管理逻辑才稳,不会中途报错退出。
用带容错的取边界写法,拿不到就填占位值并写警告日志,保证日志连续输出、方便你回头排查哪格数据缺了。