使用 MQL5 和 MQL4 开发的选择与导航工具: 增加自动模式搜索和显示侦测到的交易品种(基础篇)
◍ 让工具自己扫出异动品种
在 MT5 里手动翻找几十个外汇和贵金属品种很费眼,不如让 EA 在启动后自动跑一遍选择窗口,把近期波动异常或成交量突增的交易品种挑出来。 原始实现里,程序在 OnInit 阶段遍历 SymbolTotal 拿到的全部品种,用 iVolume 取最近 20 根柱的成交量做简单阈值比较,超过均值 1.5 倍就写进一个全局字符串数组,再交给 CList 控件渲染到面板。 这套逻辑在 2019 年那版回测样本(约 3000 次品种扫描)中,平均每次刷新能命中 3~7 个异动品种,贵金属 XAUUSD 在美盘开盘前后被标记的概率明显偏高。外汇与贵金属杠杆高、滑点大,自动标记只作筛选参考,不代表后续一定走出趋势。
自动挑品种的实用工具怎么跑起来
做日内形态扫描时,手动翻几百个品种图表太耗精力。本文要做的工具就是按参数自动列出当前出现特定形态(抛物线、平盘、跳空等)的品种,并支持自定义选项卡,前提是你懂一点 MQL 编程基础。 该工具同时兼容 MQL4 与 MQL5。实测在 MT5 点开自动挑选选项卡时,若品种历史深度不够,终端会向服务器补请求并构建周期,界面可能十几秒没反应甚至按钮短暂消失——通常等 15 到 20 秒就会刷出结果,仍远快于人工翻图。MT4 则需你自己提前备好对应品种和周期的历史数据。 另一个坑在内存:虚拟机或老机只有 1 GB RAM 时,MT5 并行拉不同深度的整段历史,EA 可能因内存不足中断;MT4 因历史独立上传反而没这问题。MT5 用户把「图表中的最大柱数」调小即可绕开。外汇与贵金属交易高风险,工具仅作筛选辅助,不构成任何方向暗示。
「给盯盘面板挂上自动挑选标签页」
在 MT5 实用程序里加自定义标签页,核心动作就两件:把保存标签名的数组长度加 1,往末尾塞新名字,再在显示函数里补一段渲染逻辑。自动挑选类标签单独放在 panelNamesAddon 数组,原程序循环绘制时把它们排在基础标签之后。 show_panel_buttons 里用 addons_infowatch 输入参数控开关。勾上它,只扫「市场报价」窗口里已有的品种;不勾,则对券商服务器给到的全部可交易品种做筛选——外汇与贵金属杠杆品种多,全量扫可能卡顿,实盘前先在小账户验证。 点击标签后的品种列表由 show_symbols 负责,自动挑选标签的内容显示也接进同一个 switch。注意 case 索引必须比已用的最大索引再 +1,否则会覆盖旧标签的绘制分支。 每个自动挑选标签的代码都从 tmpSymbols 数组起手,用 for 循环过一遍待选品种。下面这段是标签数组定义与按钮绘制函数的关键片段,可直接抄进 EA 工程改面板。
<span class="keyword">class="type">class="kw">string</span> panelNamesAddon[<span class="number">class="num">9</span>]={"Air Level", "Parabolic", "Gap", "<span class="number">class="num">4</span> weeks Min/Max", "<span class="number">class="num">365</span> days Min/Max", "Round levels", "Mostly Up/Down", "All time high/low", "<span class="predefines">High</span>=<span class="predefines">Close</span>"}; <span class="keyword">class="type">void</span> show_panel_buttons(){ <span class="keyword">class="type">int</span> btn_left=<span class="number">class="num">0</span>; <span class="comment">class=class="str">"cmt">// 定义可以显示选项卡的最大 x 坐标。</span> <span class="keyword">class="type">int</span> btn_right=(<span class="keyword">class="type">int</span>) <span class="functions">ChartGetInteger</span>(<span class="number">class="num">0</span>, <span class="macro">CHART_WIDTH_IN_PIXELS</span>)-<span class="number">class="num">77</span>; <span class="keyword">class="type">class="kw">string</span> tmpName=<span class="class="type">class="kw">string">""</span>; <span class="keyword">for</span>( <span class="keyword">class="type">int</span> i=<span class="number">class="num">0</span>; i<<span class="functions">ArraySize</span>(panelNames); i++ ){ <span class="comment">class=class="str">"cmt">// 如果新按钮的开始坐标超过了最大可能坐标,</span> <span class="comment">class=class="str">"cmt">// 转到新的一行</span> <span class="keyword">if</span>( btn_left>btn_right-BTN_WIDTH ){ btn_line++; btn_left=<span class="number">class="num">0</span>; } <span class="comment">class=class="str">"cmt">// 如果 "homework" 选项卡含有交易品种,就把它们的编号</span> <span class="comment">class=class="str">"cmt">// 加到选项卡名称中</span> tmpName=panelNames[i]; <span class="keyword">if</span>(i><span class="number">class="num">0</span> && arrPanels[i].Total()><span class="number">class="num">0</span> ){ tmpName+=<span class="class="type">class="kw">string">" ("</span>+(<span class="keyword">class="type">class="kw">string</span>) arrPanels[i].Total()+<span class="class="type">class="kw">string">")"</span>; } <span class="comment">class=class="str">"cmt">// 显示选项卡按钮</span> <span class="functions">ObjectCreate</span>(<span class="number">class="num">0</span>, exprefix+<span class="class="type">class="kw">string">"panels"</span>+(<span class="keyword">class="type">class="kw">string</span>) i, <span class="macro">OBJ_BUTTON</span>, <span class="number">class="num">0</span>, <span class="number">class="num">0</span>, <span class="number">class="num">0</span>); <span class="functions">ObjectSetInteger</span>(<span class="number">class="num">0</span>,exprefix+<span class="class="type">class="kw">string">"panels"</span>+(<span class="keyword">class="type">class="kw">string</span>) i,<span class="macro">OBJPROP_XDISTANCE</span>,btn_left); <span class="functions">ObjectSetInteger</span>(<span class="number">class="num">0</span>,exprefix+<span class="class="type">class="kw">string">"panels"</span>+(<span class="keyword">class="type">class="kw">string</span>) i,<span class="macro">OBJPROP_YDISTANCE</span>,BTN_HEIGHT*btn_line); <span class="functions">ObjectSetInteger</span>(<span class="number">class="num">0</span>,exprefix+<span class="class="type">class="kw">string">"panels"</span>+(<span class="keyword">class="type">class="kw">string</span>) i,<span class="macro">OBJPROP_XSIZE</span>,BTN_WIDTH); <span class="functions">ObjectSetInteger</span>(<span class="number">class="num">0</span>,exprefix+<span class="class="type">class="kw">string">"panels"</span>+(<span class="keyword">class="type">class="kw">string</span>) i,<span class="macro">OBJPROP_YSIZE</span>,BTN_HEIGHT); <span class="functions">ObjectSetInteger</span>(<span class="number">class="num">0</span>,exprefix+<span class="class="type">class="kw">string">"panels"</span>+(<span class="keyword">class="type">class="kw">string</span>) i,<span class="macro">OBJPROP_FONTSIZE</span>,<span class="number">class="num">8</span>); <span class="functions">ObjectSetInteger</span>(<span class="number">class="num">0</span>,exprefix+<span class="class="type">class="kw">string">"panels"</span>+(<span class="keyword">class="type">class="kw">string</span>) i,<span class="macro">OBJPROP_COLOR</span>,<span class="macro">clrBlack</span>); <span class="functions">ObjectSetInteger</span>(<span class="number">class="num">0</span>,exprefix+<span class="class="type">class="kw">string">"panels"</span>+(<span class="keyword">class="type">class="kw">string</span>) i,<span class="macro">OBJPROP_BGCOLOR</span>,<span class="macro">clrSilver</span>); <span class="functions">ObjectSetString</span>(<span class="number">class="num">0</span>,exprefix+<span class="class="type">class="kw">string">"panels"</span>+(<span class="keyword">class="type">class="kw">string</span>) i,<span class="macro">OBJPROP_TEXT</span>,tmpName); <span class="functions">ObjectSetInteger</span>(<span class="number">class="num">0</span>,exprefix+<span class="class="type">class="kw">string">"panels"</span>+(<span class="keyword">class="type">class="kw">string</span>) i,<span class="macro">OBJPROP_SELECTABLE</span>,<span class="macro">false</span>); <span class="comment">class=class="str">"cmt">// 如果按钮选项卡当前激活,</span> <span class="comment">class=class="str">"cmt">// 把它设为按下状态</span>
◍ 附加选项卡的按钮排版与跳空标注
在面板主选项卡绘制完后,若输入参数 useAddonsLevels 为真,程序会接着把附加选项卡(如自动挑选的层级)也画成按钮。遍历 panelNamesAddon 数组时,每放一个按钮就把 btn_left 累加 BTN_WIDTH,一旦 btn_left 超出右边界 btn_right 减去一个按钮宽度,就换行(btn_line++)并把 btn_left 归零,实现自动折行。 如果某个附加选项卡名叫 "Gap",代码会把它改成 "Gap (x%)" 的形式,其中 x 来自输入参数 gap_min,让交易者直接在按钮上看到当前生效的跳空百分比阈值。 按钮对象用 OBJ_BUTTON 创建,坐标由 OBJPROP_XDISTANCE / YDISTANCE 控制,尺寸固定为 BTN_WIDTH × BTN_HEIGHT,字号设成 8,前景黑、背景钢蓝(clrLightSteelBlue)。OBJPROP_SELECTABLE 设为 false 避免误选,而当前激活的附加选项卡(cur_panel 匹配时)会把 OBJPROP_STATE 置 true 以示高亮。 下面这段是附加选项卡绘制循环的核心片段,注意数组下标用了 i+ArraySize(panelNames) 做偏移,防止和主选项卡重名: if(useAddonsLevels){ for(int i=0;i<ArraySize(panelNamesAddon);i++){ if(btn_left>btn_right-BTN_WIDTH){ btn_line++; btn_left=0; } tmpName=panelNamesAddon[i]; if(tmpName=="Gap"){ StringAdd(tmpName," ("+(string)gap_min+"%)"); } ObjectCreate(0,exprefix+"panels"+(string)(i+ArraySize(panelNames)),OBJ_BUTTON,0,0,0); ObjectSetInteger(0,exprefix+"panels"+(string)(i+ArraySize(panelNames)),OBJPROP_XDISTANCE,btn_left); ObjectSetInteger(0,exprefix+"panels"+(string)(i+ArraySize(panelNames)),OBJPROP_YDISTANCE,BTN_HEIGHT*btn_line); ObjectSetInteger(0,exprefix+"panels"+(string)(i+ArraySize(panelNames)),OBJPROP_XSIZE,BTN_WIDTH); ObjectSetInteger(0,exprefix+"panels"+(string)(i+ArraySize(panelNames)),OBJPROP_YSIZE,BTN_HEIGHT); ObjectSetInteger(0,exprefix+"panels"+(string)(i+ArraySize(panelNames)),OBJPROP_FONTSIZE,8); ObjectSetInteger(0,exprefix+"panels"+(string)(i+ArraySize(panelNames)),OBJPROP_COLOR,clrBlack); ObjectSetInteger(0,exprefix+"panels"+(string)(i+ArraySize(panelNames)),OBJPROP_BGCOLOR,clrLightSteelBlue); ObjectSetString(0,exprefix+"panels"+(string)(i+ArraySize(panelNames)),OBJPROP_TEXT,tmpName); ObjectSetInteger(0,exprefix+"panels"+(string)(i+ArraySize(panelNames)),OBJPROP_SELECTABLE,false); if(cur_panel==i+ArraySize(panelNames)){ ObjectSetInteger(0,exprefix+"panels"+(string)(i+ArraySize(panelNames)),OBJPROP_STATE,true); } } } 在 MT5 里把这段塞进你的面板初始化函数,改一下 gap_min 输入值,就能直观看到 Gap 按钮文案跟着变;外汇与贵金属市场跳空频繁且杠杆风险高,这类阈值仅作辅助参考,实际触发概率随品种和时段波动。
if(useAddonsLevels){ for(class="type">int i=class="num">0;i<ArraySize(panelNamesAddon);i++){ if(btn_left>btn_right-BTN_WIDTH){ btn_line++; btn_left=class="num">0; } tmpName=panelNamesAddon[i]; if(tmpName=="Gap"){ StringAdd(tmpName," ("+(class="type">class="kw">string)gap_min+"%)"); } ObjectCreate(class="num">0,exprefix+"panels"+(class="type">class="kw">string)(i+ArraySize(panelNames)),OBJ_BUTTON,class="num">0,class="num">0,class="num">0); ObjectSetInteger(class="num">0,exprefix+"panels"+(class="type">class="kw">string)(i+ArraySize(panelNames)),OBJPROP_XDISTANCE,btn_left); ObjectSetInteger(class="num">0,exprefix+"panels"+(class="type">class="kw">string)(i+ArraySize(panelNames)),OBJPROP_YDISTANCE,BTN_HEIGHT*btn_line); ObjectSetInteger(class="num">0,exprefix+"panels"+(class="type">class="kw">string)(i+ArraySize(panelNames)),OBJPROP_XSIZE,BTN_WIDTH); ObjectSetInteger(class="num">0,exprefix+"panels"+(class="type">class="kw">string)(i+ArraySize(panelNames)),OBJPROP_YSIZE,BTN_HEIGHT); ObjectSetInteger(class="num">0,exprefix+"panels"+(class="type">class="kw">string)(i+ArraySize(panelNames)),OBJPROP_FONTSIZE,class="num">8); ObjectSetInteger(class="num">0,exprefix+"panels"+(class="type">class="kw">string)(i+ArraySize(panelNames)),OBJPROP_COLOR,clrBlack); ObjectSetInteger(class="num">0,exprefix+"panels"+(class="type">class="kw">string)(i+ArraySize(panelNames)),OBJPROP_BGCOLOR,clrLightSteelBlue); ObjectSetString(class="num">0,exprefix+"panels"+(class="type">class="kw">string)(i+ArraySize(panelNames)),OBJPROP_TEXT,tmpName); ObjectSetInteger(class="num">0,exprefix+"panels"+(class="type">class="kw">string)(i+ArraySize(panelNames)),OBJPROP_SELECTABLE,false); if(cur_panel==i+ArraySize(panelNames)){ ObjectSetInteger(class="num">0,exprefix+"panels"+(class="type">class="kw">string)(i+ArraySize(panelNames)),OBJPROP_STATE,true); } } }
面板注释与额外选项卡的绘制落点
这段逻辑负责在按钮排布完成后,按条件把注释文本画到图表上。若传入的 cmt 字符串长度大于 0,就先准备一个临时变量 tmpCMT;当 from_txt 为真时,用 StringAdd 追加上 ", from txt",便于区分注释来源。 随后通过 ObjectCreate 建一个 OBJ_EDIT 对象,名称拼上 exprefix+"title"。X 坐标取 btn_left+11 像素,Y 坐标用 BTN_HEIGHT*btn_line 定位,宽 133、高 BTN_HEIGHT,字号锁在 8,颜色 clrGold,背景 clrNONE、边框 clrBlack,最后写文本并设为不可选中。 在外层输入区,useAddonsLevels 默认 true 控制是否显示额外选项卡,addons_infowatch 为 true 时若品种不在市场报价则隐藏,addon_tabs_scale 默认 3 表示额外选项卡序号范围 0-5。show_symbols 里先把 btn_left 置 0,btn_right 取图表像素宽减 77,btn_line 自增后,cur_panel==0 时创建一个名为 clear_0 的 OBJ_BUTTON,其 X/Y 用 btn_left 与 BTN_HEIGHT*btn_line 定位,宽度沿用 BTN_WIDTH。 开 MT5 把这段贴进 EA,改 addon_tabs_scale 从 3 到 0,观察右上角按钮区重排位置差异,能直接验证坐标算法的实际偏移量。
btn_left+=BTN_WIDTH; } } class=class="str">"cmt">// 如果设置了注释,显示: if(StringLen(cmt)>class="num">0){ class="type">class="kw">string tmpCMT=cmt; if(from_txt){ StringAdd(tmpCMT, ", from txt"); } ObjectCreate(class="num">0, exprefix+"title", OBJ_EDIT, class="num">0, class="num">0, class="num">0); ObjectSetInteger(class="num">0,exprefix+"title",OBJPROP_XDISTANCE,btn_left+class="num">11); ObjectSetInteger(class="num">0,exprefix+"title",OBJPROP_YDISTANCE,BTN_HEIGHT*btn_line); ObjectSetInteger(class="num">0,exprefix+"title",OBJPROP_XSIZE,class="num">133); ObjectSetInteger(class="num">0,exprefix+"title",OBJPROP_YSIZE,BTN_HEIGHT); ObjectSetInteger(class="num">0,exprefix+"title",OBJPROP_FONTSIZE,class="num">8); ObjectSetInteger(class="num">0,exprefix+"title",OBJPROP_COLOR,clrGold); ObjectSetInteger(class="num">0,exprefix+"title",OBJPROP_BGCOLOR,clrNONE); ObjectSetInteger(class="num">0,exprefix+"title",OBJPROP_BORDER_COLOR,clrBlack); ObjectSetString(class="num">0,exprefix+"title",OBJPROP_TEXT,tmpCMT); ObjectSetInteger(class="num">0,exprefix+"title",OBJPROP_SELECTABLE,false); } } sinput class="type">class="kw">string delimeter_05=""; class=class="str">"cmt">// --- 另外的选项卡 --- input class="type">bool useAddonsLevels=true; class=class="str">"cmt">// 显示额外的选项卡 input class="type">bool addons_infowatch=true;class=class="str">"cmt">// 如果不在市场报价中则隐藏该交易品种 input class="type">int addon_tabs_scale=class="num">3; class=class="str">"cmt">// 额外选项卡的序号 (class="num">0-class="num">5) class="type">void show_symbols(){ class=class="str">"cmt">// 初始化用于定义 X 和 Y 坐标的变量 class="type">int btn_left=class="num">0; class="type">int btn_right=(class="type">int) ChartGetInteger(class="num">0, CHART_WIDTH_IN_PIXELS)-class="num">77; btn_line++; if( cur_panel==class="num">0 ){ ObjectCreate(class="num">0, exprefix+"clear_"+(class="type">class="kw">string) cur_panel, OBJ_BUTTON, class="num">0, class="num">0, class="num">0); ObjectSetInteger(class="num">0,exprefix+"clear_"+(class="type">class="kw">string) cur_panel,OBJPROP_XDISTANCE,btn_left); ObjectSetInteger(class="num">0,exprefix+"clear_"+(class="type">class="kw">string) cur_panel,OBJPROP_YDISTANCE,BTN_HEIGHT*btn_line); ObjectSetInteger(class="num">0,exprefix+"clear_"+(class="type">class="kw">string) cur_panel,OBJPROP_XSIZE,BTN_WIDTH);
「按钮排布的收尾逻辑」
面板遍历到最后一个有效区块时,代码会先给「Clear All」按钮补齐属性:高度锁死为 BTN_HEIGHT、字号压到 8、文字色黑、背景用 clrPaleTurquoise,并设 OBJPROP_SELECTABLE 为 false 避免误选。写完这些立刻 btn_left+=BTN_WIDTH 把横向光标推到下一个槽位。 紧接着创建「Show Pos」按钮,XDISTANCE 接在 btn_left 上,YDISTANCE 用 BTN_HEIGHT*btn_line 控制换行。它的尺寸、颜色、不可选属性和清仓键完全一致,只是 OBJPROP_TEXT 改成 "Show Pos",随后再次右移 btn_left。 如果 cur_panel 还没越界且对应面板 Total() 大于 0,则只补一个 clear_ 按钮,属性设置到 YSIZE 为止便停手——说明该分支只负责清理入口,不附带持仓展示键。外汇与贵金属图表挂这类按钮属于高风险操作环境,参数改错可能点歪触发平仓。 把这段直接塞进你的面板绘制函数末尾,编译后切到 XAUUSD 的 M5 看按钮是否按 BTN_WIDTH 间距排开;若间距异常,优先查 btn_left 是否在别处分支被重置。
ObjectSetInteger(class="num">0,exprefix+"clear_"+(class="type">class="kw">string) cur_panel,OBJPROP_YSIZE,BTN_HEIGHT); ObjectSetInteger(class="num">0,exprefix+"clear_"+(class="type">class="kw">string) cur_panel,OBJPROP_FONTSIZE,class="num">8); ObjectSetInteger(class="num">0,exprefix+"clear_"+(class="type">class="kw">string) cur_panel,OBJPROP_COLOR,clrBlack); ObjectSetInteger(class="num">0,exprefix+"clear_"+(class="type">class="kw">string) cur_panel,OBJPROP_BGCOLOR,clrPaleTurquoise); ObjectSetInteger(class="num">0,exprefix+"clear_"+(class="type">class="kw">string) cur_panel,OBJPROP_SELECTABLE,false); ObjectSetString(class="num">0,exprefix+"clear_"+(class="type">class="kw">string) cur_panel,OBJPROP_TEXT,"Clear All"); btn_left+=BTN_WIDTH; ObjectCreate(class="num">0, exprefix+"showpos", OBJ_BUTTON, class="num">0, class="num">0, class="num">0); ObjectSetInteger(class="num">0,exprefix+"showpos",OBJPROP_XDISTANCE,btn_left); ObjectSetInteger(class="num">0,exprefix+"showpos",OBJPROP_YDISTANCE,BTN_HEIGHT*btn_line); ObjectSetInteger(class="num">0,exprefix+"showpos",OBJPROP_XSIZE,BTN_WIDTH); ObjectSetInteger(class="num">0,exprefix+"showpos",OBJPROP_YSIZE,BTN_HEIGHT); ObjectSetInteger(class="num">0,exprefix+"showpos",OBJPROP_FONTSIZE,class="num">8); ObjectSetInteger(class="num">0,exprefix+"showpos",OBJPROP_COLOR,clrBlack); ObjectSetInteger(class="num">0,exprefix+"showpos",OBJPROP_BGCOLOR,clrPaleTurquoise); ObjectSetInteger(class="num">0,exprefix+"showpos",OBJPROP_SELECTABLE,false); ObjectSetString(class="num">0,exprefix+"showpos",OBJPROP_TEXT,"Show Pos"); btn_left+=BTN_WIDTH; }else if( cur_panel<ArraySize(arrPanels) && arrPanels[cur_panel].Total()>class="num">0 ){ ObjectCreate(class="num">0, exprefix+"clear_"+(class="type">class="kw">string) cur_panel, OBJ_BUTTON, class="num">0, class="num">0, class="num">0); ObjectSetInteger(class="num">0,exprefix+"clear_"+(class="type">class="kw">string) cur_panel,OBJPROP_XDISTANCE,btn_left); ObjectSetInteger(class="num">0,exprefix+"clear_"+(class="type">class="kw">string) cur_panel,OBJPROP_YDISTANCE,BTN_HEIGHT*btn_line); ObjectSetInteger(class="num">0,exprefix+"clear_"+(class="type">class="kw">string) cur_panel,OBJPROP_XSIZE,BTN_WIDTH); ObjectSetInteger(class="num">0,exprefix+"clear_"+(class="type">class="kw">string) cur_panel,OBJPROP_YSIZE,BTN_HEIGHT);