监视多币种的交易信号(第三部分):引入搜索算法·综合运用
(3/3)·手工翻品种、漏信号的老办法在 7 个步骤里被一套可保存模板加检索逻辑彻底替掉
「信号面板的图标与坐标映射实现」
在 MT5 的 EA 面板开发里,给按钮挂箭头图标和提示文字,本质是往 CButton 派生类写资源路径。下面这段把上行/下行箭头 bmp 放进数组,按 number 参数决定用哪个,负数则清空图标并改为居中文字。 SetIcon 里用三元表达式选路径:number>=0 取 image[number],否则置空串。随后 IsCenterText 切换文字居中状态,IconFile 与 IconFilePressed 同路径保证按下不变样,最后 Update(true) 重绘。 行号列号靠索引算:GetRow 用 MathFloor(index/row_size)+1,GetCol 用 MathMod(index,row_size)+1。比如 row_size=5、index=12,行是第 3 行、列是第 3 列。 时间框架字符串转枚举别写一堆 if 也行,但原文用的就是逐条比对:"M1" 返 PERIOD_M1,一直到 "M4" 返 PERIOD_M4,后续可照此补 M5/M15 等。复制下面代码到你的 CProgram 类即可编译验证。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Set the icon | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CProgram::SetIcon(class="type">int index,class="type">int number) { class=class="str">"cmt">//--- class="type">class="kw">string image[]= { "Images\\EasyAndFastGUI\\Icons\\bmp16\\arrow_up.bmp", "Images\\EasyAndFastGUI\\Icons\\bmp16\\arrow_down.bmp" }; class="type">class="kw">string path=(number>=class="num">0)?image[number]:""; if(number<class="num">0) m_signal_button[index].IsCenterText(true); else m_signal_button[index].IsCenterText(false); m_signal_button[index].IconFile(path); m_signal_button[index].IconFilePressed(path); m_signal_button[index].Update(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Set the tooltip | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CProgram::SetTooltip(class="type">int index,class="type">class="kw">string text="\n") { m_signal_button[index].Tooltip(text); m_signal_button[index].ShowTooltip(true); } class="type">int GetRow(class="type">int index,class="type">int row_size); class="type">int GetCol(class="type">int index,class="type">int row_size); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Determining a row by the index | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CProgram::GetRow(class="type">int index,class="type">int row_size) { class="kw">return(class="type">int(MathFloor(index/row_size)+class="num">1)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Determining a column by the index | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CProgram::GetCol(class="type">int index,class="type">int row_size) { class="kw">return(class="type">int(MathMod(index,row_size)+class="num">1)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return timeframe by row | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_TIMEFRAMES CProgram::StringToTimeframe(class="kw">const class="type">class="kw">string timeframe) { if(timeframe=="M1") class="kw">return(PERIOD_M1); if(timeframe=="M2") class="kw">return(PERIOD_M2); if(timeframe=="M3") class="kw">return(PERIOD_M3); if(timeframe=="M4") class="kw">return(PERIOD_M4);
字符串周期到枚举的映射与表格定位
在 MT5 面板类工具里,用户看到的周期标签往往是 "M5""H4" 这类字符串,而底层调用 iMACD、iRSI 时必须喂 ENUM_TIMEFRAMES 枚举。下面这段映射把 21 种非常规周期(含 M6/M10/M12/M20/H2/H3/H6/H8/H12 等非标准档)逐一转成对应宏,最后兜底返回当前图表 Period(),避免空字符串直接崩句柄。 if(timeframe=="M5") return(PERIOD_M5); // 字符串M5命中即返回5分钟枚举 if(timeframe=="M6") return(PERIOD_M6); // M6非常规周期,MT5原生支持 if(timeframe=="M10") return(PERIOD_M10); // 10分钟线常用于剥头皮统计 if(timeframe=="M12") return(PERIOD_M12); if(timeframe=="M15") return(PERIOD_M15); if(timeframe=="M20") return(PERIOD_M20); if(timeframe=="M30") return(PERIOD_M30); if(timeframe=="H1") return(PERIOD_H1); if(timeframe=="H2") return(PERIOD_H2); // 2小时线在贵金属隔夜波动里有用 if(timeframe=="H3") return(PERIOD_H3); if(timeframe=="H4") return(PERIOD_H4); if(timeframe=="H6") return(PERIOD_H6); if(timeframe=="H8") return(PERIOD_H8); if(timeframe=="H12") return(PERIOD_H12); if(timeframe=="D1") return(PERIOD_D1); if(timeframe=="W1") return(PERIOD_W1); if(timeframe=="MN") return(PERIOD_MN1); //--- The default value return(::Period()); // 未匹配时退回图表自身周期,保证不中断 表格坐标反查靠两个函数:GetTimeframe 用 index 和 m_timeframes 大小算列,再取标签文本走上面的映射;GetSymbol 同理算行拿品种名。这样在 21 周期 × N 品种的矩阵面板里,点哪个格子都能还原出 (symbol, timeframe) 对。 GetSignal 接收还原后的 sy 与 tf,准备开指标句柄。外汇与贵金属杠杆高,周期映射错乱会直接让信号取样错位,实盘前务必在策略测试器用 EURUSD 的 H3 跑一遍确认返回的是 PERIOD_H3 而非默认周期。
if(timeframe=="M5") class="kw">return(PERIOD_M5); if(timeframe=="M6") class="kw">return(PERIOD_M6); if(timeframe=="M10") class="kw">return(PERIOD_M10); if(timeframe=="M12") class="kw">return(PERIOD_M12); if(timeframe=="M15") class="kw">return(PERIOD_M15); if(timeframe=="M20") class="kw">return(PERIOD_M20); if(timeframe=="M30") class="kw">return(PERIOD_M30); if(timeframe=="H1") class="kw">return(PERIOD_H1); if(timeframe=="H2") class="kw">return(PERIOD_H2); if(timeframe=="H3") class="kw">return(PERIOD_H3); if(timeframe=="H4") class="kw">return(PERIOD_H4); if(timeframe=="H6") class="kw">return(PERIOD_H6); if(timeframe=="H8") class="kw">return(PERIOD_H8); if(timeframe=="H12") class="kw">return(PERIOD_H12); if(timeframe=="D1") class="kw">return(PERIOD_D1); if(timeframe=="W1") class="kw">return(PERIOD_W1); if(timeframe=="MN") class="kw">return(PERIOD_MN1); class=class="str">"cmt">//--- The class="kw">default value class="kw">return(::Period()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Determine the timeframe | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_TIMEFRAMES CProgram::GetTimeframe(class="type">int index) { class="type">int tf=ArraySize(m_timeframes); class="kw">return(StringToTimeframe((m_timeframe_label[GetCol(index,tf)-class="num">1].LabelText()))); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Determine the symbol | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CProgram::GetSymbol(class="type">int index) { class="type">int tf=ArraySize(m_timeframes); class="kw">return(m_symbol_label[GetRow(index,tf)-class="num">1].LabelText()); } class="type">bool GetSignal(class="type">class="kw">string sy,ENUM_TIMEFRAMES tf,SIGNAL &signal_set); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CProgram::GetSignal(class="type">class="kw">string sy,ENUM_TIMEFRAMES tf,SIGNAL &signal_set) { class=class="str">"cmt">//--- Getting the indicator handle class="type">int h=INVALID_HANDLE;
◍ 把信号配置映射成具体指标句柄
多品种扫描器要兼顾灵活与性能,常见做法是把「用哪个价格、用哪个指标」塞进一个 signal_set 结构,再在运行时翻译成 MT5 原生句柄。下面这段就是典型的映射层:先按 app_price 的 0~6 整数把收盘价、开盘价、最高价、最低价、中位数、典型价、加权价对应到 ENUM_APPLIED_PRICE,缺省落回 PRICE_CLOSE。 紧接着按 ind_type 的 0~6 去拿指标句柄:0 是 iATR、1 是 iCCI、2 是 iDeMarker、3 是 iForce(写死 MODE_SMA+VOLUME_TICK)、4 是 iWPR、5 是 iRSI、6 是 iMomentum;其中 CCI/RSI/Momentum 会吃前面解析出的 app_price,ATR/DeMarker/WPR 不依赖价格类型。 拿到句柄后立刻判 INVALID_HANDLE,失败就 Print 出品种名和 GetLastError() 并 return false,避免在后续 CopyBuffer 上踩坑。 真正取数只用 CopyBuffer(h,0,0,1,arr) 拉最新一根的缓冲区,若返回值不等于 1 同样报错退出;确认无误后调 IndicatorRelease(h) 释放句柄,再把 arr[0] 与 rule_value 比较前暂存到 m_ind_value。外汇与贵金属波动剧烈,这类动态建柄代码在跨周期扫描时可能频繁申请释放,建议在 MT5 策略测试器里用 EURUSD 的 M15 跑一遍,观察 Print 日志里句柄申请失败的频率。
ENUM_APPLIED_PRICE app_price; class="kw">switch(signal_set.app_price) { case class="num">0: app_price=PRICE_CLOSE; class="kw">break; case class="num">1: app_price=PRICE_OPEN; class="kw">break; case class="num">2: app_price=PRICE_HIGH; class="kw">break; case class="num">3: app_price=PRICE_LOW; class="kw">break; case class="num">4: app_price=PRICE_MEDIAN; class="kw">break; case class="num">5: app_price=PRICE_TYPICAL; class="kw">break; case class="num">6: app_price=PRICE_WEIGHTED; class="kw">break; class="kw">default: app_price=PRICE_CLOSE; class="kw">break; } class=class="str">"cmt">//--- class="kw">switch(signal_set.ind_type) { case class="num">0: h=iATR(sy,tf,signal_set.ind_period); class="kw">break; case class="num">1: h=iCCI(sy,tf,signal_set.ind_period,app_price); class="kw">break; case class="num">2: h=iDeMarker(sy,tf,signal_set.ind_period); class="kw">break; case class="num">3: h=iForce(sy,tf,signal_set.ind_period,MODE_SMA,VOLUME_TICK); class="kw">break; case class="num">4: h=iWPR(sy,tf,signal_set.ind_period); class="kw">break; case class="num">5: h=iRSI(sy,tf,signal_set.ind_period,app_price); class="kw">break; case class="num">6: h=iMomentum(sy,tf,signal_set.ind_period,app_price); class="kw">break; class="kw">default: class="kw">break; } if(h==INVALID_HANDLE) { Print(sy+". Failed to get handle"); Print("Handle = ",h," error = ",GetLastError()); class="kw">return(false); } class=class="str">"cmt">//--- class="type">class="kw">double arr[class="num">1]; if(CopyBuffer(h,class="num">0, class="num">0,class="num">1,arr)!=class="num">1) { Print("sy= ",sy,"tf= ",EnumToString(tf)," Failed to get handle data ",GetLastError()); class="kw">return(false); } IndicatorRelease(h); class=class="str">"cmt">//--- Check the condition class="type">class="kw">double r_value=signal_set.rule_value; class="type">class="kw">double c_value=arr[class="num">0]; m_ind_value=c_value;
「信号判定与时间框架过滤的实现细节」
在自定义信号引擎里,rule_type 用 0~4 五个整数编码了五类比较关系:大于、大于等于、等于、小于、小于等于。下面这段 switch 把当前值 c_value 和参考值 r_value 按规则比对,命中就给 s 赋 1,否则走 default 归零。 int s=0; switch(signal_set.rule_type) { case 0: if(c_value>r_value) s=1; break; case 1: if(c_value>=r_value) s=1; break; case 2: if(c_value==r_value) s=1; break; case 3: if(c_value<r_value) s=1; break; case 4: if(c_value<=r_value) s=1; break; default: s=0; break; } //--- if(s>0) return(true); return(false); 逐行看:s 初始化为 0;case 0 处理严格大于,case 1 放宽到含等号,case 2 只认相等,case 3 与 4 是小于方向的两种精度。任何未定义 rule_type 都落 default 返回 false,避免脏数据触发信号。 时间框架的白名单检查由 CheckTimeframe 完成,它在一个长度 21 的数组里挨个比对 tf_name,命中传入的 ENUM_TIMEFRAMES 才放行。实测若信号文件里配了非常规周期(如自设的 2 分钟图),而数组未覆盖,该函数会直接返回 false 导致信号沉默。 SearchSignals 先扫 Signal Monitor 目录下 signal_0.bin 到 signal_4.bin 共 5 个可能文件,用 FileIsExist 统计实际存在数量 cnt,再 ArrayResize 动态开数组。外汇与贵金属市场跳空频繁,这类基于本地 bin 文件的信号加载对文件写入时机敏感,实盘使用前建议在 MT5 策略测试器里跑一轮验证文件落盘逻辑。
class="type">int s=class="num">0; class="kw">switch(signal_set.rule_type) { case class="num">0: if(c_value>r_value) s=class="num">1; class="kw">break; case class="num">1: if(c_value>=r_value) s=class="num">1; class="kw">break; case class="num">2: if(c_value==r_value) s=class="num">1; class="kw">break; case class="num">3: if(c_value<r_value) s=class="num">1; class="kw">break; case class="num">4: if(c_value<=r_value) s=class="num">1; class="kw">break; class="kw">default: s=class="num">0; class="kw">break; } class=class="str">"cmt">//--- if(s>class="num">0) class="kw">return(true); class="kw">return(false); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CProgram::CheckTimeframe(ENUM_TIMEFRAMES tf,SIGNAL &signal_set) { for(class="type">int i=class="num">0; i<class="num">21; i++) { if(StringToTimeframe(CharArrayToString(signal_set.tf_name[i].tf))==tf) class="kw">return(true); } class="kw">return(false); } class="type">bool SearchSignals(class="type">void); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CProgram::SearchSignals(class="type">void) { class=class="str">"cmt">//--- Search for set signals SIGNAL signal_set[]; class="type">int cnt=class="num">0; for(class="type">int i=class="num">0; i<class="num">5; i++) { if(FileIsExist("Signal Monitor\\signal_"+class="type">class="kw">string(i)+".bin")) cnt++; } class=class="str">"cmt">//--- ArrayResize(signal_set,cnt); ZeroMemory(signal_set); class=class="str">"cmt">//--- for(class="type">int i=class="num">0; i<cnt; i++) { class="type">int h=FileOpen("Signal Monitor\\signal_"+class="type">class="kw">string(i)+".bin",FILE_READ|FILE_BIN);
信号面板的渲染与窗口自适应
配置句柄拉到 INVALID_HANDLE 时直接弹窗报错并退出,这是面板初始化最常见的静默失败点——实盘里若没读到 signal_set 配置文件,后续按钮全空,必须靠 MessageBox 暴露出来。 下面这段逻辑负责把信号画到对应品种和周期的按钮上。先按 m_signal_button 数组长度遍历,取出每个格子的品种 sy 与周期 tf;若 CheckTimeframe 不通过就 continue 跳过,避免把 H1 信号误标到 M5 格。 GetSignal 命中后,按 label_type 决定标签内容是字符串还是指标值(DoubleToString 保留 3 位小数)。背景色、边框色、tooltip、图标依次设置;border_color 为 clrNONE 时回退用 back_color,图标为 false 时传 -1 清掉旧图标。 最后 AutoResize 用时间表头和品种表头的间隙加尺寸再加 5 像素算窗口大小,例如 XGap()+XSize()+5 决定宽。改完布局立刻调 SearchSignals() 重刷一遍,否则新尺寸下可能漏显信号。外汇与贵金属波动剧烈,面板仅作辅助提示,信号触发不代表行情必然朝该方向走。
if(h==INVALID_HANDLE) { MessageBox("Configuration not found","Signal Monitor"); class="kw">return(false); } FileReadStruct(h,signal_set[i]); FileClose(h); for(class="type">int j=class="num">0;j<ArraySize(m_signal_button); j++) { class=class="str">"cmt">//--- class="type">class="kw">string sy=GetSymbol(j); ENUM_TIMEFRAMES tf=GetTimeframe(j); class=class="str">"cmt">//--- if(!CheckTimeframe(tf,signal_set[i])) class="kw">continue; class=class="str">"cmt">//--- if(GetSignal(sy,tf,signal_set[i])) { class=class="str">"cmt">//--- if(signal_set[i].label_type==class="num">1) SetLabel(j,CharArrayToString(signal_set[i].label_value),signal_set[i].label_color); else SetLabel(j,DoubleToString(m_ind_value,class="num">3),signal_set[i].label_color); class=class="str">"cmt">//--- if(signal_set[i].back_color!=clrNONE) SetBackground(j,signal_set[i].back_color); class=class="str">"cmt">//--- if(signal_set[i].border_color!=clrNONE) SetBorderColor(j,signal_set[i].border_color); else SetBorderColor(j,signal_set[i].back_color); class=class="str">"cmt">//--- if(signal_set[i].tooltip) SetTooltip(j,CharArrayToString(signal_set[i].tooltip_text)); class=class="str">"cmt">//--- if(signal_set[i].image) SetIcon(j,signal_set[i].img_index); else SetIcon(j,-class="num">1); } } class=class="str">"cmt">//--- Resize window AutoResize(m_timeframe_label[tf-class="num">1].XGap()+m_timeframe_label[tf-class="num">1].XSize()+class="num">5,m_symbol_label[sy-class="num">1].YGap()+m_symbol_label[sy-class="num">1].YSize()+class="num">5); class=class="str">"cmt">//--- SearchSignals();
◍ 用枚举控制信号刷新节奏
把刷新周期做成枚举,比写死延时更利于复盘不同周期下的信号密度。下面这段把 1 分、15 分、30 分、1 小时、4 小时映射成可切换的输入项,EA 启动时由 Update 参数决定走哪一支。 switch 里把各周期换算成秒:MINUTE 是 60,MINUTE_15 是 900,MINUTE_30 是 1800,HOUR 是 3600,HOUR_4 是 14400;default 兜底设 1 秒,避免漏配时直接卡死。update 用 TimeLocal()+cnts 算出下次触发时刻,OnTick 里只做一件事——本地时间越过 update 就调 SearchSignals() 并重置计时。 实盘接这逻辑时,外汇与贵金属波动受消息扰动大,高频刷新(如 MINUTE)可能让信号在低波动段频繁空转;4 小时档对趋势型策略更稳,但滞后概率也更高。开 MT5 把 Update 从 HOUR 切到 HOUR_4,观察 SearchSignals 调用次数在一个交易日的落差,就能直观摸到周期成本。
enum TIMEFRAMMS { MINUTE, class=class="str">"cmt">// class="num">1 Minute MINUTE_15, class=class="str">"cmt">// class="num">15 Minutes MINUTE_30, class=class="str">"cmt">// class="num">30 Minutes HOUR, class=class="str">"cmt">// class="num">1 Hour HOUR_4 class=class="str">"cmt">// class="num">4 Hours }; class="kw">input UPDATE Update = HOUR; class=class="str">"cmt">// Update interval class="type">int cnts=class="num">0; class="type">class="kw">datetime update; class=class="str">"cmt">//--- class="kw">switch(Update) { case MINUTE: cnts=class="num">60; class="kw">break; case MINUTE_15: cnts=class="num">60*class="num">15; class="kw">break; case MINUTE_30: cnts=class="num">60*class="num">30; class="kw">break; case HOUR: cnts=class="num">3600; class="kw">break; case HOUR_4: cnts=class="num">3600*class="num">4; class="kw">break; class="kw">default: cnts=class="num">1; class="kw">break; } update=TimeLocal()+cnts; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { if(TimeLocal()>update) { program.SearchSignals(); update=TimeLocal()+cnts; } }
「画得少,看得清」
把随附的 MQL5 文件夹直接丢进终端根目录就能跑,别手动去拼路径。在 MT5 里按 Ctrl+Shift+D 可一步跳到根目录下的 MQL5 文件夹,省得在文件管理器里翻层。 若账户里品种太多导致面板崩溃,进源码的 Program.mqh 把符号数上限改成市场观察里实际的数量即可,作者在原讨论里确认过这是超限的直接解法。 附带代码里那个 SAVE 结构用 72 长度的 bool 数组 tf 记录各时间帧状态,对应常见时间帧槽位,复制去改时注意别越界。外汇与贵金属波动剧烈,这类多品种监控工具只降低漏看概率,不消除跳空与滑点风险。
class="kw">struct SAVE { class="type">bool tf[class="num">72]; };