利用 MQL5 和 MQL4 实现的选择和导航实用程序:添加"homework"选项卡并保存图形对象(基础篇)
「给导航工具加 homework 选项卡并落盘图形对象」
在 MT5 里做选择和导航实用程序时,原生 Project 视图并不适合把临时画线、矩形这类图形对象按交易想法分组留存。Roman Klymenko 在 2019-02-06 发布的这套 MQL5/MQL4 双版本工具,核心是在原有导航树里塞进一个 homework 选项卡,专门挂用户自定义的图形对象引用。 实现落盘的关键,是借用了 MQL5 的 OBJPROP_LEVELS 与自定义全局变量配合文件流,把对象句柄和坐标写进终端公共目录的 csv。这样重启终端后,homework 选项卡能重新把对象拉回原图表,而不是丢得一干二净。 实际价值在于:你做价格行为复盘时,可以把几组未平仓假设对应的趋势线、斐波那契都扔进 homework,下次开盘直接调出,避免重复手动画线。外汇与贵金属波动大、滑点不可控,这类工具只解决组织效率,不替你判断方向。
◍ 把图表上的手绘对象自动存下来
上一篇文章里我们做了筛选合适入场品种的工具,能按参数排序、用按钮翻看品种。但落地时有个尴尬:选好的品种代码只能先记在纸上,既麻烦也不环保。 这篇直接解决这个断点——在 MT5 图表上画的趋势线、矩形、斐波那契这类图形对象,能自动写入本地预设,下次加载模板或切换周期时不用重画。外汇与贵金属波动快,手动补线容易漏掉关键位,自动保存相当于把你的价格行为分析痕迹固化下来。 实现思路不复杂:监听图表上的 OBJ_ 系列事件,把对象属性序列化进 bin 或 csv,开盘前一键恢复。高风险提示:自动化只管“画过什么”,不替你判断该不该画,信号失效时旧对象可能误导入场。
用条件编译让同一份 EA 跑在 MT4 和 MT5
把一套实用程序同时维护 MQL4 和 MQL5 两套源码,是最容易漏改、最费神的写法。更干净的做法是只写一份代码,靠编译器自带的条件指令,在构建时自动切到对应语言的实现块。
MQL4 与 MQL5 都支持 #ifdef __MQL5__ 这类预处理构造:命中 MQL5 就编进第一段,否则走 #else 里的 MQL4 分支,最后 #endif 收口。这样在 MT5 终端里编译,持仓查询走 PositionsTotal;丢进 MT4 终端,同一函数自动改成 OrdersTotal + OrderSelect。
下面这段 checkSYMBwithPOS 就是实战模板:入参是品种名,返回该品种是否已有持仓或挂单需要跳过。noSYMBwithPOS 为 true 时才进入检查,避免无谓循环。
MQL5 分支用 PositionsTotal 倒序扫持仓,再用 OrdersTotal 扫挂单,靠 PositionGetSymbol / OrderGetString(ORDER_SYMBOL) 比对 name;MQL4 分支没有持仓与订单的分离接口,只能统一用 OrdersTotal + OrderSelect(SELECT_BY_POS, MODE_TRADES) 后取 OrderSymbol。两者语义一致,但 API 完全不同。
开 MT5 新建脚本粘入下方代码,把 noSYMBwithPOS 置 true,分别用 MT4/MT5 编译一次,能在各自终端正常返回布尔值即验证通过。外汇与贵金属杠杆高,实盘前务必先在模拟盘确认分支逻辑无误。
class="macro">#ifdef __MQL5__ class=class="str">"cmt">// 代码块仅在 MQL5 中执行 class="macro">#else class=class="str">"cmt">// 代码块仅在 MQL4 中执行 class="macro">#endif class="type">bool checkSYMBwithPOS(class="type">class="kw">string name){ class=class="str">"cmt">// 隐藏已有持仓和订单的品种 class="type">bool isskip=false; if( noSYMBwithPOS ){ class="macro">#ifdef __MQL5__ class=class="str">"cmt">// 查看所有持仓清单 class="type">int cntMyPos=PositionsTotal(); for(class="type">int ti=cntMyPos-class="num">1; ti>=class="num">0; ti--){ class=class="str">"cmt">// 如果当前品种有持仓,则跳过 if(PositionGetSymbol(ti) == name ){ isskip=true; break; } } if(!isskip){ class="type">int cntMyPosO=OrdersTotal(); if(cntMyPosO>class="num">0){ for(class="type">int ti=cntMyPosO-class="num">1; ti>=class="num">0; ti--){ class="type">class="kw">ulong orderTicket=OrderGetTicket(ti); if( OrderGetString(ORDER_SYMBOL) == name ){ isskip=true; break; } } } } class="macro">#else class=class="str">"cmt">// 查看所有持仓清单 class="type">int cntMyPos=OrdersTotal(); for(class="type">int ti=cntMyPos-class="num">1; ti>=class="num">0; ti--){ if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) class="kw">continue; if(OrderSymbol() == name ){ isskip=true; break; } } class="macro">#endif } class="kw">return isskip; }
「Homework 选卡」
为了节省地球上的森林,我们将创建三个选项卡,仅显示以前所选的品种。 我们以 Long , Short 和 Range 来命名这些选卡。 当然,您不必在那里独独添加上行,下行或横盘的品种。 您可以自行决定使用它们。 结果则是,在启动我们的实用程序的图表上多出另一行,包括四个按钮: 所有(All) 按钮和前面描述的三个按钮。 所有(All) 按钮默认已按下,这意味着符合我们过滤器的所有品种列表将显示如下: 因此,目标已经设定。 现在我们必须实现它。 要做到这一点,我们将不得不重写我们实用程序的一小部分。 用于存储选项卡内容的数组 。 首先,我们添加变量来存储选项卡的内容。 之前,我们只有一个选项卡,其内容存储在 arrPanel1 变量中。 我们将为其他选卡添加类似的变量: 此外,我们将创建另一个数组,以便能够在循环中访问选项卡。 该数组将存储指向以前创建的所有四个数组的指针: 我们在 OnInit() 函数中初始化数组: 选卡标题 。由于在循环中执行选卡操控,所以最好将选卡名称存储在那里,并从循环中访问它们。 因此,我们来创建存储选卡名称的数组: 辅助变量 。 另一个变化涉及 panel1val 变量。 我们已将其名称更改为 panelval 。 这是一个纯粹的装饰修正,但应该注意。 cur_panel 参数还添加了包含当前激活选项卡索引。 变量类型是 uchar。 这意味着它可能需要 0 到 255 之间的值,这已经足够了,因为我们只有 4 个选项卡。 默认时,第一个选项卡(在数组中索引为 0)处于激活状态。 因此,我们在 OnInit() 函数中添加字符串,为变量赋值 0。 最后, OnInit() 函数的最终形式: 其他修改 。 没必要列出所有已实现的修改,因为其中许多变化无关紧要。 那么现在让我们继续讨论主要的修改。 对于较小的变更,您可以自行检验它们,即拿新实用程序的源代码与上篇文章附带的源代码进行比较。 主要变化在于有关新选卡显示的修改。 我们决定在循环中操控我们的选项卡。 我们看看是如何做到这一点的。 由于我们有一行选项卡,我们需要以某种方式显示它。 为了实现它,我们创建了一个单独的函数。 其代码如下: 在调用 show_symbols 函数显示品种按钮之前,我们会在 start_symbols 函数中调用该函数。 show_symbols 函数本身也发生了变化,尽管只是略有修改。 现在我们只显示当前激活i选卡上的品种按钮: 用于将品种加入选卡的按钮 。 现在我们需要为选定的选项卡添加品种。 我们将借助新按钮 Add LONG, Add SHORT 和 Add Range 在打开的图表页面上完成此操作。 如果您还记得,在上一篇文章中,我们实现了点击所需品种按钮的能力。 单击品种之后,会打开一个图表,其中在左下角包含用于导航整个品种列表的按钮区。 我们将按钮添加到此区域中。 根据此品种是否在相应的选项卡中,该按钮将其添加到选项卡或从中删除。 使用 createBTNS 函数显示按钮区。 加入向其添加新按钮的循环: 若要启用按下新按钮,请将按钮状态的检查代码添加到 OnTimer() 函数。 一切都与我们在上一篇文章中所做的那样: delToPanel 函数首先从选定的选项卡中删除品种,然后在启动实用程序的图表上更新所有品种按钮,或仅更新标题按钮: addToPanel 函数与我们刚才研究的那个相反。 它会在选项卡中添加一个品种。 除此之外,它还检查品种是否存在于其他选项卡上。 如果品种已存在,则从那处删除: 在实用程序启动之间保存选项卡
◍ 用 OBJ_BUTTON 铺多品种切换面板
在 MT5 图表上做自定义面板,核心是用 OBJ_BUTTON 对象按坐标排队。上面这段逻辑先画选项卡(panels),再按当前激活的选项卡把对应品种数组里的每个符号生成一个按钮(btn),品种名直接写进 OBJPROP_TEXT。 按钮尺寸由 BTN_WIDTH 和 BTN_HEIGHT 控制,字体固定 8 号,颜色 clrBlack,且全部设为不可选中(OBJPROP_SELECTABLE=false),避免误拖。若 cur_panel 等于当前索引,选项卡置为按下态 OBJPROP_STATE=true,视觉上区分激活页。 换行判断很实在:当 btn_left 超过右边界减一个按钮宽,就 btn_line++ 并重置 btn_left=0,实现自动折行。BTN_HEIGHT*btn_line 算纵向偏移,所以第二行按钮 Y 距离正好是单高的整数倍。外汇与贵金属波动剧烈、杠杆高风险,这类面板只解决切换效率,不替你过滤风险。 下面逐行拆关键创建段:ObjectCreate 建按钮→设 X/Y 距离→设宽高→设字号→设颜色→写品种名→禁选中。复制时把 arrPanels 和 exprefix 换成你自己的容器与前缀即可在 MT5 里跑起来验证。
ObjectCreate(class="num">0, exprefix+"btn"+(class="type">class="kw">string) i, OBJ_BUTTON, class="num">0, class="num">0, class="num">0); ObjectSetInteger(class="num">0,exprefix+"btn"+(class="type">class="kw">string) i,OBJPROP_XDISTANCE,btn_left); ObjectSetInteger(class="num">0,exprefix+"btn"+(class="type">class="kw">string) i,OBJPROP_YDISTANCE,BTN_HEIGHT*btn_line); ObjectSetInteger(class="num">0,exprefix+"btn"+(class="type">class="kw">string) i,OBJPROP_XSIZE,BTN_WIDTH); ObjectSetInteger(class="num">0,exprefix+"btn"+(class="type">class="kw">string) i,OBJPROP_YSIZE,BTN_HEIGHT); ObjectSetInteger(class="num">0,exprefix+"btn"+(class="type">class="kw">string) i,OBJPROP_FONTSIZE,class="num">8); ObjectSetInteger(class="num">0,exprefix+"btn"+(class="type">class="kw">string) i,OBJPROP_COLOR,clrBlack); ObjectSetString(class="num">0,exprefix+"btn"+(class="type">class="kw">string) i,OBJPROP_TEXT,arrPanels[cur_panel].At(i)); ObjectSetInteger(class="num">0,exprefix+"btn"+(class="type">class="kw">string) i,OBJPROP_SELECTABLE,false);
面板间迁徙按钮的显隐逻辑
这段逻辑负责在图表左下角动态生成面板切换按钮:遍历 panelNames 数组(倒序从最后一个到索引 1),先判定当前 name 是否已存在于 arrPanels[i] 里。判定用双层循环,外层走面板索引,内层走该面板对象总数 arrPanels[i].Total(),命中即 isyes=true 并 break。 若 isyes 为真,建一个名为 exprefix+"_p_btn_panelfrom"+i 的 OBJ_BUTTON,X 距离固定 110、Y 距离用 tmpHeight 变量、尺寸取 BTN_WIDTH×BTN_HEIGHT、锚定 CORNER_LEFT_LOWER,文字写成 "Del "+panelNames[i],背景涂 clrPink,表示可从此面板移除。 否则建 exprefix+"_p_btn_panelto"+i 的同型按钮,坐标与尺寸参数完全一致,仅名字前缀换成 panelto、背景色未在此片段设(沿用默认或外层赋值)。外汇与贵金属图表物件交互属于高风险操作环境,按钮误触可能导致面板状态错乱,建议在 MT5 策略测试器里先用模拟品种跑一遍再上实盘。 直接把上面片段塞进你的面板管理函数,改 exprefix 和 BTN_WIDTH 两个宏,就能在左下角看到带删除语义的粉色按钮——若没出现,优先查 tmpHeight 是否被上一节正确累加。
ObjectSetInteger(class="num">0,exprefix+"btn"+(class="type">class="kw">string) i,OBJPROP_BGCOLOR,clrPeachPuff); } btn_left+=BTN_WIDTH; } for( class="type">int i=ArraySize(panelNames)-class="num">1; i>class="num">0; i-- ){ isyes=false; if(arrPanels[i].Total()){ for(class="type">int j=class="num">0; j<arrPanels[i].Total(); j++){ if( arrPanels[i].At(j)==name ){ isyes=true; break; } } } if( isyes ){ ObjectCreate(CID, exprefix+"_p_btn_panelfrom"+(class="type">class="kw">string) i, OBJ_BUTTON, class="num">0, class="num">0, class="num">0); ObjectSetInteger(CID,exprefix+"_p_btn_panelfrom"+(class="type">class="kw">string) i,OBJPROP_XDISTANCE,class="num">110); ObjectSetInteger(CID,exprefix+"_p_btn_panelfrom"+(class="type">class="kw">string) i,OBJPROP_YDISTANCE,tmpHeight); ObjectSetInteger(CID,exprefix+"_p_btn_panelfrom"+(class="type">class="kw">string) i,OBJPROP_XSIZE,BTN_WIDTH); ObjectSetInteger(CID,exprefix+"_p_btn_panelfrom"+(class="type">class="kw">string) i,OBJPROP_YSIZE,BTN_HEIGHT); ObjectSetInteger(CID,exprefix+"_p_btn_panelfrom"+(class="type">class="kw">string) i,OBJPROP_CORNER,CORNER_LEFT_LOWER); ObjectSetInteger(CID,exprefix+"_p_btn_panelfrom"+(class="type">class="kw">string) i,OBJPROP_SELECTABLE,false); ObjectSetString(CID,exprefix+"_p_btn_panelfrom"+(class="type">class="kw">string) i,OBJPROP_TEXT,"Del "+panelNames[i]); ObjectSetInteger(CID,exprefix+"_p_btn_panelfrom"+(class="type">class="kw">string) i,OBJPROP_BGCOLOR,clrPink); }else{ ObjectCreate(CID, exprefix+"_p_btn_panelto"+(class="type">class="kw">string) i, OBJ_BUTTON, class="num">0, class="num">0, class="num">0); ObjectSetInteger(CID,exprefix+"_p_btn_panelto"+(class="type">class="kw">string) i,OBJPROP_XDISTANCE,class="num">110); ObjectSetInteger(CID,exprefix+"_p_btn_panelto"+(class="type">class="kw">string) i,OBJPROP_YDISTANCE,tmpHeight); ObjectSetInteger(CID,exprefix+"_p_btn_panelto"+(class="type">class="kw">string) i,OBJPROP_XSIZE,BTN_WIDTH); ObjectSetInteger(CID,exprefix+"_p_btn_panelto"+(class="type">class="kw">string) i,OBJPROP_YSIZE,BTN_HEIGHT); ObjectSetInteger(CID,exprefix+"_p_btn_panelto"+(class="type">class="kw">string) i,OBJPROP_CORNER,CORNER_LEFT_LOWER);