使用 MQL5 和 MQL4 开发的选择与导航工具: 增加自动模式搜索和显示侦测到的交易品种·进阶篇
(2/3)· 手动翻几百张图表找抛物线和跳空?这篇把筛选做成后台自动选项卡
接上篇,我们继续深挖导航工具的自动模式搜索。很多交易者点开自定义选项卡发现空白或按钮消失就以为程序崩了,其实 MQL5 在拉取深层历史时只是沉默地等服务器返回,硬切反而打断进程。
面板按钮的绘制与自动选项卡判定
这段逻辑在 MT5 图表上连续画出两个不可选中的 GUI 按钮:先画一个名为 clear_ 的按钮,文字为 "Clear",字体 8 号,黑字配淡蓝绿底(clrPaleTurquoise),用于清空当前面板;随后 btn_left 累加 BTN_WIDTH,在其右侧再创建 new_ 按钮,文字 "Open All",尺寸与配色同前,用来一键展开全部品种。 按钮坐标由 BTN_WIDTH 与 BTN_HEIGHT 驱动:XDISTANCE 取 btn_left,YDISTANCE 取 BTN_HEIGHT*btn_line,因此多行面板靠 btn_line 递增换行,按钮不会重叠。OBJPROP_SELECTABLE 设 false 可以避免鼠标误选对象导致面板漂移。 当 cur_panel 超过 arrPanels 数组下标上限(ArraySize(arrPanels)-1)时,代码判定进入了“自动挑选选项卡”:用 MqlRates 数组(已 ArraySetAsSeries 置为时间倒序)配合 tmpNumAddon 计算偏移,并把 addonArr、arrTT 重置长度,再从 tmpSymbols 里按 air_only_home 标记筛选。若 tmpNumAddon==0 且开启仅作业区,则嵌套遍历 arrPanels[1..end] 的所有子项。外汇与贵金属品种波动大、跳空频繁,这类自动选项卡在极端行情可能漏选,建议在 MT5 策略测试器用真实符号集跑一遍确认。
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"); btn_left+=BTN_WIDTH; ObjectCreate(class="num">0, exprefix+"new_"+(class="type">class="kw">string) cur_panel, OBJ_BUTTON, class="num">0, class="num">0, class="num">0); ObjectSetInteger(class="num">0,exprefix+"new_"+(class="type">class="kw">string) cur_panel,OBJPROP_XDISTANCE,btn_left); ObjectSetInteger(class="num">0,exprefix+"new_"+(class="type">class="kw">string) cur_panel,OBJPROP_YDISTANCE,BTN_HEIGHT*btn_line); ObjectSetInteger(class="num">0,exprefix+"new_"+(class="type">class="kw">string) cur_panel,OBJPROP_XSIZE,BTN_WIDTH); ObjectSetInteger(class="num">0,exprefix+"new_"+(class="type">class="kw">string) cur_panel,OBJPROP_YSIZE,BTN_HEIGHT); ObjectSetInteger(class="num">0,exprefix+"new_"+(class="type">class="kw">string) cur_panel,OBJPROP_FONTSIZE,class="num">8); ObjectSetInteger(class="num">0,exprefix+"new_"+(class="type">class="kw">string) cur_panel,OBJPROP_COLOR,clrBlack); ObjectSetInteger(class="num">0,exprefix+"new_"+(class="type">class="kw">string) cur_panel,OBJPROP_BGCOLOR,clrPaleTurquoise); ObjectSetInteger(class="num">0,exprefix+"new_"+(class="type">class="kw">string) cur_panel,OBJPROP_SELECTABLE,false); ObjectSetString(class="num">0,exprefix+"new_"+(class="type">class="kw">string) cur_panel,OBJPROP_TEXT,"Open All"); btn_left+=BTN_WIDTH; } class=class="str">"cmt">// 如果当前打开的选项卡索引超过了 homework 选项卡数组元素的元素数 class=class="str">"cmt">// 加上所有交易品种的选项卡 class=class="str">"cmt">// 则这就是自动挑选选项卡 if(cur_panel>(ArraySize(arrPanels)-class="num">1)){ class="type">MqlRates rates[]; ArraySetAsSeries(rates, true); class="type">int tmpNumAddon=cur_panel-ArraySize(arrPanels); addonArr.Resize(class="num">0); arrTT.Resize(class="num">0); class="type">class="kw">string addonName; CArrayString tmpSymbols; if( tmpNumAddon==class="num">0 && air_only_home ){ for( class="type">int j=class="num">1; j<ArraySize(arrPanels); j++ ){ for( class="type">int k=class="num">0; k<arrPanels[j].Total(); k++ ){
◍ 从面板与市场全量里捞符号清单
这段逻辑干的事很直接:先把面板数组 arrPanels 里出现过的品种名去重塞进 tmpSymbols,避免重复添加。内层 for 循环拿 curName 去 tmpSymbols 里比一遍,命中就置 isYes=true 并 break,没命中才 Add,时间复杂度随已收集数量线性增长。 如果走 else 分支,说明不是从面板取符号。当正则拆分 result 数组长度大于 1 时,逐条去掉空格、跳过空串,再拼上 onlySymbolsPrefix 和 onlySymbolsSuffix 前缀后缀写进 tmpSymbols;若 result 只有 0 或 1 项,则直接调 SymbolsTotal(addons_infowatch) 把当前观察窗口里的全部品种名通过 SymbolName(i, addons_infowatch) 捞进来。 switch(tmpNumAddon) 按 0 到 5 分流显示内容:0 水平、1 抛物线、2 跳空、3 为 4 周最值、4 为 365 天最值、5 近似水平,各自留了选项卡渲染代码的坑位。外汇与贵金属品种在此类全量扫描下可能触发数十至上百个对象,实盘加载前建议在 MT5 里先打印 tmpSymbols.Total() 确认规模,高杠杆品种波动剧烈,误加载会带来显示与资源开销风险。
class="type">class="kw">string curName=arrPanels[j].At(k); class="type">bool isYes=false; for( class="type">int r=class="num">0; r<tmpSymbols.Total(); r++ ){ if(tmpSymbols.At(r)==curName){ isYes=true; break; } } if(!isYes){ tmpSymbols.Add(arrPanels[j].At(k)); } }else{ if( ArraySize(result)>class="num">1 ){ for(class="type">int j=class="num">0;j<ArraySize(result);j++){ StringReplace(result[j], " ", ""); if(StringLen(result[j])<class="num">1){ class="kw">continue; } tmpSymbols.Add(onlySymbolsPrefix+result[j]+onlySymbolsSuffix); } }else{ for( class="type">int i=class="num">0; i<SymbolsTotal(addons_infowatch); i++ ){ tmpSymbols.Add(SymbolName(i, addons_infowatch)); } } } class="kw">switch(tmpNumAddon){ case class="num">0: class=class="str">"cmt">// 水平 class=class="str">"cmt">// 选项卡内容显示代码 break; case class="num">1: class=class="str">"cmt">// 抛物线 class=class="str">"cmt">// 选项卡内容显示代码 break; case class="num">2: class=class="str">"cmt">// 跳空 class=class="str">"cmt">// 选项卡内容显示代码 break; case class="num">3: class=class="str">"cmt">//4周最小值/最大值 class=class="str">"cmt">// 选项卡内容显示代码 break; case class="num">4: class=class="str">"cmt">//class="num">365 天最小值/最大值 class=class="str">"cmt">// 选项卡内容显示代码 break; case class="num">5: class=class="str">"cmt">// 近似水平 class=class="str">"cmt">// 选项卡内容显示代码
「用按钮把多品种信号铺到图表上」
这段逻辑负责把数组里的每个交易品种都生成一个图表按钮,按钮文字直接取品种名,方便一眼定位。当横向坐标 btn_left 超出右边界(btn_right - BTN_WIDTH)时,换行计数 btn_line 加一、btn_left 归零,从而避免按钮重叠出屏。 按钮对象用 OBJ_BUTTON 创建,依次写死 XDISTANCE / YDISTANCE 控制位置,XSIZE / YSIZE 控制尺寸,FONTSIZE 设为 8 号字,COLOR 用 clrBlack。若 arrTT 里对应索引大于 0,则把该值设为按钮 TOOLTIP,悬停能看到附加说明。 checkSYMBwithPOS 返回 true 的品种,背景色改为 clrPeachPuff,相当于在几十个按钮里高亮你当前持仓或关注的标的。外汇与贵金属波动剧烈、杠杆风险高,这种高亮仅作提醒,不代表方向判断。 按钮全部绘制完后函数直接退出,不再重复刷新,因此若品种数组变化需重跑整套创建流程。
break; case class="num">6: class=class="str">"cmt">// 最上升/下降 class=class="str">"cmt">// 选项卡内容显示代码 break; case class="num">7: class=class="str">"cmt">// 所有时间最高价/最低价 class=class="str">"cmt">// 选项卡内容显示代码 break; case class="num">8: class=class="str">"cmt">// 最高价=收盘价 class=class="str">"cmt">// 选项卡内容显示代码 break; } class=class="str">"cmt">// 数组中的每个交易品种都在图表中显示一个按钮 class=class="str">"cmt">// 按钮上加上交易品种名称 for( class="type">int i=class="num">0; i<addonArr.Total(); i++ ){ if( btn_left>btn_right-BTN_WIDTH ){ btn_line++; btn_left=class="num">0; } 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,addonArr.At(i)); ObjectSetInteger(class="num">0,exprefix+"btn"+(class="type">class="kw">string) i,OBJPROP_SELECTABLE,false); if( arrTT.At(i)>class="num">0 ){ ObjectSetString(class="num">0,exprefix+"btn"+(class="type">class="kw">string) i,OBJPROP_TOOLTIP,(class="type">class="kw">string) arrTT.At(i)); } if( checkSYMBwithPOS(addonArr.At(i)) ){ ObjectSetInteger(class="num">0,exprefix+"btn"+(class="type">class="kw">string) i,OBJPROP_BGCOLOR,clrPeachPuff); } btn_left+=BTN_WIDTH; } class=class="str">"cmt">// 因为按钮已经显示,退出函数
用按钮把多品种面板铺到图表上
这段逻辑负责把当前选项卡里的交易品种逐个画成图表按钮。循环从 i=0 跑满 arrPanels[cur_panel].Total(),每画一个 btn_left 就右移 BTN_WIDTH,一旦超出 btn_right-BTN_WIDTH 就换行并归零左边距。 换行判定很直白:btn_left>btn_right-BTN_WIDTH 时 btn_line 加一、btn_left 重置为 0。这样多品种再密也能自动折行,不会叠在边框外。 每个按钮用 ObjectCreate 建 OBJ_BUTTON,再依次写死坐标、尺寸与字体:XDISTANCE 取 btn_left,YDISTANCE 取 BTN_HEIGHT*btn_line,XSIZE/YSIZE 用宏定义,FONTSIZE 压到 8 号以免挤变形。TEXT 直接填 arrPanels[cur_panel].At(i) 的交易品种名。 持仓高亮靠后台判断:当 noSYMBwithPOS 为假或 cur_panel>0 时,checkSYMBwithPOS 返回真就把 BGCOLOR 设成 clrPeachPuff(桃色),一眼能分出有单的品种。外汇与贵金属波动剧烈、杠杆风险高,这类按钮仅作盯盘辅助,不预示方向。 下面这段是面板绘制核心,复制进 MT5 脚本即可在图表看到品种按钮阵列:
class="kw">return; } class=class="str">"cmt">// 在图表上显示数组中每个交易品种的按钮 class=class="str">"cmt">// 当前活动选项卡 class=class="str">"cmt">// 按钮上加上交易品种名称 for( class="type">int i=class="num">0; i<arrPanels[cur_panel].Total(); i++ ){ if( btn_left>btn_right-BTN_WIDTH ){ btn_line++; btn_left=class="num">0; } 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); if( !noSYMBwithPOS || cur_panel>class="num">0 ){ if( checkSYMBwithPOS(arrPanels[cur_panel].At(i)) ){ ObjectSetInteger(class="num">0,exprefix+"btn"+(class="type">class="kw">string) i,OBJPROP_BGCOLOR,clrPeachPuff); } } btn_left+=BTN_WIDTH; } } for( class="type">int i=class="num">0; i<tmpSymbols.Total(); i++ ){ addonName=tmpSymbols[i]; class=class="str">"cmt">// 如果交易品种应当显示,定义的代码 }
◍ 盘整区自动扫描的参数与取舍
所谓空中水平,指的是价格在一个横向区间内反复触碰近似相同的顶或底,交易机会就藏在这些平盘范围里。手工翻图表找这种结构极其枯燥,而且假突破频出——不过假突破越多,该水平被市场记住的概率反而越高。 实际扫描多在 M5 上跑,M15、M30 也有人用。停留时间越长的平盘,后续介入质量倾向更好,但搜索越慢,所以最好只锁 1~2 个周期。 核心参数就两个:水平柱数默认 4,柱数越大平盘质量越高、出现频率越低;相对水平偏移默认 3 点,设为 0 要求价格精准重合,在高中周期几乎碰不到。若只在当日或前一日方向交易,可限只看逆着价格运动的平盘——方向由收盘价对开盘价的比例定,前日收高于开就只捞做多侧的近底平盘。 CopyRates 调用后立刻读数组而非等延时,是刻意的选择:等 50 毫秒扫 100 个品种就耗 5 秒,实盘几百股票品种会卡死选项卡。代价是偶尔拿到旧数据,按 R 刷新即可。满足条件的品种名进 addonArr,arrTT 里填了价格就会在图表自动画水平线,省去肉眼对位。
sinput class="type">class="kw">string delimeter_06=""; class=class="str">"cmt">// --- 额外的空中水平选项卡 --- input class="type">bool air_level_m5=true; class=class="str">"cmt">// 在 M5 上寻找空中水平 input class="type">bool air_level_m15=true; class=class="str">"cmt">// 在 M15 上寻找空中水平 input class="type">bool air_level_m30=false; class=class="str">"cmt">// 在 M30 上寻找空中水平 input class="type">bool air_level_h1=false; class=class="str">"cmt">// 在 H1 上寻找空中水平 input class="type">bool air_level_h4=false; class=class="str">"cmt">// 在 H4 上寻找空中水平 input class="type">bool air_level_d1=false; class=class="str">"cmt">// 在 D1 上寻找空中水平 input class="type">uchar air_level_count=class="num">4; class=class="str">"cmt">// Number of level bars(水平的柱数) input class="type">uchar air_level_offset=class="num">3; class=class="str">"cmt">// Offset relative to level in points(相对水平的点数偏移) input class="type">bool air_level_cur_day=true; class=class="str">"cmt">// Only levels in the current day&class="macro">#x27;s direction(只有当天方向的水平) input class="type">bool air_level_prev_day=true; class=class="str">"cmt">// Only levels in the previous day&class="macro">#x27;s direction(只有前一天方向上的水平) input class="type">bool air_only_home=false; class=class="str">"cmt">// Search only in "homework"(只在 "homework" 中搜索) if(air_level_m5 && CopyRates(addonName, PERIOD_M5, class="num">0, air_level_count+class="num">1, rates)==air_level_count+class="num">1){ if( (!air_level_cur_day || getmeinfoday_symbol(addonName, class="num">0)<=class="num">0) && (!air_level_prev_day || getmeinfoday_symbol(addonName, class="num">1)<=class="num">0) && rates[class="num">0].high<rates[class="num">1].high ){ class="type">bool isOk=true; for( class="type">int j=class="num">1; j<air_level_count; j++ ){
「高低点偏移容差内的同列筛选」
这段逻辑干的事很直接:在 M5 周期上,把候选品种的前一根 K 线高点(或低点)与之前若干根同方向极值点做容差比对,容差由 air_level_offset 乘以品种点值决定。
| 若比对所有 j 的 | rates[1].high - rates[j+1].high | 都 ≤ 容差点值,isOk 保持 true,否则立即置 false 跳出近似判定。低点分支同理,只是换成 rates[1].low 与 rates[j+1].low 比,且额外要求当日与昨日的标记函数返回值 ≥0、当前低价高于前一根低价才进入。 |
|---|
通过校验且未被 skip_symbol 屏蔽时,品种名加「 (M5)」塞进 addonArr,对应触发价 rates[1].high 或 low 写进 arrTT。你在 MT5 里把 air_level_offset 从默认 20 调到 5,能明显看到同列候选数变多,但假突破概率可能上升——外汇与贵金属杠杆高,这类扫描结果只作参考。
if( MathAbs(rates[class="num">1].high-rates[j+class="num">1].high) <= air_level_offset*SymbolInfoDouble(addonName, SYMBOL_POINT) ){ }else{ isOk=false; } } if(isOk && !skip_symbol(addonName)){ addonArr.Add(addonName+" (M5)"); arrTT.Add(rates[class="num">1].high); } }else if( (!air_level_cur_day || getmeinfoday_symbol(addonName, class="num">0)>=class="num">0) && (!air_level_prev_day || getmeinfoday_symbol(addonName, class="num">1)>=class="num">0) && rates[class="num">0].low>rates[class="num">1].low ){ class="type">bool isOk=true; for( class="type">int j=class="num">1; j<air_level_count; j++ ){ if( MathAbs(rates[class="num">1].low-rates[j+class="num">1].low) <= air_level_offset*SymbolInfoDouble(addonName, SYMBOL_POINT) ){ }else{ isOk=false; } } if(isOk && !skip_symbol(addonName)){ addonArr.Add(addonName+" (M5)"); arrTT.Add(rates[class="num">1].low); } }