基于 Merill(梅里尔) 形态的策略构建器·综合运用
(3/3)·从信号分区到自定义指标接入,手把手把图形界面变成可测的交易策略工厂
接上篇对梅里尔形态在多品种与指标上应用的铺垫,这一篇直接进到落地环节。很多交易者卡在“形态看明白了却没法系统验证”这一步,手动切图表、记信号、算胜率既慢又容易漏。我们用一个带构造器与选项卡的界面,把信号生成和回测压缩进同一工作流。
◍ 下拉框里塞进均线类型与成交量源
在 MT5 自定义面板里做指标设置控件时,最省事的办法是把可选参数直接做成下拉组合框。下面两段函数分别处理了均线算法和成交量来源两个设置项,读者可以照抄到自己的 EA 面板工程里。 IndicatorSetting1 给组合框预置了 4 个均线类型:Simple、Exponential、Smoothed、Linear weighted,对应 ENUM_MA_METHOD 的常用取值。控件本体宽 200、高 25,字号 12,标签色用了 C'0,100,255' 这种 RGB 蓝,按钮区宽 100、字号 10、背景 clrAliceBlue。 IndicatorSetting3 则只放 2 项:Tick volume 与 Real Volume,这是多数券商 MT5 行情里能切的成交量口径。它和前一个函数结构完全一致,ItemsTotal 改成 2,循环上界也跟着变成 2。外汇与贵金属杠杆高,Real Volume 在某些品种上可能不推送,切过去容易空值。 两个函数都把控件挂到 m_tabs1 的标签索引 1 上,并通过 CWndContainer 加进全局元素数组。开 MT5 把这段编进你的 CProgram 类,编译后点开面板第二页就能看到下拉框直接选参数。
class="type">bool CProgram::IndicatorSetting1(const class="type">int x_gap,const class="type">int y_gap,const class="type">class="kw">string text) { class=class="str">"cmt">//--- Pass the object to the panel m_ind_set1.MainPointer(m_tabs1); class=class="str">"cmt">//--- Attach to tab m_tabs1.AddToElementsArray(class="num">1,m_ind_set1); class=class="str">"cmt">//--- Array of the item values in the list view class="type">class="kw">string pattern_names[class="num">4]= { "Simple","Exponential","Smoothed","Linear weighted" }; class=class="str">"cmt">//--- Set properties before creation m_ind_set1.XSize(class="num">200); m_ind_set1.YSize(class="num">25); m_ind_set1.ItemsTotal(class="num">4); m_ind_set1.FontSize(class="num">12); m_ind_set1.LabelColor(C&class="macro">#x27;class="num">0,class="num">100,class="num">255&class="macro">#x27;); CButton *but=m_ind_set1.GetButtonPointer(); but.FontSize(class="num">10); but.XSize(class="num">100); but.BackColor(clrAliceBlue); m_ind_set1.GetListViewPointer().FontSize(class="num">10); class=class="str">"cmt">//--- Save the item values in the combobox list view for(class="type">int i=class="num">0; i<class="num">4; i++) m_ind_set1.SetValue(i,pattern_names[i]); class=class="str">"cmt">//--- Get the list view pointer CListView *lv=m_ind_set1.GetListViewPointer(); class=class="str">"cmt">//--- Set the list view properties lv.LightsHover(true); m_ind_set1.SelectItem(class="num">0); class=class="str">"cmt">//--- Creating a control if(!m_ind_set1.CreateComboBox(text,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">0,m_ind_set1); class="kw">return(true); } class="type">bool CProgram::IndicatorSetting3(const class="type">int x_gap,const class="type">int y_gap,const class="type">class="kw">string text) { class=class="str">"cmt">//--- Pass the object to the panel m_ind_set3.MainPointer(m_tabs1); class=class="str">"cmt">//--- Attach to tab m_tabs1.AddToElementsArray(class="num">1,m_ind_set3); class=class="str">"cmt">//--- Array of the item values in the list view class="type">class="kw">string pattern_names[class="num">2]= { "Tick volume","Real Volume" }; class=class="str">"cmt">//--- Set properties before creation m_ind_set3.XSize(class="num">200); m_ind_set3.YSize(class="num">25); m_ind_set3.ItemsTotal(class="num">2); m_ind_set3.FontSize(class="num">12); m_ind_set3.LabelColor(C&class="macro">#x27;class="num">0,class="num">100,class="num">255&class="macro">#x27;); CButton *but=m_ind_set3.GetButtonPointer(); but.FontSize(class="num">10); but.XSize(class="num">100); but.BackColor(clrAliceBlue); m_ind_set3.GetListViewPointer().FontSize(class="num">10); class=class="str">"cmt">//--- Save the item values in the combobox list view for(class="type">int i=class="num">0; i<class="num">2; i++) m_ind_set3.SetValue(i,pattern_names[i]); class=class="str">"cmt">//--- Get the list view pointer
「给下拉框塞进K线字段与语种」
在 MT5 自定义面板里,ComboBox 的列表项必须在 CreateComboBox 之前写进去,否则运行时下拉是空的。下面两段分别构造了指标字段选择框和语言选择框,核心差异在 ItemsTotal 与列表数组长度必须一致。 以 m_ind_set4 为例,先声明 string pattern_names[4] 存 "Open","Close","High","Low",随后 m_ind_set4.ItemsTotal(4) 告诉控件预留 4 行;若这里填 3 而数组有 4 项,最后一项不会被渲染。ListView 的 YSize 设为 82 时,配合 ItemYSize(20) 刚好露出约 4 行(82/20≈4.1),Hover 高亮 LightsHover(true) 能让鼠标所在项反色。 语言框 m_language_set 只放 "Русский" 与 "English" 两项,ItemsTotal 对应填 2;这种硬绑定意味着想加中文得改数组长度与 ItemsTotal 两处,漏一个就错位。外汇与贵金属图表加载此类面板属低风险 UI 操作,但误改 ItemsTotal 可能引发下标越界导致 EA 初始化失败。 让小布替你跑这套 把两段代码分别粘进 CProgram 成员函数,编译后切到 tab 索引 1,能看到字段框默认选中索引 1(Close)、语言框默认选中 0(俄文),说明 SelectItem 的入参直接决定初始落点。
class="type">bool CProgram::IndicatorSetting4(const class="type">int x_gap,const class="type">int y_gap,const class="type">class="kw">string text) { class=class="str">"cmt">//--- Pass the object to the panel m_ind_set4.MainPointer(m_tabs1); class=class="str">"cmt">//--- Attach to tab m_tabs1.AddToElementsArray(class="num">1,m_ind_set4); class=class="str">"cmt">//--- Array of the item values in the list view class="type">class="kw">string pattern_names[class="num">4]= { "Open","Close","High","Low" }; class=class="str">"cmt">//--- Set properties before creation m_ind_set4.XSize(class="num">200); m_ind_set4.YSize(class="num">25); m_ind_set4.ItemsTotal(class="num">4); m_ind_set4.FontSize(class="num">12); m_ind_set4.LabelColor(C&class="macro">#x27;class="num">0,class="num">100,class="num">255&class="macro">#x27;); CButton *but=m_ind_set4.GetButtonPointer(); but.FontSize(class="num">10); but.XSize(class="num">100); but.BackColor(clrAliceBlue); m_ind_set4.GetListViewPointer().FontSize(class="num">10); class=class="str">"cmt">//--- Save the item values in the combobox list view for(class="type">int i=class="num">0; i<class="num">4; i++) m_ind_set4.SetValue(i,pattern_names[i]); class=class="str">"cmt">//--- Get the list view pointer CListView *lv=m_ind_set4.GetListViewPointer(); class=class="str">"cmt">//--- Set the list view properties lv.LightsHover(true); lv.ItemYSize(class="num">20); lv.YSize(class="num">82); m_ind_set4.SelectItem(class="num">1); class=class="str">"cmt">//--- Creating a control if(!m_ind_set4.CreateComboBox(text,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">0,m_ind_set4); class="kw">return(true); } class="type">bool CProgram::LanguageSetting(const class="type">int x_gap,const class="type">int y_gap,const class="type">class="kw">string text) { class=class="str">"cmt">//--- Pass the object to the panel m_language_set.MainPointer(m_tabs1); class=class="str">"cmt">//--- Attach to tab m_tabs1.AddToElementsArray(class="num">1,m_language_set); class=class="str">"cmt">//--- Array of the item values in the list view class="type">class="kw">string pattern_names[class="num">2]= { "Русский","English" }; class=class="str">"cmt">//--- Set properties before creation m_language_set.XSize(class="num">200);
下拉框与文本框的控件落地细节
在 MT5 自定义面板里,组合框 m_language_set 先被硬性设定为 2 个条目、字体 12、标签色 C'0,100,255',按钮字号压到 10、宽 100、背景 clrAliceBlue,列表视图行高 20、总高 42,默认选中第 1 项(索引从 0 计)。这些数值直接决定面板渲染时的占位与 hover 高亮表现,改一处就可能让控件重叠。 文本编辑框 CreateCustomEdit 把 XSize 锁在 100、YSize 24,内部 TextBox 却拉到 325 宽、左间隙 1、默认字色 clrSilver,并开启 AutoSelectionMode(true)。如果你要做多语言备注输入框,把 default_text 传进函数即可,不用重写创建逻辑。 点击事件走 CHARTEVENT_CUSTOM+ON_CLICK_COMBOBOX_ITEM,在回调里用 lparam 调 ChangeLanguage 并触发 Update(true) 重绘。ChangeLanguage 内部只认 m_language_set.Id() 匹配,否则直接返回 false,索引 0 对应俄文“Конструктор”标签——说明这套界面至少内置了双语切换骨架。 外汇与贵金属 EA 面板开发属高风险改造,参数误写可能导致图表对象错乱,建议在策略测试器外的独立图表先验证控件树。
m_language_set.YSize(class="num">25); m_language_set.ItemsTotal(class="num">2); m_language_set.FontSize(class="num">12); m_language_set.LabelColor(C&class="macro">#x27;class="num">0,class="num">100,class="num">255&class="macro">#x27;); CButton *but=m_language_set.GetButtonPointer(); but.FontSize(class="num">10); but.XSize(class="num">100); but.BackColor(clrAliceBlue); but.XGap(class="num">140); m_language_set.GetListViewPointer().FontSize(class="num">10); class=class="str">"cmt">//--- Save the item values in the combobox list view for(class="type">int i=class="num">0; i<class="num">2; i++) m_language_set.SetValue(i,pattern_names[i]); class=class="str">"cmt">//--- Get the list view pointer CListView *lv=m_language_set.GetListViewPointer(); class=class="str">"cmt">//--- Set the list view properties lv.LightsHover(true); lv.ItemYSize(class="num">20); lv.YSize(class="num">42); m_language_set.SelectItem(class="num">1); class=class="str">"cmt">//--- Creating a control if(!m_language_set.CreateComboBox(text,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">0,m_language_set); class="kw">return(true); } class="type">bool CProgram::CreateCustomEdit(CTextEdit &text_edit,const class="type">int x_gap,const class="type">int y_gap,const class="type">class="kw">string default_text) { class=class="str">"cmt">//--- Save the pointer to the main control text_edit.MainPointer(m_tabs1); class=class="str">"cmt">//--- Attach to tab m_tabs1.AddToElementsArray(class="num">1,text_edit); class=class="str">"cmt">//--- Properties text_edit.XSize(class="num">100); text_edit.YSize(class="num">24); text_edit.LabelColor(C&class="macro">#x27;class="num">0,class="num">100,class="num">255&class="macro">#x27;); CTextBox *box=text_edit.GetTextBoxPointer(); box.AutoSelectionMode(true); box.XSize(class="num">325); box.XGap(class="num">1); box.DefaultTextColor(clrSilver); box.DefaultText(default_text); class=class="str">"cmt">//--- Creating a control if(!text_edit.CreateTextEdit("",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">0,text_edit); class="kw">return(true); } class=class="str">"cmt">//--- Selection of a combo box item if(id==CHARTEVENT_CUSTOM+ON_CLICK_COMBOBOX_ITEM) { class=class="str">"cmt">//--- Changing the interface language if(ChangeLanguage(lparam)) Update(true); } class="type">bool CProgram::ChangeLanguage(const class="type">long id) { class=class="str">"cmt">//--- Check the element ID if(id!=m_language_set.Id()) class="kw">return(false); m_lang_index=m_language_set.GetListViewPointer().SelectedItemIndex(); class=class="str">"cmt">//--- if(m_lang_index==class="num">0) { class=class="str">"cmt">//--- Constructor tab m_tabs1.Text(class="num">0,"Конструктор");
◍ 面板多语言标签的初始化写法
在 MT5 自定义分析面板里,界面文案的本地化通常在构造阶段一次性赋值。上面这段逻辑用 m_language_set 的值分流:俄语分支把标签页、表头、按钮、报表行全部写成西里尔字符串;英语分支则换成对应英文。
循环里 8 个复选框的命名有规律:前 2 个标为 Pattern,中间 3 个(索引 2~4)显示为 Signal 1~3,后 3 个(索引 5~7)显示为 Signal 1~3——注意这里用 i-1 和 i-4 做偏移,所以索引 5 输出的也是 Signal 1 而非 Signal 4,复制时容易抄错。
报表区固定 6 行文本:总交易数、空单数、盈利单数、盈利点数、多单数、亏损单数,初始值统一拼了「-」占位,等回测跑完再刷新。外汇与贵金属波动剧烈,这类面板只是辅助统计,实盘前务必在策略测试器里用历史数据验证标签与统计是否对得上。
自定义指标那块留了两个文本框默认值:一个填指标路径,一个填逗号分隔的参数,等于强制用户自己接数据源,没写死任何内置品种。
m_tabs1.Text(class="num">1,"Настройки"); m_table_symb.SetHeaderText(class="num">0,"Символ"); m_request.LabelText("Поиск"); m_date_range.LabelText("Диапазон дат"); m_timeframe1.LabelText("Таймфрейм"); for(class="type">int i=class="num">0; i<class="num">8; i++) { if(i<class="num">2) m_checkbox[i].LabelText("Паттерн"); else if(i>=class="num">2 && i<class="num">5) m_checkbox[i].LabelText("Сигнал "+class="type">class="kw">string(i-class="num">1)); else if(i>=class="num">5) m_checkbox[i].LabelText("Сигнал "+class="type">class="kw">string(i-class="num">4)); } m_takeprofit1.LabelText("Тейк Профит"); m_takeprofit2.LabelText("Тейк Профит"); m_stoploss1.LabelText("Стоп Лосс"); m_stoploss2.LabelText("Стоп Лосс"); m_frame[class="num">2].GetTextLabelPointer().LabelText("Отчёт"); class="type">class="kw">string report_label[class="num">6]= { "Всего трейдов: ","Короткие трейды: ","Прибыльные трейды: ", "Прибыль в пунктах: ","Длинные трейды: ","Убыточные трейды: " }; for(class="type">int i=class="num">0; i<class="num">6; i++) m_report_text[i].LabelText(report_label[i]+"-"); class=class="str">"cmt">//--- Settings tab m_frame[class="num">0].GetTextLabelPointer().LabelText("Настройки стандартных индикаторов"); m_frame[class="num">1].GetTextLabelPointer().LabelText("Настройки кастомных индикаторов"); m_custom_buffer.LabelText("Номер буфера"); m_custom_path.GetTextBoxPointer().DefaultText("Введите адрес индикатора"); m_custom_param.GetTextBoxPointer().DefaultText("Введите параметры индикатора через запятую"); m_language_set.LabelText("Язык интерфейса"); class=class="str">"cmt">//--- Date Range window m_window[class="num">1].LabelText("Настройки диапазона дат"); m_time_edit1.LabelText("Время"); m_time_edit2.LabelText("Время"); m_time_edit3.LabelText("Время"); m_time_edit4.LabelText("Время"); m_status_bar.SetValue(class="num">0,"Не выбран символ для анализа"); } else { class=class="str">"cmt">//--- Constructor tab m_tabs1.Text(class="num">0,"Constructor"); m_tabs1.Text(class="num">1,"Settings"); m_table_symb.SetHeaderText(class="num">0,"Symbol"); m_request.LabelText("Search"); m_date_range.LabelText("Date range"); m_timeframe1.LabelText("Timeframe"); for(class="type">int i=class="num">0; i<class="num">8; i++) {
「面板标签与数据请求的初始化落点」
这段初始化逻辑把 GUI 控件的静态文字一次性铺好,并定义了点击按钮后的符号表刷新动作。复选框按索引分段命名:前 2 个叫 Pattern,索引 2~4 显示 Signal 1~3,索引 ≥5 则显示 Signal 1~2(i-4),总共至少 6 个复选框在跑。 报告区固定 6 行标签:Total trades、Short Trades、Profit Trades、Profit in points、Long Trades、Loss Trades,初始值统一补“-”占位,说明统计模块在用户触发前不预填。 日期范围窗口挂了 4 个 Time 编辑框(m_time_edit1~4),状态栏默认写“No symbol selected for analysis”——没选品种时面板不会瞎算。外汇与贵金属品种在此类回测面板上波动剧烈,未选符号就跑数据可能得到无意义样本。 点击按钮走 CHARTEVENT_CUSTOM+ON_CLICK_BUTTON,命中 m_request.Id() 后先 Hide 符号表,GetSymbols 按过滤条件抓品种,RebuildingTables 重排,再 Show。你在 MT5 里改 m_symb_filter 的传入值,就能直接控制表格里出现哪些可分析对象。
if(i<class="num">2) m_checkbox[i].LabelText("Pattern"); else if(i>=class="num">2 && i<class="num">5) m_checkbox[i].LabelText("Signal "+class="type">class="kw">string(i-class="num">1)); else if(i>=class="num">5) m_checkbox[i].LabelText("Signal "+class="type">class="kw">string(i-class="num">4)); } m_takeprofit1.LabelText("Take Profit"); m_takeprofit2.LabelText("Take Profit"); m_stoploss1.LabelText("Stop Loss"); m_stoploss2.LabelText("Stop Loss"); m_frame[class="num">2].GetTextLabelPointer().LabelText("Report"); class="type">class="kw">string report_label[class="num">6]= { "Total trades: ","Short Trades: ","Profit Trades: ", "Profit in points: ","Long Trades: ","Loss Trades: " }; for(class="type">int i=class="num">0; i<class="num">6; i++) m_report_text[i].LabelText(report_label[i]+"-"); class=class="str">"cmt">//--- Settings tab m_frame[class="num">0].GetTextLabelPointer().LabelText("Standard Indicator Settings"); m_frame[class="num">1].GetTextLabelPointer().LabelText("Custom Indicator Settings"); m_custom_buffer.LabelText("Buffer number"); m_custom_path.GetTextBoxPointer().DefaultText("Enter the indicator path"); m_custom_param.GetTextBoxPointer().DefaultText("Enter indicator parameters separated by commas"); m_language_set.LabelText("Interface language"); class=class="str">"cmt">//--- Date Range window m_window[class="num">1].LabelText("Date Range Settings"); m_time_edit1.LabelText("Time"); m_time_edit2.LabelText("Time"); m_time_edit3.LabelText("Time"); m_time_edit4.LabelText("Time"); m_status_bar.SetValue(class="num">0,"No symbol selected for analysis"); } class="kw">return(true); } class=class="str">"cmt">//--- Button click event if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON) { class=class="str">"cmt">//--- Requesting data RequestData(lparam); class="type">bool CProgram::RequestData(const class="type">long id) { if(id==m_request.Id()) { m_table_symb.Hide(); GetSymbols(m_symb_filter); RebuildingTables(m_table_symb); m_table_symb.Show(); } class="kw">return(true); }
面板交互怎么把点击事件转成分析动作
MT5 自定义面板的核心不是画界面,而是把图表事件接进逻辑。当 id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON 且 lparam 等于日期区间控件 ID 时,代码会把二级窗口定位到按钮下方并弹出“Date Range Settings”标签,这一步决定了用户后续能否改分析区间。
列表点击走另一条路:ON_CLICK_LIST_ITEM 里先比对 lparam 与品种表的 ID,若 SelectedItem()==WRONG_VALUE 就在状态栏写“未选品种”,否则用 SymbolInfoString(symbol,SYMBOL_DESCRIPTION) 把完整描述刷进状态栏,再调用 GetResult(symbol) 进入计算。
GetResult 里把日历和时间编辑框拼成 datetime:起始 = 日历1日期 + 时:分:00,结束同理。若起始晚于结束、或结束大于 TimeCurrent(),会弹 MessageBox 报错(俄语或英语取决于 m_lang_index)。外汇与贵金属波动剧烈,这类日期校验能避免拿未来时间跑回测导致伪信号。
在 MT5 里验证时,重点看 lparam 比对是否用控件自身 .Id() 而不是硬编码数字——硬编码会在界面控件顺序变动后静默失效。
if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON) { class=class="str">"cmt">//--- if(lparam==m_date_range.Id()) { class="type">int x=m_date_range.X(); class="type">int y=m_date_range.Y()+m_date_range.YSize(); m_window[class="num">1].X(x); m_window[class="num">1].Y(y); m_window[class="num">1].OpenWindow(); class="type">class="kw">string val=(m_lang_index==class="num">0)?"Настройки диапазона дат":"Date Range Settings"; m_window[class="num">1].LabelText(val); } } class=class="str">"cmt">//--- Event of pressing on a list or table item if(id==CHARTEVENT_CUSTOM+ON_CLICK_LIST_ITEM) { class=class="str">"cmt">//--- Select a symbol for further work class=class="str">"cmt">//--- Check the element ID if(lparam==m_table_symb.Id()) { class=class="str">"cmt">//--- Exit if the line is not selected if(m_table_symb.SelectedItem()==WRONG_VALUE) { class=class="str">"cmt">//--- Show the full symbol description in the status bar m_status_bar.SetValue(class="num">0,"Не выбран символ для анализа"); m_status_bar.GetItemPointer(class="num">0).Update(true); } class=class="str">"cmt">//--- Get a selected symbol class="type">class="kw">string symbol=m_table_symb.GetValue(class="num">0,m_table_symb.SelectedItem()); class=class="str">"cmt">//--- Show the full symbol description in the status bar m_status_bar.SetValue(class="num">0,"Selected symbol: "+::SymbolInfoString(symbol,SYMBOL_DESCRIPTION)); m_status_bar.GetItemPointer(class="num">0).Update(true); GetResult(symbol); } } class="type">void CProgram::GetResult(const class="type">class="kw">string symbol) { class=class="str">"cmt">//--- Get the date range m_start_date=StringToTime(TimeToString(m_calendar1.SelectedDate(),TIME_DATE)+" "+(class="type">class="kw">string)m_time_edit1.GetHours()+":"+(class="type">class="kw">string)m_time_edit1.GetMinutes()+":class="num">00"); m_end_date=StringToTime(TimeToString(m_calendar2.SelectedDate(),TIME_DATE)+" "+(class="type">class="kw">string)m_time_edit2.GetHours()+":"+(class="type">class="kw">string)m_time_edit2.GetMinutes()+":class="num">00"); class=class="str">"cmt">//--- Check specified dates if(m_start_date>m_end_date || m_end_date>TimeCurrent()) { if(m_lang_index==class="num">0) MessageBox("Неправильно выбран диапазон дат!","Ошибка",MB_OK); else if(m_lang_index==class="num">1)
◍ 回测循环里的信号互斥与日期步进
面板里买、卖图案若选成同一个,程序会直接弹窗拦截并退出,俄文与英文提示按 m_lang_index 切换,逻辑上杜绝自相矛盾的开仓条件。外汇与贵金属这类高杠杆品种,历史上因参数冲突导致双向持仓的案例不少,先卡死这种低级错误比事后止损更划算。 核心扫描段用 while(cur_date<m_end_date) 把起止日期之间的 K 线逐根推过去:三组 BuySignal 任一为真就跑 CalculateBuyDeals,三组 SellSignal 任一为真就跑 CalculateSellDeals,命中后把 cur_date 重置回 m_start_date 并 continue,未命中才用 PeriodSeconds(StringToTimeframe(tf)) 往后跳一根。 [CODE]片段里的信号开关很直白:BuySignal 首行就查 m_checkbox[0].IsPressed(),没勾选买信号直接返回 false;后续按 m_checkbox[signal+1] 决定是否启用对应序号的买条件。你在 MT5 里接这套逻辑时,可以把 checkbox 索引打印出来核对,避免界面顺序和数组下标对不上。 让小布替你跑这套 把 m_start_date 和 m_end_date 间距先压到 200 根内做肉眼验证,确认信号触发次数和图表标记一致,再放开到全历史;否则 PeriodSeconds 跳帧一旦写错,回测会静默漏掉大段行情。
MessageBox("Incorrect date range selected!","Error",MB_OK); class="kw">return; } class=class="str">"cmt">//--- Проверка выбора паттернов class="type">int buy_pat=m_combobox1.GetListViewPointer().SelectedItemIndex(); class="type">int sell_pat=m_combobox2.GetListViewPointer().SelectedItemIndex(); if(buy_pat==sell_pat) { if(m_lang_index==class="num">0) MessageBox("Паттерн на покупку и продажу не может быть одинаков!","Ошибка",MB_OK); else if(m_lang_index==class="num">1) MessageBox("The pattern for buying and selling cannot be the same!","Error",MB_OK); class="kw">return; } class=class="str">"cmt">//--- ZeroMemory(m_report); class="type">class="kw">datetime cur_date=m_start_date; class="type">class="kw">string tf=m_timeframe1.GetListViewPointer().SelectedItemText(); class="type">int applied1=m_applied1.GetListViewPointer().SelectedItemIndex(); class="type">int applied2=m_applied2.GetListViewPointer().SelectedItemIndex(); class="type">int applied3=m_applied3.GetListViewPointer().SelectedItemIndex(); class="type">int applied4=m_applied4.GetListViewPointer().SelectedItemIndex(); class="type">int applied5=m_applied5.GetListViewPointer().SelectedItemIndex(); class="type">int applied6=m_applied6.GetListViewPointer().SelectedItemIndex(); class=class="str">"cmt">//--- while(cur_date<m_end_date) { if( BuySignal(symbol,m_start_date,applied1,class="num">1) || BuySignal(symbol,m_start_date,applied2,class="num">2) || BuySignal(symbol,m_start_date,applied3,class="num">3)) { CalculateBuyDeals(symbol,m_start_date); cur_date=m_start_date; class="kw">continue; } if( SellSignal(symbol,m_start_date,applied4,class="num">1) || SellSignal(symbol,m_start_date,applied5,class="num">2) || SellSignal(symbol,m_start_date,applied6,class="num">3)) { CalculateSellDeals(symbol,m_start_date); cur_date=m_start_date; class="kw">continue; } m_start_date+=PeriodSeconds(StringToTimeframe(tf)); cur_date=m_start_date; } class=class="str">"cmt">//--- Output the report PrintReport(); } class="type">bool CProgram::BuySignal(const class="type">class="kw">string symbol,class="type">class="kw">datetime start,class="type">int applied,class="type">int signal) { class=class="str">"cmt">//--- Exit if the buy signal is disabled if(!m_checkbox[class="num">0].IsPressed()) class="kw">return(false); class=class="str">"cmt">//--- class="type">int Handle=INVALID_HANDLE; class="type">class="kw">string tf=m_timeframe1.GetListViewPointer().SelectedItemText(); class=class="str">"cmt">//--- Preparing data if(m_checkbox[signal+class="num">1].IsPressed()) { class=class="str">"cmt">//--- Price
「价格序列与指标句柄的分支装配」
这段逻辑把「形态识别」和「技术指标」拆成了两个互不干扰的入口,靠 applied 参数决定走哪条路。applied 为 0 时直接拉最近 5 根 K 线的 open/close/high/low 之一,拼成 POINTS 结构再做模式比对;少于 5 根就直接返回 false,避免在数据不足时误触发。 [CODE] if(applied==0) { MqlRates rt[]; int sl=0,tp=0; POINTS pat; double arr[]; int copied=CopyRates(symbol,StringToTimeframe(tf),m_start_date,5,rt); int app_price=m_ind_set4.GetListViewPointer().SelectedItemIndex(); ArrayResize(arr,copied); //Print(m_start_date+": "+copied); if(copied<5) return(false); //--- for(int i=0; i<copied; i++) { if(app_price==0) arr[i]=rt[i].open; else if(app_price==1) arr[i]=rt[i].close; else if(app_price==2) arr[i]=rt[i].high; else if(app_price==3) arr[i]=rt[i].low; } //--- Pattern search pat.A=arr[0]; pat.B=arr[1]; pat.C=arr[2]; pat.D=arr[3]; pat.E=arr[4]; //--- If the pattern is found, check the signal if(GetPatternType(pat)==m_combobox1.GetListViewPointer().SelectedItemIndex()) { m_start_date=IndexToDate(m_start_date,StringToTimeframe(tf),5); return(true); } return(false); } //--- ATR if(applied==1) Handle=iATR(symbol,StringToTimeframe(tf),int(m_ind_setting[0].GetValue())); //--- CCI if(applied==2) { int app_price; switch(m_ind_set4.GetListViewPointer().SelectedItemIndex()) { case 0: app_price=PRICE_OPEN; break; case 1: app_price=PRICE_CLOSE; break; case 2: [/CODE] 代码里 CopyRates 取数是硬上限 5 根,意味着形态窗口被锁死在 5 根 K 线内;若你做黄金 1 分钟这种跳空频繁的品种,copied<5 的概率会明显抬高,回测时信号次数可能比日线少一个数量级。 applied==1 走 iATR,周期和参数分别来自 StringToTimeframe(tf) 与 m_ind_setting[0],注意这里没做句柄有效性判断,实盘里若 symbol 非法会拿到 INVALID_HANDLE。外汇与贵金属杠杆高,这类隐藏失败分支可能让 EA 在极端行情静默失效,上 MT5 跑之前建议自己补一句 Handle<0 的日志。
if(applied==class="num">0) { class="type">MqlRates rt[]; class="type">int sl=class="num">0,tp=class="num">0; POINTS pat; class="type">class="kw">double arr[]; class="type">int copied=CopyRates(symbol,StringToTimeframe(tf),m_start_date,class="num">5,rt); class="type">int app_price=m_ind_set4.GetListViewPointer().SelectedItemIndex(); ArrayResize(arr,copied); class=class="str">"cmt">//Print(m_start_date+": "+copied); if(copied<class="num">5) class="kw">return(false); class=class="str">"cmt">//--- for(class="type">int i=class="num">0; i<copied; i++) { if(app_price==class="num">0) arr[i]=rt[i].open; else if(app_price==class="num">1) arr[i]=rt[i].close; else if(app_price==class="num">2) arr[i]=rt[i].high; else if(app_price==class="num">3) arr[i]=rt[i].low; } class=class="str">"cmt">//--- Pattern search pat.A=arr[class="num">0]; pat.B=arr[class="num">1]; pat.C=arr[class="num">2]; pat.D=arr[class="num">3]; pat.E=arr[class="num">4]; class=class="str">"cmt">//--- If the pattern is found, check the signal if(GetPatternType(pat)==m_combobox1.GetListViewPointer().SelectedItemIndex()) { m_start_date=IndexToDate(m_start_date,StringToTimeframe(tf),class="num">5); class="kw">return(true); } class="kw">return(false); } class=class="str">"cmt">//--- ATR if(applied==class="num">1) Handle=iATR(symbol,StringToTimeframe(tf),class="type">int(m_ind_setting[class="num">0].GetValue())); class=class="str">"cmt">//--- CCI if(applied==class="num">2) { class="type">int app_price; class="kw">switch(m_ind_set4.GetListViewPointer().SelectedItemIndex()) { case class="num">0: app_price=PRICE_OPEN; break; case class="num">1: app_price=PRICE_CLOSE; break; case class="num">2:
多指标句柄的按需创建逻辑
这段片段展示了一个指标工厂函数里,根据 applied 编号切换不同 MT5 内置指标句柄的写法。CCI 部分先用 switch 把界面下拉索引映射成 PRICE_HIGH / PRICE_LOW / PRICE_CLOSE 三种应用价,默认走收盘价,再调用 iCCI(symbol, StringToTimeframe(tf), int(m_ind_setting[1].GetValue()), app_price) 拿到句柄。 DeMarker 最简单,applied==3 时直接 iDeMarker(symbol, tf周期, int(m_ind_setting[2].GetValue())),周期和参数都来自配置项,没有价格类型分支。 Force Index 的构造最啰嗦:周期取自 m_ind_setting[3],均线方法由 m_ind_set1 列表选中项决定——0/1/2/3 分别对应 SMA、EMA、SMMA、LWMA,越界则回落 SMA;成交量类型由 m_ind_set3 决定,0 是 VOLUME_TICK、1 是 VOLUME_REAL,默认 TICK。最后拼成 iForce(symbol, tf, force_period, force_ma_method, force_applied_volume)。 在 MT5 里跑这套,最容易被忽略的是 SelectedItemIndex 越界保护:若你改了界面列表顺序却没同步 case 数量,Force 指标会静默用 SMA+TICK,回测曲线可能因此偏离预期。外汇与贵金属杠杆高,这类参数映射错误会放大滑点风险,调参前先在策略测试器打印 Handle 返回值确认非 INVALID_HANDLE。
app_price=PRICE_HIGH;
break;
case class="num">3:
app_price=PRICE_LOW;
break;
class="kw">default:
app_price=PRICE_CLOSE;
break;
}
Handle=iCCI(symbol,StringToTimeframe(tf),class="type">int(m_ind_setting[class="num">1].GetValue()),app_price);
}
class=class="str">"cmt">//--- DeMarker
if(applied==class="num">3)
Handle=iDeMarker(symbol,StringToTimeframe(tf),class="type">int(m_ind_setting[class="num">2].GetValue()));
class=class="str">"cmt">//--- Force Index
if(applied==class="num">4)
{
class="type">int force_period=class="type">int(m_ind_setting[class="num">3].GetValue());
ENUM_MA_METHOD force_ma_method;
ENUM_APPLIED_VOLUME force_applied_volume;
class="kw">switch(m_ind_set1.GetListViewPointer().SelectedItemIndex())
{
case class="num">0:
force_ma_method=MODE_SMA;
break;
case class="num">1:
force_ma_method=MODE_EMA;
break;
case class="num">2:
force_ma_method=MODE_SMMA;
break;
case class="num">3:
force_ma_method=MODE_LWMA;
break;
class="kw">default:
force_ma_method=MODE_SMA;
break;
}
class="kw">switch(m_ind_set3.GetListViewPointer().SelectedItemIndex())
{
case class="num">0:
force_applied_volume=VOLUME_TICK;
break;
case class="num">1:
force_applied_volume=VOLUME_REAL;
break;
class="kw">default:
force_applied_volume=VOLUME_TICK;
break;
}
Handle=iForce(symbol,StringToTimeframe(tf),force_period,force_ma_method,force_applied_volume);
}
class=class="str">"cmt">//--- WPR
if(applied==class="num">5)◍ 指标句柄分支与自定义参数上限
在多指标切换的 EA 框架里,applied 变量决定了到底挂载哪一类震荡指标。代码里 applied==6 走 RSI、==7 走 Momentum、==5 对应的 WPR 则在前面段落处理,周期统一由 StringToTimeframe(tf) 转换,参数取自 m_ind_setting 数组的不同下标。 自定义指标(applied==8)最麻烦:先用 StringSplit 按逗号切自定义参数字符串,一旦参数个数超过 20 就弹窗报错并中断,俄语和英文提示由 m_lang_index 切换。这一步直接卡死了外部配置文件里乱填参数的可能。 拿到参数数组后,调用 GetCustomValue 取自定义指标句柄,随后用 CopyBuffer 拉最近 5 根 K 线的数据。若 copied<5,函数直接 return(false),说明数据不足,外层逻辑应放弃本次信号判定。外汇与贵金属波动剧烈,这类句柄失效或复制失败在高波动时段概率会明显上升,实盘前建议在 MT5 策略测试器里故意断网验证 INVALID_HANDLE 分支。
Handle=iWPR(symbol,StringToTimeframe(tf),class="type">int(m_ind_setting[class="num">5].GetValue())); class=class="str">"cmt">//--- RSI if(applied==class="num">6) Handle=iRSI(symbol,StringToTimeframe(tf),class="type">int(m_ind_setting[class="num">4].GetValue()),PRICE_CLOSE); class=class="str">"cmt">//--- Momentum if(applied==class="num">7) Handle=iMomentum(symbol,StringToTimeframe(tf),class="type">int(m_ind_setting[class="num">6].GetValue()),PRICE_CLOSE); class=class="str">"cmt">//--- Custom if(applied==class="num">8) { class="type">class="kw">string str[]; class="type">class="kw">double arr[]; class="type">class="kw">string parameters=m_custom_param.GetValue(); StringSplit(parameters,&class="macro">#x27;,&class="macro">#x27;,str); if(ArraySize(str)>class="num">20) { if(m_lang_index==class="num">0) MessageBox("Количество параметров не должно быть больше class="num">20!","Ошибка",MB_OK); else if(m_lang_index==class="num">1) MessageBox("The number of parameters should not be more than class="num">20!","Error",MB_OK); } ArrayResize(arr,ArraySize(str)); for(class="type">int i=class="num">0; i<ArraySize(str); i++) arr[i]=StringToDouble(str[i]); class="type">class="kw">string name=m_custom_path.GetValue(); Handle=GetCustomValue(StringToTimeframe(tf),name,arr); } class=class="str">"cmt">//--- if(applied>class="num">0) { if(Handle==INVALID_HANDLE) { if(m_lang_index==class="num">0) MessageBox("Не удалось получить хендл индикатора!","Ошибка",MB_OK); else if(m_lang_index==class="num">1) MessageBox("Failed to get indicator handle!","Error",MB_OK); } class="type">class="kw">double arr[]; class="type">int buffer=(applied==class="num">8)?class="type">int(m_custom_buffer.GetValue()):class="num">0; class="type">int copied=CopyBuffer(Handle,buffer,m_start_date,class="num">5,arr); class=class="str">"cmt">//--- class="type">int sl=class="num">0,tp=class="num">0; POINTS pat; if(copied<class="num">5) class="kw">return(false);
「多空回测的退出判定怎么写」
形态命中后,真正的盈亏统计要靠 Buy/Sell 两套独立函数跑。下面这段 MT5 代码把 A~E 五个点从数组读入,再比对下拉框选中的形态类型,命中才把起点时间回退 5 根 K 线并返回 true。 pat.A=arr[0]; pat.B=arr[1]; pat.C=arr[2]; pat.D=arr[3]; pat.E=arr[4]; if(GetPatternType(pat)==m_combobox1.GetListViewPointer().SelectedItemIndex()) { m_start_date=IndexToDate(m_start_date,StringToTimeframe(tf),5); return(true); } return(false); CalculateBuyDeals 里先用 CopyRates 把起止区间拉进 rt[],deal_price 取 copied 索引处的开盘价。循环里只要 (iHigh-deal_price)/_Point 大于等于 TP,就记一笔盈利并 profit+=TP;若 (deal_price-iLow)/_Point 大于等于 SL 则记亏损 profit-=SL。注意这里 TP、SL 单位是 point,外汇和贵金属点值随品种跳动,回测前务必核对 SymbolInfoDouble 的 SYMBOL_POINT。 Sell 方向逻辑对称,但 TP 取自 m_takeprofit2,和 Buy 的 m_takeprofit1 分开存。实盘中外挂这种固定点数止盈止损,遇跳空或点差扩张可能触发偏差,属于高风险验证项,建议先开 MT5 策略测试器用 2023 年 XAUUSD 的 H1 跑一遍看成交分布。
pat.A=arr[class="num">0]; pat.B=arr[class="num">1]; pat.C=arr[class="num">2]; pat.D=arr[class="num">3]; pat.E=arr[class="num">4]; if(GetPatternType(pat)==m_combobox1.GetListViewPointer().SelectedItemIndex()) { m_start_date=IndexToDate(m_start_date,StringToTimeframe(tf),class="num">5); class="kw">return(true); } class="kw">return(false); class="type">void CProgram::CalculateBuyDeals(const class="type">class="kw">string symbol,class="type">class="kw">datetime start) { class="type">MqlRates rt[]; class="type">int TP=class="type">int(m_takeprofit1.GetValue()); class="type">int SL=class="type">int(m_stoploss1.GetValue()); class="type">class="kw">string tf=m_timeframe1.GetListViewPointer().SelectedItemText(); class="type">int copied=CopyRates(symbol,StringToTimeframe(tf),m_start_date,m_end_date,rt); class="type">class="kw">double deal_price=iOpen(symbol,StringToTimeframe(tf),copied); for(class="type">int j=class="num">0; j<copied; j++) { if((iHigh(symbol,StringToTimeframe(tf),copied-j)-deal_price)/SymbolInfoDouble(symbol,SYMBOL_POINT)>=TP) { m_report.profit_trades++; m_report.profit+=TP; m_report.long_trades++; m_report.total_trades++; m_start_date=IndexToDate(m_start_date,StringToTimeframe(tf),j); class="kw">return; } else if((deal_price-iLow(symbol,StringToTimeframe(tf),copied-j))/SymbolInfoDouble(symbol,SYMBOL_POINT)>=SL) { m_report.loss_trades++; m_report.profit-=SL; m_report.long_trades++; m_report.total_trades++; m_start_date=IndexToDate(m_start_date,StringToTimeframe(tf),j); class="kw">return; } } m_start_date=m_end_date; } class="type">void CProgram::CalculateSellDeals(const class="type">class="kw">string symbol,class="type">class="kw">datetime start) { class="type">MqlRates rt[]; class="type">int TP=class="type">int(m_takeprofit2.GetValue());
空单回测与多语报告输出
这段逻辑只处理做空情景:以某根 K 线开盘价 deal_price 进场,之后逐根向前扫描,看价格向下触及 TP 点数或向上触及 SL 点数谁先发生。判定用 (deal_price - iLow)/_Point >= TP 表示空单盈利,用 (iHigh - deal_price)/_Point >= SL 表示空单止损,命中即累加对应统计并回退起始日期退出本轮。 若整段区间都没触发 TP 或 SL,循环跑完会把 m_start_date 直接推到 m_end_date,相当于这段样本被丢弃不计入交易。外汇与贵金属点值随品种跳动,SYMBOL_POINT 必须实时取,硬编码点数会令回测失真,属高危操作。 PrintReport 按 m_lang_index 切换俄语或英文标签,把 total/short/profit/profit_points/long/loss 六项写进界面 Label。想验证可自行把 report_label 改成中文数组,挂 EA 后观察面板输出是否与手动点数统计一致。
class="type">int SL=class="type">int(m_stoploss2.GetValue()); class="type">class="kw">string tf=m_timeframe1.GetListViewPointer().SelectedItemText(); class="type">int copied=CopyRates(symbol,StringToTimeframe(tf),m_start_date,m_end_date,rt); class="type">class="kw">double deal_price=iOpen(symbol,StringToTimeframe(tf),copied); for(class="type">int j=class="num">0; j<copied; j++) { if((deal_price-iLow(symbol,StringToTimeframe(tf),copied-j))/SymbolInfoDouble(symbol,SYMBOL_POINT)>=TP) { m_report.profit_trades++; m_report.profit+=TP; m_report.short_trades++; m_report.total_trades++; m_start_date=IndexToDate(m_start_date,StringToTimeframe(tf),j); class="kw">return; } else if((iHigh(symbol,StringToTimeframe(tf),copied-j)-deal_price)/SymbolInfoDouble(symbol,SYMBOL_POINT)>=SL) { m_report.loss_trades++; m_report.profit-=SL; m_report.short_trades++; m_report.total_trades++; m_start_date=IndexToDate(m_start_date,StringToTimeframe(tf),j); class="kw">return; } } m_start_date=m_end_date; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CProgram::PrintReport(class="type">void) { if(m_lang_index==class="num">0) { class="type">class="kw">string report_label[class="num">6]= { "Всего трейдов: ","Короткие трейды: ","Прибыльные трейды: ", "Прибыль в пунктах: ","Длинные трейды: ","Убыточные трейды: " }; class=class="str">"cmt">//--- m_report_text[class="num">0].LabelText(report_label[class="num">0]+class="type">class="kw">string(m_report.total_trades)); m_report_text[class="num">1].LabelText(report_label[class="num">1]+class="type">class="kw">string(m_report.short_trades)); m_report_text[class="num">2].LabelText(report_label[class="num">2]+class="type">class="kw">string(m_report.profit_trades)); m_report_text[class="num">3].LabelText(report_label[class="num">3]+class="type">class="kw">string(m_report.profit)); m_report_text[class="num">4].LabelText(report_label[class="num">4]+class="type">class="kw">string(m_report.long_trades)); m_report_text[class="num">5].LabelText(report_label[class="num">5]+class="type">class="kw">string(m_report.loss_trades)); } else { class="type">class="kw">string report_label[class="num">6]= { "Total trades: ","Short Trades: ","Profit Trades: ", "Profit in points: ","Long Trades: ","Loss Trades: " }; class=class="str">"cmt">//--- m_report_text[class="num">0].LabelText(report_label[class="num">0]+class="type">class="kw">string(m_report.total_trades));
◍ 把回测统计写进图表标签
在 MT5 策略构建器生成的 EA 里,统计面板往往靠一组文本标签实时刷新。上面这段代码就是典型写法:把多空交易次数、盈利笔数、亏损笔数以及净利润分别拼到预设的 label 后面,再交给 Update(true) 推到图表。 逐行看,m_report_text[1] 对应短单计数 m_report.short_trades,m_report_text[4] 才是长单 m_report.long_trades,顺序和常见「先多后空」的直觉相反,复制时别搞错下标。m_report_text[3] 装的是 m_report.profit,浮点转字符串直接显示,未做四舍五入,小数位长度会随账户币种波动。 想验证很简单:开 MT5 随便加载一个带 CReport 结构的 EA,在 OnInit 里给 report_label 数组填好「空单:」「盈利:」这类前缀,跑一轮历史回测,图表左上角就会随 tick 更新这五项。外汇与贵金属杠杆高,回测统计仅反映过去样本,实盘滑点可能让盈利笔数倾向减少。
m_report_text[class="num">1].LabelText(report_label[class="num">1]+class="type">class="kw">string(m_report.short_trades)); m_report_text[class="num">2].LabelText(report_label[class="num">2]+class="type">class="kw">string(m_report.profit_trades)); m_report_text[class="num">3].LabelText(report_label[class="num">3]+class="type">class="kw">string(m_report.profit)); m_report_text[class="num">4].LabelText(report_label[class="num">4]+class="type">class="kw">string(m_report.long_trades)); m_report_text[class="num">5].LabelText(report_label[class="num">5]+class="type">class="kw">string(m_report.loss_trades)); } Update(true); }
「把文件丢进终端根目录」
随文打包的整套 MQL5 文件必须落到正确路径,否则指标和脚本在 MT5 里根本不会被加载。最省事的做法是把压缩包里的 MQL5 文件夹直接覆盖保存到终端根目录,系统和自定义文件各归各位。 打开根目录不用翻资源管理器:在 MetaTrader 5 里按 Ctrl+Shift+D,或者右键调出关联菜单选对应项,弹出的就是终端根目录窗口(原文图 14 有示意)。这一步做错,后面所有代码都跑不起来。 外汇与贵金属品种波动剧烈、杠杆风险高,部署完先在模拟盘验证脚本行为,再考虑实盘。
一点提醒
把压缩包解进 MQL5 根目录后,若终端「专家」标签页报出 CElement::CreateCanvas 创建画布失败 4016,多半是 Canvas.mqh 第 254 行生成的资源名过长。 原写法把 ChartID 与 GetTickCount()+MathRand() 拼进名称,在 2280 版终端下会超长。删掉随机串、只留 ChartID 即可编译通过,两位用户在 2020 年 1 月的实测都验证了这点。 外汇与贵金属自带高杠杆风险,这类界面库改法只解决加载问题,不涉及任何策略收益承诺;动标准库前先备份原文件,改完在策略测试器跑一遍再上真仓。
m_rcname="::"+name+(class="type">class="kw">string)(GetTickCount()+MathRand());