监视多币种的交易信号(第五部分):复合信号·进阶篇
(2/3)· 横盘里趋势指标狂吐假信号?用 AND/OR 把多个简单信号拧成一条复合信号,覆盖更大行情范围
◍ 信号命名与配置落盘的实现细节
在信号监控面板里,每个信号都需要一个可编辑的文本框来命名。CreateSignalName 函数把文本框宽度锁在 110 像素、高度 24 像素,文本框本体再向右偏移 110 像素、宽度扩到 200 像素,默认文字色用 clrSilver,这套尺寸在外语版 MT5 上基本不会挤变形。 SaveSignalSet 负责把信号配置写成二进制文件。文件名按 index 拼成 Signal Monitor\signal_{index}.bin,用 FILE_WRITE|FILE_BIN 打开;若句柄等于 INVALID_HANDLE 就弹窗报错并返回 false。 存盘前有个防重名检查:first_save 为 true 时调用 CheckSignalNames 比对已用名,撞名就按终端语言弹俄文或英文提示并中止。索引还受 SIMPLE_SIGNALS 常量约束,超界同样弹窗拦截。 下面这段是落盘与防重名的原始实现,逐行看逻辑更直观:text_edit.MainPointer 绑定主窗口;XSize/YSize 定控件尺寸;GetTextBoxPointer().XGap(110) 把输入区右移;DefaultText 取多语言数组第 44 项;CreateTextEdit 失败返回 false;AddToElementsArray(1,text_edit) 归入元素组。SaveSignalSet 里 first_save&&!CheckSignalNames 成立则按 TERMINAL_LANGUAGE 弹对应 MessageBox 并 return false;FileOpen 失败同理;index>SIMPLE_SIGNALS-1 也走语言分支报错。
class="type">bool CProgram::CreateSignalName(CTextEdit &text_edit,const class="type">int x_gap,const class="type">int y_gap) { class=class="str">"cmt">//--- Store the pointer to the main control text_edit.MainPointer(m_set_window); class=class="str">"cmt">//--- Properties text_edit.XSize(class="num">110); text_edit.YSize(class="num">24); text_edit.Font(m_base_font); text_edit.FontSize(m_base_font_size); text_edit.GetTextBoxPointer().XGap(class="num">110); text_edit.GetTextBoxPointer().XSize(class="num">200); text_edit.GetTextBoxPointer().DefaultTextColor(clrSilver); text_edit.GetTextBoxPointer().DefaultText(m_lang[class="num">44]); class=class="str">"cmt">//--- Create a control element if(!text_edit.CreateTextEdit(m_lang[class="num">44],x_gap,y_gap)) class="kw">return(false); class=class="str">"cmt">//--- Add the object to the common array of object groups CWndContainer::AddToElementsArray(class="num">1,text_edit); class="kw">return(true); } class="type">uchar signal_name[class="num">50]; class="type">bool SaveSignalSet(class="type">int index,class="type">bool first_save=true); class="type">bool CProgram::SaveSignalSet(class="type">int index,class="type">bool first_save=true) { class=class="str">"cmt">//--- if(first_save && !CheckSignalNames(m_signal_name.GetValue())) { if(TerminalInfoString(TERMINAL_LANGUAGE)=="Russian") MessageBox("Это имя уже используется","Монитор сигналов"); else MessageBox("This name is already in use","Signal Monitor"); class="kw">return(false); } 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) { if(TerminalInfoString(TERMINAL_LANGUAGE)=="Russian") MessageBox("Не удалось создать файл конфигурации","Монитор сигналов"); else MessageBox("Failed to create configuration file","Signal Monitor"); class="kw">return(false); } if(index>SIMPLE_SIGNALS-class="num">1) { if(TerminalInfoString(TERMINAL_LANGUAGE)=="Russian") MessageBox("Максимальное число сигналов не должно быть больше "+class="type">class="kw">string(SIMPLE_SIGNALS),"Монитор сигналов"); else
「信号录入时的参数校验与落地」
在把用户配置写进信号集之前,面板会先做几道硬校验。若信号名留空,弹窗提示后直接 FileClose(h) 并 return(false),俄语终端显示「Введите Имя Сигнала」,其他语言显示「Enter the Signal Name」。 信号名长度不足 3 个字符同样被拒,提示「Signal Name must be at least 3 letters」,这里用 StringLen(m_signal_name.GetValue())<3 做判断,少于 3 个字母就不允许保存。 指标类型不等于 9 时,才读取周期与 applied price:ind_period 取自编辑框的整型值,app_price 取列表选中项索引。类型等于 9 代表自定义指标,必须填 path 与 param,任一为空都会弹窗并中断保存。 自定义指标参数要求以逗号分隔传入字符串,例如「14,0」这种写法;不填路径则提示「Enter the indicator path」,不填参数提示「Enter indicator parameters separated by commas」。外汇与贵金属信号监控涉及实时报价,误配参数可能导致监控失效,实盘前请在 MT5 策略测试器或用模拟账户验证。
MessageBox("Maximum number of signals is "+class="type">class="kw">string(SIMPLE_SIGNALS),"Signal Monitor"); class="kw">return(false); } class=class="str">"cmt">//--- Save the selection class=class="str">"cmt">//--- Indicator name if(m_signal_name.GetValue()=="") { if(TerminalInfoString(TERMINAL_LANGUAGE)=="Russian") MessageBox("Введите Имя Сигнала","Монитор сигналов"); else MessageBox("Enter the Signal Name","Signal Monitor"); FileClose(h); class="kw">return(false); } else if(StringLen(m_signal_name.GetValue())<class="num">3) { if(TerminalInfoString(TERMINAL_LANGUAGE)=="Russian") MessageBox("Имя Сигнала должно быть не менее class="num">3 букв","Монитор сигналов"); else MessageBox("Signal Name must be at least class="num">3 letters","Signal Monitor"); FileClose(h); class="kw">return(false); } else StringToCharArray(m_signal_name.GetValue(),m_signal_set[index].signal_name); class=class="str">"cmt">//--- Indicator type m_signal_set[index].ind_type=m_indicator_type.GetListViewPointer().SelectedItemIndex(); class=class="str">"cmt">//--- Indicator period if(m_signal_set[index].ind_type!=class="num">9) { 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(); } else { class="type">class="kw">string path=m_custom_path.GetValue(); class="type">class="kw">string param=m_custom_param.GetValue(); if(path=="") { if(TerminalInfoString(TERMINAL_LANGUAGE)=="Russian") MessageBox("Введите путь к индикатору","Монитор сигналов"); else MessageBox("Enter the indicator path","Signal Monitor"); FileClose(h); class="kw">return(false); } if(param=="") { if(TerminalInfoString(TERMINAL_LANGUAGE)=="Russian") MessageBox("Введите параметры индикатора через запятую","Монитор сигналов"); else MessageBox("Enter indicator parameters separated by commas","Signal Monitor"); FileClose(h); class="kw">return(false); }
信号配置落盘前的字段装配与校验
这段逻辑发生在用户点保存之后,先把界面控件的值逐一搬进 m_signal_set[index] 结构体的对应字段。自定义路径和参数走 StringToCharArray 转成字符数组,指标周期用 (int) 强转自编辑框数值,规则类型与比较方式取列表选中项的索引。 标签显示分两种:按钮0按下记0、否则记1;若按钮1按下,只截取文本框前3字符写进 label_value,这是给第二显示类型留的短标记。颜色类字段都先看对应开关是否按下,没按就填 clrNONE,避免无意义的边框和背景渲染。 时间段(timeframe)用 0~20 的循环扫21个按钮,按下且未锁定的才标 true 并记名称,同时 tf 计数器加1。若循环结束 tf<1,弹窗报“No timeframes selected”(俄语环境出俄文),关文件并返回 false——也就是说不挂任何周期的配置根本写不进磁盘。 [CODE] StringToCharArray(path,m_signal_set[index].custom_path); StringToCharArray(param,m_signal_set[index].custom_val); m_signal_set[index].ind_period=(int)m_period_edit.GetValue(); } //--- Rule type m_signal_set[index].rule_int=m_rule_interval.GetListViewPointer().SelectedItemIndex(); //--- Comparison type m_signal_set[index].rule_type=m_rule_type.GetListViewPointer().SelectedItemIndex(); //--- Rule value m_signal_set[index].rule_value1=(double)m_rule_value[0].GetValue(); m_signal_set[index].rule_value2=(double)m_rule_value[1].GetValue(); //--- Text label display type m_signal_set[index].label_type=m_label_button[0].IsPressed()?0:1; //--- Save the value of the text field for the second type if(m_label_button[1].IsPressed()) StringToCharArray(StringSubstr(m_text_box.GetValue(),0,3),m_signal_set[index].label_value); //--- Color of the text label m_signal_set[index].label_color=m_color_button[0].CurrentColor(); //--- Backdrop color if(m_set_param[0].IsPressed()) m_signal_set[index].back_color=m_color_button[1].CurrentColor(); else m_signal_set[index].back_color=clrNONE; //--- Border color if(m_set_param[1].IsPressed()) m_signal_set[index].border_color=m_color_button[2].CurrentColor(); else m_signal_set[index].border_color=clrNONE; //--- Hint value m_signal_set[index].tooltip=m_set_param[2].IsPressed(); if(m_signal_set[index].tooltip) StringToCharArray(m_tooltip_text.GetValue(),m_signal_set[index].tooltip_text); //--- Selected image m_signal_set[index].image=m_set_param[3].IsPressed(); if(m_signal_set[index].image) m_signal_set[index].img_index=m_pictures_slider.GetRadioButtonsPointer().SelectedButtonIndex(); //--- Selected timegrames int tf=0; for(int i=0; i<21; i++) { if(!m_tf_button[i].IsLocked() && m_tf_button[i].IsPressed()) { m_signal_set[index].timeframes[i]=true; StringToCharArray(m_tf_button[i].LabelText(),m_signal_set[index].tf_name[i].tf); tf++; } else m_signal_set[index].timeframes[i]=false; } //--- if(tf<1) { if(TerminalInfoString(TERMINAL_LANGUAGE)=="Russian") MessageBox("Не выбран ни один таймфрейм","Монитор сигналов"); else MessageBox("No timeframes selected","Signal Monitor"); FileClose(h); return(false); } //--- FileWriteStruct(h,m_signal_set[index]); FileClose(h); Print("Configuration "+m_signal_name.GetValue()+" successfully saved"); //--- return(true); } [/CODE] 代码逐行拆解: 第1-3行:把路径串和参数字串转字符数组存入结构体,周期值从编辑框取并转整型。 第6-12行:规则间隔、比较类型取列表选中索引;两个规则值从双精度编辑框取。 第14-17行:标签类型由按钮0状态决定;按钮1按下时截文本框前3字符存 label_value。 第19-27行:标签色直接取;背景/边框色按开关决定是否取色,否则 clrNONE。 第29-37行:提示文本与图片按对应开关装配,图片记单选滑块的选中序号。 第39-53行:扫21个周期按钮,按下且未锁定的记 true 并累加 tf;循环后 tf<1 则弹窗并中止保存。 第55-60行:校验通过才 FileWriteStruct 写结构、关文件、打印成功日志并返回 true。 开 MT5 把这段嵌进自己的信号监控 EA,故意不勾任何周期点保存,能直接复现那个英文(或俄文)弹窗并返回 false,证明周期校验在写盘前就已拦截。外汇与贵金属信号监控涉及高风险,配置错误可能导致漏报或误报,实盘前务必在策略测试器跑通保存-加载闭环。
StringToCharArray(path,m_signal_set[index].custom_path); StringToCharArray(param,m_signal_set[index].custom_val); m_signal_set[index].ind_period=(class="type">int)m_period_edit.GetValue(); } class=class="str">"cmt">//--- Rule type m_signal_set[index].rule_int=m_rule_interval.GetListViewPointer().SelectedItemIndex(); class=class="str">"cmt">//--- Comparison type m_signal_set[index].rule_type=m_rule_type.GetListViewPointer().SelectedItemIndex(); class=class="str">"cmt">//--- Rule value m_signal_set[index].rule_value1=(class="type">class="kw">double)m_rule_value[class="num">0].GetValue(); m_signal_set[index].rule_value2=(class="type">class="kw">double)m_rule_value[class="num">1].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">//--- Backdrop class="type">class="kw">color if(m_set_param[class="num">0].IsPressed()) m_signal_set[index].back_color=m_color_button[class="num">1].CurrentColor(); else m_signal_set[index].back_color=clrNONE; class=class="str">"cmt">//--- Border class="type">class="kw">color if(m_set_param[class="num">1].IsPressed()) m_signal_set[index].border_color=m_color_button[class="num">2].CurrentColor(); else m_signal_set[index].border_color=clrNONE; class=class="str">"cmt">//--- Hint value m_signal_set[index].tooltip=m_set_param[class="num">2].IsPressed(); if(m_signal_set[index].tooltip) StringToCharArray(m_tooltip_text.GetValue(),m_signal_set[index].tooltip_text); class=class="str">"cmt">//--- Selected image m_signal_set[index].image=m_set_param[class="num">3].IsPressed(); if(m_signal_set[index].image) m_signal_set[index].img_index=m_pictures_slider.GetRadioButtonsPointer().SelectedButtonIndex(); class=class="str">"cmt">//--- Selected timegrames class="type">int tf=class="num">0; for(class="type">int i=class="num">0; i<class="num">21; i++) { if(!m_tf_button[i].IsLocked() && m_tf_button[i].IsPressed()) { m_signal_set[index].timeframes[i]=true; StringToCharArray(m_tf_button[i].LabelText(),m_signal_set[index].tf_name[i].tf); tf++; } else m_signal_set[index].timeframes[i]=false; } class=class="str">"cmt">//--- if(tf<class="num">1) { if(TerminalInfoString(TERMINAL_LANGUAGE)=="Russian") MessageBox("Не выбран ни один таймфрейм","Монитор сигналов"); else MessageBox("No timeframes selected","Signal Monitor"); FileClose(h); class="kw">return(false); } class=class="str">"cmt">//--- FileWriteStruct(h,m_signal_set[index]); FileClose(h); Print("Configuration "+m_signal_name.GetValue()+" successfully saved"); class=class="str">"cmt">//--- class="kw">return(true); }
◍ 信号名查重与编辑入口的实现细节
信号面板里最容易被忽视的一环,是新信号命名不能和已加载的 SET 文件撞车。CheckSignalNames 先扫一遍 Signal Monitor 目录下的 signal_0.bin ~ signal_(SIMPLE_SIGNALS-1).bin,用 FileIsExist 统计实际存在的配置文件数量 cnt;若 cnt<1 说明还没任何信号集,直接放行返回 true。 当已有配置时,函数把 signal_set 数组按 cnt 重置尺寸并清零,再逐个用 FileOpen(FILE_READ|FILE_BIN) 读入结构体。关键判定在 CharArrayToString(m_signal_set[i].signal_name)==name:只要传进来的 name 和某一个已存信号名相等,就关句柄并返回 false,阻止重名。 EditSimpleSignal 负责把界面控件 ID 映射到信号槽位。它遍历 SIMPLE_SIGNALS,当 lparam 匹配 m_signal_ind[i].Id() 时,调 LoadSignalSet(i) 载入该槽配置,把新建按钮文案切成多语言项 m_lang[38],弹开 m_set_window 并记 m_number_signal=i,后续编辑就锁定在这个索引上。 外汇与贵金属信号监控涉及杠杆与滑点,实盘前务必在 MT5 策略测试器用历史数据跑一遍文件读写逻辑,确认 bin 路径在你的 terminal 数据目录下真实存在,否则 INVALID_HANDLE 会直接弹窗中断。
class="type">bool CProgram::CheckSignalNames(class="type">class="kw">string name) { 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<SIMPLE_SIGNALS; i++) { if(FileIsExist("Signal Monitor\\signal_"+class="type">class="kw">string(i)+".bin")) cnt++; } if(cnt<class="num">1) class="kw">return(true); 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); if(h==INVALID_HANDLE) { MessageBox("Configuration not found","Signal Monitor"); class="kw">return(false); } FileReadStruct(h,signal_set[i]); if(CharArrayToString(m_signal_set[i].signal_name)==name) { FileClose(h); class="kw">return(false); } FileClose(h); } class="kw">return(true); } class="type">void CProgram::EditSimpleSignal(class="type">long lparam) { for(class="type">int i=class="num">0;i<SIMPLE_SIGNALS; i++) { if(lparam==m_signal_ind[i].Id()) { LoadSignalSet(i); m_new_signal.LabelText(m_lang[class="num">38]); m_new_signal.Update(true); m_set_window.OpenWindow(); m_number_signal=i; } } } class="type">void CProgram::SignalSwitch(class="type">long lparam) { for(class="type">int i=class="num">0;i<SIMPLE_SIGNALS; i++) { class=class="str">"cmt">//--- if(lparam==m_signal_type[i].Id()) {
「信号按钮的循环配色逻辑」
这段片段在做一件事:根据信号标签当前文字,把按钮重绘成对应的颜色与下一状态字符。它出现在自定义 GUI 的循环里,用来在 S(卖)、B(买)、C(平仓)之间做视觉轮换。 具体判断链是:若标签为 "S",调用 SetButtonParam 把按钮设成 "C" 并填充 clrMediumOrchid(中 orchid 紫);若为 "C",设成 "B" 并填 C'240,120,0'(橙棕);若为 "B",设成 "S" 并填 C'75,190,240'(浅蓝)。这种写法把状态机直接焊进了界面刷新循环,改一处配色就要同步动三行。 在 CProgram::CreateCompositeEdit 里能看到另一个落点:复合信号编辑窗被定为 590×590 像素、标题栏高 22、可移动、字体与背景走基类变量。开 MT5 把 XSize/YSize 改成 400 验证一下,窗体就会明显缩小,方便多屏盯盘时并排摆。外汇与贵金属 GUI 自动化改造属高风险操作,参数误写可能导致面板错乱。
if(m_signal_type[i].LabelText()=="S") SetButtonParam(m_signal_type[i],"C",clrMediumOrchid); else if(m_signal_type[i].LabelText()=="C") SetButtonParam(m_signal_type[i],"B",C&class="macro">#x27;class="num">240,class="num">120,class="num">0&class="macro">#x27;); else if(m_signal_type[i].LabelText()=="B") SetButtonParam(m_signal_type[i],"S",C&class="macro">#x27;class="num">75,class="num">190,class="num">240&class="macro">#x27;); }
把复合信号编辑器接进 GUI 主流程
在 MT5 的自定义指标或 EA 里搭图形界面,最怕某个子窗口创建失败却没人拦住,后面全盘崩溃。上面这段把复合信号编辑窗的创建塞进 CreateGUI 的总装环节,用连续 if 返回 false 做短路保护:只要 CreateCompositeEdit(m_lang[46]) 这一步没过,整个 GUI 初始化直接中止。 注意 CreateCompositeEdit 内部先调 m_composite_edit.CreateWindow(m_chart_id,m_subwin,caption_text,x,y),传入图表 ID、子窗口序号和标题文本;窗口建不出来就返回 false,外层立即 return(false),不会继续往下跑 CWndEvents::CompletedGUI()。 OpenCompositeSignalEditor 只认 lparam==m_add_signal[1].Id() 这一事件,命中才 m_composite_edit.OpenWindow() 弹出编辑面板。也就是说,按钮事件和具体信号槽位[1]是硬绑定的,改信号顺序得同步改这里。 CreateCSignalName 给文本编辑框定了实参数:XSize(110)、YSize(24),内部文本框 XGap(110)、XSize(200),默认字色 clrSilver,并 CWndContainer::AddToElementsArray(4,text_edit) 归到第 4 组对象数组。CreateSimpleSignal 则把按钮底色 baseclr=clrDodgerBlue、按下色 pressed=clrCrimson,尺寸锁死 110×40。外汇与贵金属图表加载这类自定义 GUI 属于高风险的客户端扩展行为,参数乱改可能导致终端卡死或图表资源泄漏,建议在模拟盘 MT5 先单步验证。
if(!m_composite_edit.CreateWindow(m_chart_id,m_subwin,caption_text,x,y)) class="kw">return(false); class="kw">return(true); } class="type">bool CProgram::CreateGUI(class="type">void) { class=class="str">"cmt">//--- ChangeLanguage(); class=class="str">"cmt">//--- Step class="num">1-class="num">3. Symbol selection window. if(!CreateStepWindow(m_lang[class="num">0])) class="kw">return(false); class=class="str">"cmt">//--- if(!CreateSetWindow(m_lang[class="num">17])) class="kw">return(false); class=class="str">"cmt">//--- Creating form class="num">2 for the class="type">class="kw">color picker if(!CreateColorWindow(m_lang[class="num">39])) class="kw">return(false); class=class="str">"cmt">//--- Creating a quick edit form if(!CreateFastEdit(m_lang[class="num">40])) class="kw">return(false); class=class="str">"cmt">//--- if(!CreateCompositeEdit(m_lang[class="num">46])) class="kw">return(false); class=class="str">"cmt">//--- Complete GUI creation CWndEvents::CompletedGUI(); class="kw">return(true); } class="type">void CProgram::OpenCompositeSignalEditor(class="type">long lparam) { if(lparam==m_add_signal[class="num">1].Id()) { m_composite_edit.OpenWindow(); } } class="type">bool CProgram::CreateCSignalName(CTextEdit &text_edit,const class="type">int x_gap,const class="type">int y_gap) { class=class="str">"cmt">//--- Store the pointer to the main control text_edit.MainPointer(m_composite_edit); class=class="str">"cmt">//--- Properties text_edit.XSize(class="num">110); text_edit.YSize(class="num">24); text_edit.Font(m_base_font); text_edit.FontSize(m_base_font_size); text_edit.GetTextBoxPointer().XGap(class="num">110); text_edit.GetTextBoxPointer().XSize(class="num">200); text_edit.GetTextBoxPointer().DefaultTextColor(clrSilver); text_edit.GetTextBoxPointer().DefaultText(m_lang[class="num">45]); class=class="str">"cmt">//--- Create a control element if(!text_edit.CreateTextEdit(m_lang[class="num">45],x_gap,y_gap)) class="kw">return(false); class=class="str">"cmt">//--- Add the object to the common array of object groups CWndContainer::AddToElementsArray(class="num">4,text_edit); class="kw">return(true); } class="type">bool CProgram::CreateSimpleSignal(CButton &button,const class="type">int x_gap,const class="type">int y_gap) { class=class="str">"cmt">//--- class="type">class="kw">color baseclr=clrDodgerBlue; class="type">class="kw">color pressed=clrCrimson; class=class="str">"cmt">//--- Store the window pointer button.MainPointer(m_composite_edit); class=class="str">"cmt">//--- Set properties before creation button.XSize(class="num">110); button.YSize(class="num">40); button.Font(m_base_font); button.FontSize(m_base_font_size); button.BackColor(baseclr);