监视多币种的交易信号(第五部分):复合信号(基础篇)
📘

监视多币种的交易信号(第五部分):复合信号(基础篇)

第 1/3 篇

多币种复合信号的基础构成

在 MT5 里做跨品种盯盘,单一币种的信号容易受局部噪声干扰,把多个关联品种的信号叠加成复合信号,能压低假突破带来的误触发概率。这套思路在 2020 年 8 月公开的实例中,被用于同时监视 4 个以上外汇品种的方向共振。 原文给出的第五部分定位是「复合信号」,即不再孤立判断 EURUSD 或 XAUUSD,而是用统一函数把各品种的阈值状态聚合成一个可比较的输出。外汇与贵金属杠杆高、跳空频繁,共振失效时回撤可能超过单品种,实盘前务必在策略测试器里跑历史样本。 实现上通常先写品种枚举与信号权重的容器,再在主循环里按 tick 刷新状态;下一篇会落到具体 MQL5 函数,当前只需明确:复合信号的核心是可验证的权重表,而非主观印象。

「为什么单指标系统在横盘里会失灵」

在前面做多币种信号监控器时,我们已经能用自定义指标加过滤规则跑起来。但实盘里很多交易者和开发者其实是叠多个指标一起用的,因为单一类型指标在某种行情下必然有短板。 比如趋势类指标,在长时间横盘震荡里会刷出大量假信号,这时候它必须靠别的工具来确认。做法要么是挂几个非主线指标当过滤器,要么是干脆用一组指向同一状态指标共同确认入场。 外汇和贵金属市场的高风险在于,横盘假信号会直接吃掉震荡段的利润。下一阶段的逻辑就是把若干简单信号合成复合信号,我们直接沿用上一篇文章结尾的那个项目骨架来改。

◍ 用 AND/OR 把简单信号拧成复合信号

早期版本里系统只跑简单信号:接一个数据源,套个条件,用 RSI、WPR、CCI 这类指标就能表达,也留了自定义指标的口子。 复合信号则是把两个及以上简单信号用逻辑 AND(与)或 OR(或)拼起来。AND 要求多个条件同周期成立,OR 能在多个方向同时扫信号,覆盖更宽的行情状态。 界面上要加三样东西:建复合信号的按钮、已建复合信号列表、给每个简单信号打用途标。标 S 的只能单独跑监视器;标 C 的只能进复合信号;标 B 的两种都行。图例里若信号标成 S,就不会出现在复合信号的可选清单中。 建复合信号时,把 Simple 1、Simple 2 这类标了 C 或 B 的信号拖进规则区的三个批次,再用按钮选批次间的逻辑运算符。比如 Simple 1 AND Simple 2,就只在两者同屏触发时才报复合信号。外汇和贵金属波动大、杠杆高,信号组合只提高过滤精度,不消除踩错节奏的风险。

把信号编辑器从单体拆成可扩展架构

要给 MT5 面板加复合信号之前,得先确认旧的单信号 UI 已能承载新交互。原简单按钮上的信号名直接换成用户自定义文本标签,再补一个 Edit 按钮和用法开关——这一步不改好,后面复合逻辑塞不进去。 打开上一篇文章的工程,先删掉 CreateSignalEditor() 方法和 m_signal_editor[] 数组。在写新控件前,用宏定死上限:SIMPLE_SIGNALS 10、COMPOSITE_SIGNALS 5,也就是单信号最多 10 个、复合信号最多 5 个,超了界面不画。 基类 CProgram 里新增 m_signal_ind[]、m_signal_type[](CButton 数组)和 m_signal_label[](CTextLabel 数组),再声明 CreateSignalSet()。复合信号部分只需补 m_c_signal_label[] 和 m_c_signal_ind[],结构是同构的。 UI 元素一多,OnEvent() 就会膨胀到没法读。我的做法是把所有关键交互包成独立方法,OnEvent() 只留关键事件清单。例如把新增简单信号的代码挪进 AddSimpleSignal(),编辑进 EditSimpleSignal(),用法开关进 SignalSwitch()。 复合信号对话框放在新头文件 CSetWindow.mqh,基类引 Program.mqh 拿控制权。CreateCompositeEdit() 画出卡槽和逻辑符:三个卡槽 + 两个三态键(AND / OR / (OR)),注意 OR 和 (OR) 在三个表达式连排时括号含义不同,图例 10 有反例。 选择简单信号填充卡槽用 SignalSelection():点可用信号,第一卡槽变蓝写名;再点填第二槽;点已占槽的信号则释放。LoadForCompositeSignal() 做校验——至少两个可用信号且属性为 C 或 B,名为 S 的纯简单信号不进列表。 存盘时 SaveCompositeSignalSet() 加 first_save 参数区分首次保存和改参覆盖;CheckCSignalNames() 查重名,CheckCorrectSlots() 验卡槽完整性。编译报缺 m_c_signal_set 时,补 C_SIGNAL 结构和上面两个检查方法即可。 SearchSignals() 拆成两段:简单信号段用 GetSimpleSignal() 读文件、SetVisualSignal() 画图,并跳过仅作复合部件的信号;复合段用 GetUsedSlots() 判是两槽还是三槽结构,再 SetVisualCompositeSignal() 渲染。新版本比旧版短近三倍,逻辑更直。

MQL5 / C++
class="macro">#define SIMPLE_SIGNALS class="num">10
class="macro">#define COMPOSITE_SIGNALS class="num">5
  for(class="type">int i=class="num">0;i<class="num">5;i++)
  {

「信号面板控件的批量布局与封装」

在 MT5 自定义面板里,信号编辑器和标签的排布靠循环 + 固定偏移实现。简单信号区用 SIMPLE_SIGNALS 次循环,每个控件纵向间隔 40 像素,起始 Y 为 90,标签再下移 5 像素到 95,X 统一为 10。 CreateSignalSet 把按钮样式写死在内部:尺寸 30×30,背景与边框同色(如 C'255,195,50' 橙、C'75,190,240' 蓝),文字白色居中。这样调用方只需传文字、基准色和坐标,就能批量生成风格一致的按钮,避免重复设属性。 复合信号区(COMPOSITE_SIGNALS)逻辑类似,但 X 基准右移到 300,按钮 X 再加 290 到 440,说明面板横向分了两栏。若 CreateLabel 或 CreateSignalSet 任一返回 false,整个初始化直接 return false,面板构建会中断——实盘前应在 MT5 策略测试器里跑一遍,确认控件数不超限。 外汇与贵金属界面开发属高风险环境,控件创建失败可能让 EA 面板空白,先在模拟盘验证再上真仓。

MQL5 / C++
if(!CreateSignalEditor(m_signal_editor[i],"Signal_"+class="type">class="kw">string(i),class="num">10,class="num">40*i+class="num">90))
   class="kw">return(false);
  }
  for(class="type">int i=class="num">0;i<SIMPLE_SIGNALS; i++)
  {
   if(!CreateLabel(m_signal_label[i],class="num">10,class="num">40*i+class="num">95,"Signal_"+class="type">class="kw">string(i)))
     class="kw">return(false);
   if(!CreateSignalSet(m_signal_ind[i],"Edit",C&class="macro">#x27;class="num">255,class="num">195,class="num">50&class="macro">#x27;,class="num">150,class="num">40*i+class="num">90))
     class="kw">return(false);
   if(!CreateSignalSet(m_signal_type[i],"S",C&class="macro">#x27;class="num">75,class="num">190,class="num">240&class="macro">#x27;,class="num">150+class="num">35,class="num">40*i+class="num">90))
     class="kw">return(false);
  }
  CTextLabel       m_signal_label[SIMPLE_SIGNALS];
  CButton          m_signal_ind[SIMPLE_SIGNALS];
  CButton          m_signal_type[SIMPLE_SIGNALS];
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CProgram::CreateSignalSet(CButton &button,class="type">class="kw">string text,class="type">class="kw">color baseclr,const class="type">int x_gap,const class="type">int y_gap)
{
class=class="str">"cmt">//--- Store the window pointer
  button.MainPointer(m_step_window);
class=class="str">"cmt">//--- Set properties before creation
  button.XSize(class="num">30);
  button.YSize(class="num">30);
  button.Font(m_base_font);
  button.FontSize(m_base_font_size);
  button.BackColor(baseclr);
  button.BackColorHover(baseclr);
  button.BackColorPressed(baseclr);
  button.BorderColor(baseclr);
  button.BorderColorHover(baseclr);
  button.BorderColorPressed(baseclr);
  button.LabelColor(clrWhite);
  button.LabelColorPressed(clrWhite);
  button.LabelColorHover(clrWhite);
  button.IsCenterText(true);
class=class="str">"cmt">//--- Create a control element
  if(!button.CreateButton(text,x_gap,y_gap))
    class="kw">return(false);
class=class="str">"cmt">//--- Add a pointer to the element to the database
  CWndContainer::AddToElementsArray(class="num">0,button);
  class="kw">return(true);
}
CTextLabel       m_c_signal_label[COMPOSITE_SIGNALS];
CButton          m_c_signal_ind[COMPOSITE_SIGNALS];
class=class="str">"cmt">//---
  for(class="type">int i=class="num">0;i<COMPOSITE_SIGNALS; i++)
  {
   if(!CreateLabel(m_c_signal_label[i],class="num">300,class="num">40*i+class="num">95,"Signal_"+class="type">class="kw">string(i)))
     class="kw">return(false);
   if(!CreateSignalSet(m_c_signal_ind[i],"Edit",C&class="macro">#x27;class="num">255,class="num">195,class="num">50&class="macro">#x27;,class="num">150+class="num">290,class="num">40*i+class="num">90))
     class="kw">return(false);
  }
  if(!CreateIconButton(m_add_signal,m_lang[class="num">15],class="num">10,class="num">30))

◍ 信号面板的按钮与新增逻辑落点

这段面板初始化把两个图标按钮放在横坐标 10 和 300、纵坐标 30 的位置,失败就直接 return(false) 中断创建;信号区标题标签则统一落在 y=70(30+30+10)处,左列取语言项 16、右列取 44,坐标写死意味着想加第三列得改这几个常量。 AddSimpleSignal 里用 lparam==m_new_signal.Id() 判定是不是「新建」入口;m_number_signal 小于 0 走新增分支,否则走按序号覆写分支,SaveSignalSet 的第二个参数 false 就是覆写标记。 新增成功后会关掉设置窗,且当 m_total_signals 小于 SIMPLE_SIGNALS 才自增并 Show 对应 label——超了就弹「Maximum number of signals is N」的 MessageBox,N 由 SIMPLE_SIGNALS 宏决定,改这个宏就能扩面板容量。 当信号数大于 1 时,会把 m_add_signal[1] 解锁并刷新,说明第二个添加按钮在只有 1 个信号时是锁定的;你在 MT5 里跑这套,盯一下按钮置灰时机就能反推这个阈值。

MQL5 / C++
  if(!CreateIconButton(m_add_signal[class="num">0],m_lang[class="num">15],class="num">10,class="num">30))
    class="kw">return(false);
  if(!CreateIconButton(m_add_signal[class="num">1],m_lang[class="num">43],class="num">300,class="num">30))
    class="kw">return(false);
  if(!CreateLabel(m_signal_header,class="num">10,class="num">30+class="num">30+class="num">10,m_lang[class="num">16]))
    class="kw">return(false);
  if(!CreateLabel(m_signal_header[class="num">0],class="num">10,class="num">30+class="num">30+class="num">10,m_lang[class="num">16]))
    class="kw">return(false);
  if(!CreateLabel(m_signal_header[class="num">1],class="num">300,class="num">30+class="num">30+class="num">10,m_lang[class="num">44]))
    class="kw">return(false);
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Adds a new simple trading signal                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CProgram::AddSimpleSignal(class="type">long lparam)
{
  if(lparam==m_new_signal.Id())
  {
    if(m_number_signal<class="num">0)
    {
      if(SaveSignalSet(m_total_signals))
      {
        m_set_window.CloseDialogBox();
        if(m_total_signals<SIMPLE_SIGNALS)
        {
          m_total_signals++;
          m_signal_label[m_total_signals-class="num">1].Show();
          m_signal_label[m_total_signals-class="num">1].LabelText(m_signal_name.GetValue());
          m_signal_label[m_total_signals-class="num">1].Update(true);
          m_signal_type[m_total_signals-class="num">1].Show();
          m_signal_ind[m_total_signals-class="num">1].Show();
        }
        else
          MessageBox("Maximum number of signals is "+class="type">class="kw">string(SIMPLE_SIGNALS),"Signal Monitor");
        class=class="str">"cmt">//---
        if(m_total_signals>class="num">1)
        {
          m_add_signal[class="num">1].IsLocked(false);
          m_add_signal[class="num">1].Update(true);
        }
      }
    }
    else
    {
      if(SaveSignalSet(m_number_signal,false))
      {
        m_set_window.CloseDialogBox();
        m_signal_label[m_number_signal].LabelText(m_signal_name.GetValue());

常见问题

单指标在波动率收缩时容易频繁触发,可统计一段时间内信号胜率,若低于随机水平就说明该系统在横盘已失效,需叠加过滤条件。
用 AND 要求两条件同周期成立才报信号,OR 则任一成立即报;横盘用 AND 降噪,趋势用 OR 提敏,回测确定阈值。
小布可直接加载多品种复合信号模板,你选好指标与 AND/OR 关系后由它批量监视并推送异动,省去手搭面板。
拆成模块后只需注册新信号源与面板控件,核心调度不变,新增成本约等于填一份配置,不必动主循环。
用容器封装按品种分组,按钮统一事件入口,新增逻辑只落在一个回调里,界面与代码都不再膨胀。