向指标或者EA中快速添加控制面板(基础篇)
「给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 三个独立勾选框。外汇与贵金属市场高杠杆、滑点频繁,实盘前务必在模拟环境验证新柱触发与报警的实际延迟。
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。外汇与贵金属行情跳空频繁,这种判定在实盘里可能因跳空跳过中间柱,概率上要留意非连续更新。
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); }
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 脚本异常可能引发下单延迟,实盘前务必在模拟盘验证。
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 里跑一遍就能确认位运算没接错。外汇与贵金属波动剧烈,这类面板只管信号分发,不替代风控。
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())); }