监视多币种的交易信号(第三部分):引入搜索算法(基础篇)
给多品种信号监控加上搜索算法
在 MT5 里做跨品种信号监控,最笨的办法是每根 K 线把观察列表全扫一遍。观察品种一旦超过 30 个,CPU 占用和图表卡顿会明显上升,尤其同时挂了几个 EA 的时候。 原文给出的第三版方案,核心是把「品种集保存」和「信号搜索」拆成两层:先由用户维护一个可编辑的品种集(含添加、删除、参数修正),再由独立的搜索算法只在这个子集里跑条件匹配,而不是遍历市场观察窗口全部品种。 实际落点时,建议把品种集存成全局结构体或文件缓存,搜索线程按固定时间间隔(例如 1 秒或每新 tick)触发一次,避免在 OnTick 里同步全量计算。这样 50 个品种的信号扫描在普通 VPS 上也能把延迟压到可接受范围。 外汇与贵金属杠杆高、点差跳变频繁,信号触发后仍需人工确认,算法只解决「找得到」不解决「该不该跟」。
「从已有项目骨架接着加功能」
上一篇文章里已经搭好了交易信号监视应用的基础结构,界面也具备基本交互能力。这一节不重做框架,而是直接在这个已有 MT5 项目上继续堆功能。 具体落点是把「配置品种」和「搜索品种」这两块算法的可视部分补进界面。你打开上一篇文章留下的 mq5 工程,确认面板还能编译运行,再往下接新模块,避免从零起步浪费时间。 外汇与贵金属品种波动受杠杆和消息面影响大,这类监视工具只做信号呈现,不构成任何方向建议,实盘前务必在策略测试器里跑通逻辑。
◍ 把盯盘品种集存成模板文件
在 MT5 面板的第一步设置里,除了手工勾选和调用 All/Major/Crosses 预定义组,还能把当前勾选结果以名称存盘,下次直接加载。实现上要在 CProgram 基类私密段加 SaveSymbolSet 和 LoadSymbolSet 两个方法,并补一个含 bool tf[] 的 m_save 结构记录每个品种勾选状态。 保存时若文件名为空会弹窗拦截;成功写盘走 Signal Monitor\\<name>.bin 的二进制通道,用 FileWriteStruct 把整结构落盘。加载则反向读回,逐一对复选框 IsPressed 赋值并 Update,界面即时还原。 热键也接上:S 存、L 读,在 OnEvent 的按钮点击和按键事件里各挂一段即可。编译过后,图例 2 里保存/加载用户模板的交互就通了,做多品种贵金属或直盘交叉盘轮动时,这套能省掉每次重选的重复动作。外汇与贵金属波动剧烈,脚本只管选择集存储,不涉及任何方向判断。
class="type">bool SaveSymbolSet(class="type">class="kw">string file_name); class="type">bool LoadSymbolSet(class="type">class="kw">string file_name); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Save class="kw">template to a file | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CProgram::SaveSymbolSet(class="type">class="kw">string file_name) { if(file_name=="") { MessageBox("Select the class="kw">template name to record","Signal Monitor"); class="kw">return(false); } class="type">int h=FileOpen("Signal Monitor\\"+file_name+".bin",FILE_WRITE|FILE_BIN); if(h==INVALID_HANDLE) { MessageBox("Failed to create a configuration file","Signal Monitor"); class="kw">return(false); } else MessageBox("The "+file_name+" configuration has been successfully saved","Signal Monitor"); class=class="str">"cmt">//--- Save the selection of timeframes and patterns for(class="type">int i=class="num">0; i<m_all_symbols; i++) m_save.tf[i]=m_checkbox[i].IsPressed(); class=class="str">"cmt">//--- FileWriteStruct(h,m_save); FileClose(h); class=class="str">"cmt">//--- class="kw">return(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Load data to a panel | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CProgram::LoadSymbolSet(class="type">class="kw">string file_name) { if(file_name=="") { MessageBox("Select the class="kw">template name to load","Signal Monitor"); class="kw">return(false); } class="type">int h=FileOpen("Signal Monitor\\"+file_name+".bin",FILE_READ|FILE_BIN); if(h==INVALID_HANDLE) { MessageBox("Configuration "+file_name+" not found","Signal Monitor"); class="kw">return(false); } ZeroMemory(m_save); FileReadStruct(h,m_save); class=class="str">"cmt">//--- Loading timeframes for(class="type">int i=class="num">0; i<m_all_symbols; i++) { m_checkbox[i].IsPressed(m_save.tf[i]); m_checkbox[i].Update(true); } class=class="str">"cmt">//--- FileClose(h); class=class="str">"cmt">//---
用键盘快捷键接管模板存读
在面板交互之外,直接绑键盘能少点两次鼠标。上面这段把存/读品种集的逻辑挂到了按键事件里,并且用了一个 m_save 结构体(tf 数组长度 100)来暂存各周期开关状态。 代码里 m_current_step==1 时才响应按键,说明这套快捷键只在特定向导步骤生效,避免误触。TranslateKey 把系统按键码转成 Unicode short,sym>0 才认为转换成功。 判定字符时同时兼容了英文字母和俄文字母:'l'/'д' 触发 LoadSymbolSet,'s'/'ы' 触发 SaveSymbolSet,等于一套键位照顾两种输入法布局。 实盘里你可以把 m_text_edit.GetValue() 换成固定模板名,省去每次手输;外汇与贵金属波动剧烈、杠杆风险高,快捷键只提效率不替你决策。
class="kw">return(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Class for creating the application | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">struct SAVE { class="type">bool tf[class="num">100]; }; class CProgram : class="kw">public CWndEvents { ... SAVE m_save; class=class="str">"cmt">//--- Save the class="kw">template if(lparam==m_save_button.Id()) { SaveSymbolSet(m_text_edit.GetValue()); } class=class="str">"cmt">//--- Load the class="kw">template if(lparam==m_load_button.Id()) { LoadSymbolSet(m_text_edit.GetValue()); } class=class="str">"cmt">//--- Key press if(id==CHARTEVENT_KEYDOWN) { if(m_current_step==class="num">1) { class="type">class="kw">short sym=TranslateKey((class="type">int)lparam); class=class="str">"cmt">//--- if the entered character is successfully converted to Unicode if(sym>class="num">0) { if(ShortToString(sym)=="l" || ShortToString(sym)=="д") LoadSymbolSet(m_text_edit.GetValue()); if(ShortToString(sym)=="s" || ShortToString(sym)=="ы") SaveSymbolSet(m_text_edit.GetValue()); } } }
「信号增改界面的按钮与数据结构落地」
做信号创建窗口,第一步不是画图表,而是先把「Add」和「Cancel」两个 CButton 钉在窗口底部。基类 Program.mqh 里先声明 m_new_signal 和 m_cancel_button 两个实例,再在 SetWindow.mqh 的 CreateSetWindow() 末尾插入创建调用,编译后窗口底部就多出两个 60×30 像素的控件。 取消按钮逻辑很直白:不写任何参数、直接关窗。Add 按钮则要串联一套动作——先把 UI 上选好的指标类型、周期、规则、阈值等塞进一个 SIGNAL 结构,存盘后关窗,并在主窗口信号列表里挂出一条记录;之后点这条记录,界面回填旧值,按钮文字从 Add 变成 Save。 SIGNAL 结构就是整套配置的容器:ind_type 记指标种类,ind_period 记周期,app_price 对 RSI 有用、对 WPR 无效;rule_type 存「== / >= / <=」类比较符,rule_value 是阈值。显示层由 label_type、label_value、label_color 管,监视器底色走 back_color 和 border_color,timeframes 数组存多周期设定。基类里再声明一个 SIGNAL 数组,配合 Save/Load 两个私有方法做文件读写。 为支持点击旧记录重新编辑,主窗口用 CButton 数组承载信号列表对象,在 StepWindow.mqh 的 CreateStepWindow() 里加 5 个,CreateGUI() 里先隐藏。构造函数调 ClearSaves() 清掉历史残留,OnEvent() 里判明被按下的信号按钮后,用 LoadSignalSet() 回填界面、改按钮名为 Save、再开窗。外汇与贵金属行情波动剧烈,这类自定义信号工具仅作辅助监控,实际触发概率不等于执行胜率,请先在 MT5 策略测试器里跑通再上真仓。
class="type">bool CreateButton3(CButton &button,class="type">class="kw">string text,class="kw">const class="type">int x_gap,class="kw">const class="type">int y_gap); CButton m_new_signal; CButton m_cancel_button; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CProgram::CreateButton3(CButton &button,class="type">class="kw">string text,class="kw">const class="type">int x_gap,class="kw">const class="type">int y_gap) { class=class="str">"cmt">//--- class="type">color baseclr=C&class="macro">#x27;class="num">70,class="num">180,class="num">70&class="macro">#x27;; class="type">color pressed=C&class="macro">#x27;class="num">70,class="num">170,class="num">70&class="macro">#x27;; class=class="str">"cmt">//--- Save the window pointer button.MainPointer(m_set_window); class=class="str">"cmt">//--- Set up properties before creation button.XSize(class="num">60); button.YSize(class="num">30); button.Font(m_base_font); button.FontSize(m_base_font_size); button.BackColor(baseclr); button.BackColorHover(baseclr); button.BackColorPressed(pressed); button.BorderColor(baseclr); button.BorderColorHover(baseclr); button.BorderColorPressed(pressed); button.LabelColor(clrWhite); button.LabelColorPressed(clrWhite); button.LabelColorHover(clrWhite); button.IsCenterText(true); class=class="str">"cmt">//--- Create the control if(!button.CreateButton(text,x_gap,y_gap)) class="kw">return(false); class=class="str">"cmt">//--- Add the element pointer to the base CWndContainer::AddToElementsArray(class="num">1,button); class="kw">return(true); } class=class="str">"cmt">//--- Add/Cancel Buttons if(!CreateButton3(m_new_signal,"Add",m_set_window.XSize()-class="num">2*(class="num">60+class="num">10),m_set_window.YSize()-(class="num">30+class="num">10))) class="kw">return(false);
◍ 信号集的落地存储结构
面板右下角退出键的摆放位置由窗口尺寸倒推:X 方向留 70 像素(60 宽 + 10 边距)、Y 方向留 40 像素(30 高 + 10 边距),若 CreateButton3 返回失败则直接中断初始化。 信号配置的核心是一个 SIGNAL 结构体,它用 ind_type、ind_period、app_price、rule_type、rule_value 五个字段锁定指标与触发规则,再用 label_type 加最长 10 字节的 label_value 控制图表标注样式,back_color、border_color 等管绘制外观。timeframes[21] 与 tf_name[21] 对应 MT5 全部 21 个周期,意味着同一信号可跨周期生效。 实际最多缓存 5 组信号于 m_signal_set[5],由 SaveSignalSet(int index) 写盘、LoadSignalSet(int index) 读回。SaveSignalSet 把面板控件当前值逐一搬进结构体,再 FileOpen 到 Signal Monitor\signal_{index}.bin 以 FILE_WRITE|FILE_BIN 二进制落盘;句柄无效就弹窗并报错返回 false。 写盘时 label_value 只截取文本框前 3 字符转 char 数组,背景色仅在 m_set_param[0] 按下时才记录——这两处截断与条件存储,是调面板时最容易漏掉的行为。
if(!CreateButton3(m_cancel_button,"Cancel",m_set_window.XSize()-(class="num">60+class="num">10),m_set_window.YSize()-(class="num">30+class="num">10))) class="kw">return(false); class="kw">struct SIGNAL { class="type">int ind_type; class="type">int ind_period; class="type">int app_price; class="type">int rule_type; class="type">class="kw">double rule_value; class="type">int label_type; class="type">uchar label_value[class="num">10]; class="type">color label_color; class="type">color back_color; class="type">color border_color; class="type">bool tooltip; class="type">uchar tooltip_text[class="num">100]; class="type">bool image; class="type">int img_index; class="type">bool timeframes[class="num">21]; TFNAME tf_name[class="num">21]; }; SIGNAL m_signal_set[class="num">5]; class="type">bool SaveSignalSet(class="type">int index); class="type">bool LoadSignalSet(class="type">int index); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CProgram::SaveSignalSet(class="type">int index) { class=class="str">"cmt">//--- class="type">int h=FileOpen("Signal Monitor\\signal_"+class="type">class="kw">string(index)+".bin",FILE_WRITE|FILE_BIN); if(h==INVALID_HANDLE) { MessageBox("Failed to create a configuration file","Signal Monitor"); class="kw">return(false); } class=class="str">"cmt">//--- Save the selection class=class="str">"cmt">//--- Indicator type m_signal_set[index].ind_type=m_indicator_type.GetListViewPointer().SelectedItemIndex(); class=class="str">"cmt">//--- Indicator period m_signal_set[index].ind_period=(class="type">int)m_period_edit.GetValue(); class=class="str">"cmt">//--- Type of applied price m_signal_set[index].app_price=m_applied_price.GetListViewPointer().SelectedItemIndex(); class=class="str">"cmt">//--- Rule type m_signal_set[index].rule_type=m_rule_type.GetListViewPointer().SelectedItemIndex(); class=class="str">"cmt">//--- Rule value m_signal_set[index].rule_value=(class="type">class="kw">double)m_rule_value.GetValue(); class=class="str">"cmt">//--- Text label display type m_signal_set[index].label_type=m_label_button[class="num">0].IsPressed()?class="num">0:class="num">1; class=class="str">"cmt">//--- Save the value of the text field for the second type if(m_label_button[class="num">1].IsPressed()) StringToCharArray(StringSubstr(m_text_box.GetValue(),class="num">0,class="num">3),m_signal_set[index].label_value); class=class="str">"cmt">//--- Color of the text label m_signal_set[index].label_color=m_color_button[class="num">0].CurrentColor(); class=class="str">"cmt">//--- Background class="type">color if(m_set_param[class="num">0].IsPressed()) m_signal_set[index].back_color=m_color_button[class="num">1].CurrentColor(); else