利用 MQL5 和 MQL4 实现的选择和导航实用程序:添加
📘

利用 MQL5 和 MQL4 实现的选择和导航实用程序:添加"homework"选项卡并保存图形对象(基础篇)

第 1/3 篇

「给导航工具加 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 编译一次,能在各自终端正常返回布尔值即验证通过。外汇与贵金属杠杆高,实盘前务必先在模拟盘确认分支逻辑无误。

MQL5 / C++
    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 里跑起来验证。

MQL5 / C++
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 是否被上一节正确累加。

MQL5 / C++
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);

常见问题

在EA里用对象遍历把OBJPROP_TIMEFRAMES等属性读出,序列化到本地文件,重开图表时再重建即可。
用条件编译宏把平台差异函数包起来,公共逻辑放外层,只在编译期切换具体API,避免维护两份。
小布可读取你的图表对象并归档到品种页,换设备登录也能直接调出当天的手绘作业。
给按钮绑可见性状态,切换选卡时按当前品种重算OBJPROP_STATE,只显当前上下文需要的控件。
按对象类型和时间戳过滤,只保留近三日手绘线,批量删旧对象能明显降渲染开销。