向指标或者EA中快速添加控制面板(基础篇)
📘

向指标或者EA中快速添加控制面板(基础篇)

第 1/3 篇

「给EA或指标塞一个控制面板」

在 MT5 里给指标或 EA 加一个可视化控制面板,不必从零写窗体类。借助标准库里的 CAppDialog 派生结构,可以把按钮、编辑框、标签直接挂到图表左上角,运行时点一下就能改参数。 官方示例工程发布于 2016-03-28,至今在终端里仍可被 7,625 次浏览记录回溯,说明这套面板骨架在 MQL5 社区属于高频复用件。 实际做法:新建一个继承 CAppDialog 的类,在 Create() 里用 CreateButton() / CreateEdit() 把控件铺进去,再在 OnClick() 里读控件状态去改输入输出变量。这样比每次手动进 EA 属性页调参更快,也方便做半自动策略切换。外汇与贵金属品种波动剧烈,面板只是操作入口,参数误设会放大高风险。

用图形面板绕开重启改参

再厉害的 MQL5 指标或 EA,默认也得进输入参数面板改设置,一改就要重编译或重启,实盘里很打断节奏。 基于标准类库自己挂一个图形面板,就能在图表上直接调参并即时生效,不用杀进程重开。MT5 市场里能翻到不少现成面板样例,交互层次差很多。 本文这节只做最小可行版:拖一个简易面板进程序,让 EA 能读输入值并响应变动。外汇和贵金属波动大、杠杆高,面板只是操作便利,不改变策略本身的风险属性。

◍ 给新柱检测指标挂上可控面板

新柱到来检测的核心其实只有一句话:比较当前 tick 的 time[0] 与上一 tick 留存的开盘时间。NewBar.mq5 在 OnCalculate() 里用 static 变量 prev_time 存旧值,只要 time[0] > prev_time 成立,就判定柱子切换。 拿平静行情下的 10 个 tick 举例:tick 1-3 的 time[0] 都等于 prev_time(如 00:01:00),不触发;tick 4 的 time[0] 变成 2015.12.01 00:02:00,大于 prev_time,判定新柱,随后 prev_time 被刷新为 00:02:00;tick 5-8 平静;tick 9 的 time[0] 为 00:03:00 再次大于前值,触发后更新;tick 10 无变化。这套逻辑在 MT5 里编译后,EA 日志会按此节奏打印新柱消息。 原生面板类 PanelDialog.mqh 只管绘图坐标和控件数量,本身没有读写信函、推送、弹窗开关状态的方法。要让指标变成可交互工具,就得在包含文件里补上 SetCheck / GetCheck 这类方法,并在 NewBar.mq5 中声明面板对象、挂上 OnChartEvent() 接收点击。 改完后在 OnInit() 里按输入参数创建面板,MetaEditor 按 F7 编译,终端输入参数页应出现 Mail / Push / Alert 三个独立勾选框。外汇与贵金属市场高杠杆、滑点频繁,实盘前务必在模拟环境验证新柱触发与报警的实际延迟。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                  NewBar.mq5 |
class=class="str">"cmt">//|             Copyright class="num">2015, MetaQuotes Software Corp. |
class=class="str">"cmt">//|                 [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2015, MetaQuotes Software Corp."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#class="kw">property description "The indicator identifies a new bar"
class="macro">#class="kw">property indicator_chart_window
class="macro">#class="kw">property indicator_plots class="num">0
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 自定义指标初始化函数                       
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- 指标缓存映射
class=class="str">"cmt">//---
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 自定义指标迭代函数                     
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnCalculate(class="kw">const class="type">int rates_total,
                class="kw">const class="type">int prev_calculated,
                class="kw">const class="type">class="kw">datetime &time[],
                class="kw">const class="type">class="kw">double &open[],
                class="kw">const class="type">class="kw">double &high[],
                class="kw">const class="type">class="kw">double &low[],
                class="kw">const class="type">class="kw">double &close[],

「新K线判定与面板控件骨架」

在自定义指标里区分「新柱」最稳妥的做法,是用静态变量记住上一帧的 time[0],每次 OnCalculate 进来先 ArraySetAsSeries(time,true) 把时间数组倒序成和图表一致的最新在前的结构。 下面这段核心逻辑只做一件事:首次计算(prev_calculated==0)时把 prev_time 初始化为当前柱时间并直接返回 rates_total;之后若 time[0] 比记录的 prev_time 更大,说明来了新 bar,Print("New bar!") 打日志,再更新 prev_time。外汇与贵金属行情跳空频繁,这种判定在实盘里可能因跳空跳过中间柱,概率上要留意非连续更新。

MQL5 / C++
class="kw">const class="type">long &tick_volume[],
class="kw">const class="type">long &volume[],
class="kw">const class="type">int &spread[])
  {
   class="kw">static class="type">class="kw">datetime prev_time;
class=class="str">"cmt">//--- 转置time[]数组的访问顺序 - 就像在时间系列中的一样
   ArraySetAsSeries(time,true);
class=class="str">"cmt">//--- 首次计算或者已经改变的柱形数量
   if(prev_calculated==class="num">0)class=class="str">"cmt">// 首次计算
     {
      prev_time=time[class="num">0];
      class="kw">return(rates_total);
     }
class=class="str">"cmt">//---
   if(time[class="num">0]>prev_time)
      Print("New bar!");
class=class="str">"cmt">//---
   prev_time=time[class="num">0];
class=class="str">"cmt">//---返回prev_calculated的值用于下次调用
   class="kw">return(rates_total);
  }
逐行拆解:第1~3行是 OnCalculate 尾部参数声明(tick_volume 即时成交量、volume 真实成交量、spread 点差数组,都用引用传入);static datetime prev_time 跨调用保留上次时间。ArraySetAsSeries(time,true) 让 time[0] 指向最新柱。prev_calculated==0 分支只跑一次,赋初值并返回全量长度。time[0]>prev_time 成立即新柱,打印后覆盖 prev_time,最后把 rates_total 交还 MT5 调度器。 界面侧若要做控件面板,可继承 CAppDialog 写个 CControlsDialog 类,私有成员放 CCheckGroup m_check_group 做多选组。布局上用宏定死边距:INDENT_LEFT / INDENT_TOP / INDENT_BOTTOM 都是 11 像素留边界,BUTTON_WIDTH 给 100 像素控 X 尺寸,直接抄进代码就能在 MT5 里编译出带复选框的对话框雏形。

MQL5 / C++
class="kw">const class="type">long &tick_volume[],
class="kw">const class="type">long &volume[],
class="kw">const class="type">int &spread[])
  {
   class="kw">static class="type">class="kw">datetime prev_time;
class=class="str">"cmt">//--- 转置time[]数组的访问顺序 - 就像在时间系列中的一样
   ArraySetAsSeries(time,true);
class=class="str">"cmt">//--- 首次计算或者已经改变的柱形数量
   if(prev_calculated==class="num">0)class=class="str">"cmt">// 首次计算
     {
      prev_time=time[class="num">0];
      class="kw">return(rates_total);
     }
class=class="str">"cmt">//---
   if(time[class="num">0]>prev_time)
      Print("New bar!");
class=class="str">"cmt">//---
   prev_time=time[class="num">0];
class=class="str">"cmt">//---返回prev_calculated的值用于下次调用
   class="kw">return(rates_total);
  }

对话框里挂复选组的落地写法

在 MT5 自定义面板开发中,把独立控件塞进对话框不能只靠声明,得在 Create 里显式调一次子控件创建函数。下面这段 CControlsDialog 的写法就是标准路径:先让基类 CAppDialog 把对话框画出来,再调 CreateCheckGroup 把复选组挂进客户区。 EVENT_MAP_BEGIN 里把 ON_CHANGE 映射到 m_check_group 的 OnChangeCheckGroup,意味着用户勾选状态变化会直接触发这个类内处理函数,不用在 OnEvent 里手写分支判断。 CreateCheckGroup 里坐标用了 INDENT_LEFT、INDENT_TOP 等宏,x2 直接等于 x1+BUTTON_WIDTH,y2 取到 ClientAreaHeight()-INDENT_BOTTOM。这几个宏若未定义,编译会报未声明标识符;开 MT5 新建 EA 把这段代码贴进自定义面板类,先确认这些宏在你的框架头文件里有值(常见为 11、18、120 之类),否则控件会画出界。 外汇与贵金属图表上挂这类面板属于高频交互改造,MT5 脚本异常可能引发下单延迟,实盘前务必在模拟盘验证。

MQL5 / C++
class="kw">virtual class="type">bool      Create(class="kw">const class="type">long chart,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwin,class="kw">const class="type">int x1,class="kw">const class="type">int y1,class="kw">const class="type">int x2,class="kw">const class="type">int y2);
  class=class="str">"cmt">//--- 图表事件处理函数
  class="kw">virtual class="type">bool      OnEvent(class="kw">const class="type">int id,class="kw">const class="type">long &lparam,class="kw">const class="type">class="kw">double &dparam,class="kw">const class="type">class="kw">string &sparam);
class="kw">protected:
  class=class="str">"cmt">//--- 创建独立控件
  class="type">bool              CreateCheckGroup(class="type">void);
  class=class="str">"cmt">//--- 独立控件事件处理函数
  class="type">void              OnChangeCheckGroup(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 事件处理                                                        
class=class="str">"cmt">//+------------------------------------------------------------------+
EVENT_MAP_BEGIN(CControlsDialog)
ON_EVENT(ON_CHANGE,m_check_group,OnChangeCheckGroup)
EVENT_MAP_END(CAppDialog)
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 构造函数                                                         
class=class="str">"cmt">//+------------------------------------------------------------------+
CControlsDialog::CControlsDialog(class="type">void)
  {
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 析构函数                                                         
class=class="str">"cmt">//+------------------------------------------------------------------+
CControlsDialog::~CControlsDialog(class="type">void)
  {
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建                                                            
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CControlsDialog::Create(class="kw">const class="type">long chart,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwin,class="kw">const class="type">int x1,class="kw">const class="type">int y1,class="kw">const class="type">int x2,class="kw">const class="type">int y2)
  {
  if(!CAppDialog::Create(chart,name,subwin,x1,y1,x2,y2))
     class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 创建独立控件
  if(!CreateCheckGroup())
     class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 成功
  class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建 "CheckGroup" 元素
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CControlsDialog::CreateCheckGroup(class="type">void)
  {
class=class="str">"cmt">//--- 坐标
  class="type">int x1=INDENT_LEFT;
  class="type">int y1=INDENT_TOP;
  class="type">int x2=x1+BUTTON_WIDTH;
  class="type">int y2=ClientAreaHeight()-INDENT_BOTTOM;
class=class="str">"cmt">//--- 创建
  if(!m_check_group.Create(m_chart_id,m_name+"CheckGroup",m_subwin,x1,y1,x2,y2))
     class="kw">return(class="kw">false);
  if(!Add(m_check_group))
     class="kw">return(class="kw">false);

◍ 用位掩码把通知开关塞进一个复选组

CCheckGroup 的 AddItem 第二个参数不是顺序编号,而是位权。上面那段把 Mail 设成 1<<0(值1)、Push 设成 1<<1(值2)、Alert 设成 1<<2(值4),三个选项同时勾选时 m_check_group.Value() 返回 7,单独勾 Alert 返回 4。 初始化里用 input bool 接外部开关:bln_mail 默认 false、bln_push 默认 false、bln_alert 默认 true,对应到位权就是 0、0、4,初始 Value 倾向为 4。想改默认通知组合,直接调这三个 input 即可,不必动界面代码。 OnChangeCheckGroup 里只做了一件事——把当前 Value 用 Comment 打到图表左上角。实盘加载后勾选/取消任意一项,左上角数字会实时变,MT5 里跑一遍就能确认位运算没接错。外汇与贵金属波动剧烈,这类面板只管信号分发,不替代风控。

MQL5 / C++
m_check_group.Alignment(WND_ALIGN_HEIGHT,class="num">0,y1,class="num">0,INDENT_BOTTOM);
class=class="str">"cmt">//--- 用字符填充
if(!m_check_group.AddItem("Mail",class="num">1<<class="num">0))
   class="kw">return(class="kw">false);
if(!m_check_group.AddItem("Push",class="num">1<<class="num">1))
   class="kw">return(class="kw">false);
if(!m_check_group.AddItem("Alert",class="num">1<<class="num">2))
   class="kw">return(class="kw">false);
Comment(__FUNCTION__+" : Value="+IntegerToString(m_check_group.Value()));
class=class="str">"cmt">//--- 成功
class="kw">return(true);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 事件处理                                                          
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CControlsDialog::OnChangeCheckGroup(class="type">void)
  {
  Comment(__FUNCTION__+" : Value="+IntegerToString(m_check_group.Value()));
  }

常见问题

用图形对象画一个控制面板,把参数绑定到面板控件,运行时直接点选修改即可,绕开重启重载。
记录上一根K线时间,和当前时间比对,不等就判定为新柱,再触发面板更新与逻辑重算。
小布可读取你的品种页并基于AIGC给出面板搭建思路与参数绑定建议,你照着落代码即可。
把多个开关塞进一个复选组控件,用户勾选即生效,避免堆一堆独立按钮占地方。
每个勾选项对应一个二进制位,选中置1未选置0,按位或汇总成一个int,读取时再用位与还原。