监视多币种的交易信号(第三部分):引入搜索算法(基础篇)
📘

监视多币种的交易信号(第三部分):引入搜索算法(基础篇)

第 1/3 篇

给多品种信号监控加上搜索算法

在 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 里保存/加载用户模板的交互就通了,做多品种贵金属或直盘交叉盘轮动时,这套能省掉每次重选的重复动作。外汇与贵金属波动剧烈,脚本只管选择集存储,不涉及任何方向判断。

MQL5 / C++
  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() 换成固定模板名,省去每次手输;外汇与贵金属波动剧烈、杠杆风险高,快捷键只提效率不替你决策。

MQL5 / C++
  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 策略测试器里跑通再上真仓。

MQL5 / C++
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] 按下时才记录——这两处截断与条件存储,是调面板时最容易漏掉的行为。

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

常见问题

把品种集写入一个本地模板文件,程序启动时读取即可;文中给出了存读文件的数据结构与按钮逻辑,照抄就能用。
在键盘事件处理函数里绑定快捷键,触发后调用模板读取函数刷新信号集;原文有具体按键接管示例。
可以,小布盯盘的 AIGC 已内置多品种信号诊断,打开对应品种页就能直接看,不用自己写监控代码。
界面按钮操作信号集数组,增改时同步写回存储结构;文中列出了按钮事件与结构体字段的映射。
用结构化文件按固定字段存,每次改动后整体覆写;原文的基础篇给出了最小可用存储结构示例。