监视多币种的交易信号(第五部分):复合信号(基础篇)
多币种复合信号的基础构成
在 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() 渲染。新版本比旧版短近三倍,逻辑更直。
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 面板空白,先在模拟盘验证再上真仓。
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 里跑这套,盯一下按钮置灰时机就能反推这个阈值。
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());