研究烛条分析技术(第四部分):形态分析器的更新和补充·进阶篇
(2/3)·老版本分析器空白浪费、周期锁死、范围僵化,这篇把重构思路拆给你看
◍ 把15种K线形态铺成可点按钮
想在 MT5 图表上做一套价格行为形态筛选器,第一步是把形态清单变成界面上能点的按钮。下面这段逻辑直接定义了 15 个形态名,从 Hammer、Invert Hammer 到 Dark Cloud Cover、All Patterns,数组下标 0 到 14 一一对应。 按钮排布用了三行网格:前 5 个(i<5)放在第一行,x 坐标每次加 150;i 在 5 到 9 之间落第二行,y 偏移 +30;i 在 10 到 13 放第三行,y 偏移 +60。第 14 号「All Patterns」跟第三行最后一个对齐,不另起 x。 两个 bmp 资源(passive / pressed)挂给了按钮的常态和按下态,意味着你编译前得把 EasyAndFastGUI\Candles 这套图丢进 MQL5\Images 目录,否则按钮可能空白。外汇和贵金属波动大、杠杆高,这类形态按钮只是辅助标记,信号出现后仍需自判风险。 开 MT5 把这段塞进你的 CProgram 类,调 x_gap 和 y_gap 就能改按钮起始位;想加形态就改 ArrayResize 的数字和字符串数组,但循环里的 i<=14 上限要同步改。
class="type">bool CProgram::CreatePatternSet(CButton &button[],class="type">int x_gap,class="type">int y_gap) { ArrayResize(button,class="num">15); class="type">class="kw">string pattern_names[class="num">15]= { "Hummer", "Invert Hummer", "Handing Man", "Shooting Star", "Engulfing Bull", "Engulfing Bear", "Harami Cross Bull", "Harami Cross Bear", "Harami Bull", "Harami Bear", "Doji Star Bull", "Doji Star Bear", "Piercing Line", "Dark Cloud Cover", "All Patterns" }; class="type">int k1=x_gap,k2=x_gap,k3=x_gap; for(class="type">int i=class="num">0;i<=class="num">14;i++) { if(i<class="num">5) { CreatePatternButton(button[i],pattern_names[i],k1,y_gap); k1+=class="num">150; } else if(i>=class="num">5 && i<class="num">10) { CreatePatternButton(button[i],pattern_names[i],k2,y_gap+class="num">30); k2+=class="num">150; } else if(i>=class="num">10 && i<class="num">14) { CreatePatternButton(button[i],pattern_names[i],k3,y_gap+class="num">60); k3+=class="num">150; } else if(i==class="num">14) { CreatePatternButton(button[i],pattern_names[i],k3,y_gap+class="num">60); } } class="kw">return(true); } class="macro">#resource "\\Images\\EasyAndFastGUI\\Candles\\passive.bmp" class="macro">#resource "\\Images\\EasyAndFastGUI\\Candles\\pressed.bmp"
形态按钮与周期集的批量生成逻辑
在 MT5 自定义面板里,单个 K 线形态按钮不是手搓的。CreatePatternButton 把按钮挂到 m_tabs1 选卡的元素数组,固定宽 120 像素、高 20 像素,字体用 Trebuchet 9 号,文字居中且设为双态(TwoState)控件,平时和按下分别读 passive.bmp / pressed.bmp 两张图。 下方那段 CHARTEVENT_CUSTOM 事件处理值得注意:索引 14 的按钮被当成「全选 / 全不选」开关。它一旦被按下,就用 for 循环把 0~13 共 14 个形态按钮全部 IsPressed(true);松开则全部置 false,最后统一 Update(true) 重绘。这种主从联动在自选形态扫描器里很实用。 周期按钮走的是另一套:CreateTimeframeSet 先把数组扩到 22,再按 M1 到 MN 加一个 ALL 的字符串表循环创建。也就是说,面板一口气铺了 22 个周期入口,含 M2/M3/M4 等非标准周期,方便后续跨周期比对形态。外汇与贵金属波动受杠杆影响大,这类面板只解决「看得快」,信号真伪仍需自行回测验证。
class="type">bool CProgram::CreatePatternButton(CButton &button,class="kw">const class="type">class="kw">string candlename,class="kw">const class="type">int x_gap,class="kw">const class="type">int y_gap) { class=class="str">"cmt">//--- 保存指向主控件的指针 button.MainPointer(m_tabs1); class=class="str">"cmt">//--- 加载到选卡 m_tabs1.AddToElementsArray(class="num">0,button); class=class="str">"cmt">//--- 属性 button.XSize(class="num">120); button.YSize(class="num">20); button.Font("Trebuchet"); button.FontSize(class="num">9); button.LabelColor(clrWhite); button.LabelColorHover(clrWhite); button.LabelColorPressed(clrWhite); button.IsCenterText(true); button.TwoState(true); button.IconFile("Images\\EasyAndFastGUI\\Candles\\passive.bmp"); button.IconFilePressed("Images\\EasyAndFastGUI\\Candles\\pressed.bmp"); class=class="str">"cmt">//--- 创建控件 if(!button.CreateButton(candlename,x_gap,y_gap)) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 将元素指针添加到数据库 CWndContainer::AddToElementsArray(class="num">0,button); class="kw">return(true); } if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON) { class=class="str">"cmt">//--- 选择并取消选择所有形态按钮 if(lparam==m_patterns[class="num">14].Id()) { if(m_patterns[class="num">14].IsPressed()) { for(class="type">int i=class="num">0;i<class="num">14;i++) m_patterns[i].IsPressed(true); } else if(!m_patterns[class="num">14].IsPressed()) { for(class="type">int i=class="num">0;i<class="num">14;i++) m_patterns[i].IsPressed(class="kw">false); } for(class="type">int i=class="num">0;i<class="num">14;i++) m_patterns[i].Update(true); } ... } class="type">bool CProgram::CreateTimeframeSet(CButton &button[],class="type">int x_gap,class="type">int y_gap,class="kw">const class="type">int tab) { ArrayResize(button,class="num">22); class="type">class="kw">string timeframe_names[class="num">22]= {"M1","M2","M3","M4","M5","M6","M10","M12","M15","M20","M30","H1","H2","H3","H4","H6","H8","H12","D1","W1","MN","ALL"}; class="type">int k1=x_gap,k2=x_gap; for(class="type">int i=class="num">0;i<class="num">22;i++) { CreateTimeframeButton(button[i],timeframe_names[i],k1,y_gap,tab);
「周期按钮与全选逻辑的落地写法」
在 MT5 面板里批量生成周期切换按钮,核心是把每个 CButton 挂到同一个标签页容器并统一设外观。下面这段 CreateTimeframeButton 把按钮尺寸锁在 30×30,字体用 Trebuchet 10 号,未按与悬停背色都是 C'200,200,200',按下态切到 C'50,180,75',两态属性 TwoState(true) 保证可来回切换。 [CODE] bool CProgram::CreateTimeframeButton(CButton &button,const string candlename,const int x_gap,const int y_gap,const int tab) { button.MainPointer(m_tabs1); m_tabs1.AddToElementsArray(tab,button); button.XSize(30); button.YSize(30); button.Font("Trebuchet"); button.FontSize(10); button.LabelColor(clrWhite); button.LabelColorHover(clrWhite); button.LabelColorPressed(clrWhite); button.BackColor(C'200,200,200'); button.BackColorHover(C'200,200,200'); button.BackColorPressed(C'50,180,75'); button.BorderColor(C'200,200,200'); button.BorderColorHover(C'200,200,200'); button.BorderColorPressed(C'50,180,75'); button.IsCenterText(true); button.TwoState(true); if(!button.CreateButton(candlename,x_gap,y_gap)) return(false); CWndContainer::AddToElementsArray(0,button); return(true); } [/CODE] 逐行看:MainPointer 把主控指向 m_tabs1,AddToElementsArray 按 tab 索引归位;XSize/YSize 定方块大小;三种 LabelColor 全白保证文字可读;背色与边色在按下时变绿(C'50,180,75')做视觉反馈;CreateButton 失败直接返 false,成功后再塞进全局元素数组。 全选按钮(索引 21)按下时,用 for(i=0;i<21;i++) 把前 21 个周期按钮 IsPressed 全置 true,取消则置 false,最后统一 Update(true) 重绘。这种「一个总控带 21 个子项」的结构,在外汇或贵金属多周期扫描面板上能直接复用,但界面逻辑出错可能导致误选周期,实盘前务必在 MT5 策略测试器外挂面板里点一遍验证。 日期范围按钮则是另一种尺寸:XSize(100) 配 YSize(25),明显比周期方块宽扁,适合放文字标签。CreateDateRange 同样先绑 m_tabs1 再 AddToElementsArray,字体继续用 Trebuchet,方便和周期区保持视觉一致。
class="type">bool CProgram::CreateTimeframeButton(CButton &button,class="kw">const class="type">class="kw">string candlename,class="kw">const class="type">int x_gap,class="kw">const class="type">int y_gap,class="kw">const class="type">int tab) { button.MainPointer(m_tabs1); m_tabs1.AddToElementsArray(tab,button); button.XSize(class="num">30); button.YSize(class="num">30); button.Font("Trebuchet"); button.FontSize(class="num">10); button.LabelColor(clrWhite); button.LabelColorHover(clrWhite); button.LabelColorPressed(clrWhite); button.BackColor(C&class="macro">#x27;class="num">200,class="num">200,class="num">200&class="macro">#x27;); button.BackColorHover(C&class="macro">#x27;class="num">200,class="num">200,class="num">200&class="macro">#x27;); button.BackColorPressed(C&class="macro">#x27;class="num">50,class="num">180,class="num">75&class="macro">#x27;); button.BorderColor(C&class="macro">#x27;class="num">200,class="num">200,class="num">200&class="macro">#x27;); button.BorderColorHover(C&class="macro">#x27;class="num">200,class="num">200,class="num">200&class="macro">#x27;); button.BorderColorPressed(C&class="macro">#x27;class="num">50,class="num">180,class="num">75&class="macro">#x27;); button.IsCenterText(true); button.TwoState(true); if(!button.CreateButton(candlename,x_gap,y_gap)) class="kw">return(class="kw">false); CWndContainer::AddToElementsArray(class="num">0,button); class="kw">return(true); }
◍ 日期范围弹窗与品种联动的代码落点
在 Analyze 选项卡里点开日期范围设置,实际是响应 m_request3 的点击事件:取它的 X 坐标和 Y+YSize 作为新窗口左上角,把 m_window[2] 推到对应位置后 OpenWindow,标签按 m_lang_index 切换俄文或英文「Date Range Settings」。 CreateWindowSetting2 把对话框定死为 372×300 像素、W_DIALOG 类型,并在 (10,25) 与 (201,25) 放两个日历:前者默认 2018-01-01,后者默认今天;时间编辑框则贴在 y=200 处,左框 x=10、右框 x=200。任何子控件创建失败直接 return false,窗体不会半残显示。 品种切换走 ChangeSymbol1,先比对 id 是否为 m_symb_table1.Id(),再读 SelectedItem();若等于 WRONG_VALUE 说明没选行,就进状态栏写完整品种说明的逻辑分支。外汇与贵金属品种切换后历史区间可能断层,回测前务必确认日历起止覆盖足够样本。 想验证这套布局,把上面坐标和尺寸抄进自己的面板类,编译后点 Analyze 看弹窗是否严格落在按钮正下方;若偏移,优先查 m_request3.YSize() 返回值。
button.FontSize(class="num">10); button.IsHighlighted(class="kw">false); button.IsCenterText(true); button.BorderColor(C&class="macro">#x27;class="num">0,class="num">100,class="num">255&class="macro">#x27;); button.BackColor(clrAliceBlue); class=class="str">"cmt">//--- 创建控件 if(!button.CreateButton("",x_gap,y_gap)) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 将元素指针添加到数据库 CWndContainer::AddToElementsArray(class="num">0,button); class="kw">return(true); } class=class="str">"cmt">//--- if(lparam==m_request3.Id()) { class="type">int x=m_request3.X(); class="type">int y=m_request3.Y()+m_request3.YSize(); m_window[class="num">2].X(x); m_window[class="num">2].Y(y); m_window[class="num">2].OpenWindow(); val=(m_lang_index==class="num">0)?"Настройки диапазона дат":"Date Range Settings"; m_window[class="num">2].LabelText(val); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 在 “Analyze” 选项卡中创建日期范围选择对话框 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CProgram::CreateWindowSetting2(class="kw">const class="type">class="kw">string caption_text) { class=class="str">"cmt">//--- 将指针添加到窗口数组 CWndContainer::AddWindow(m_window[class="num">2]); class=class="str">"cmt">//--- 坐标 class="type">int x=m_request3.X(); class="type">int y=m_request3.Y()+m_request3.YSize(); class=class="str">"cmt">//--- 属性 m_window[class="num">2].XSize(class="num">372); m_window[class="num">2].YSize(class="num">300); m_window[class="num">2].WindowType(W_DIALOG); class=class="str">"cmt">//--- 创建窗体 if(!m_window[class="num">2].CreateWindow(m_chart_id,m_subwin,caption_text,x,y)) class="kw">return(class="kw">false); class=class="str">"cmt">//--- if(!CreateCalendar(m_calendar1,m_window[class="num">2],class="num">10,class="num">25,D&class="macro">#x27;class="num">01.01.class="num">2018&class="macro">#x27;,class="num">2)) class="kw">return(class="kw">false); if(!CreateCalendar(m_calendar2,m_window[class="num">2],class="num">201,class="num">25,m_calendar2.Today(),class="num">2)) class="kw">return(class="kw">false); class=class="str">"cmt">//--- if(!CreateTimeEdit(m_time_edit1,m_window[class="num">2],class="num">10,class="num">200,"Time",class="num">2)) class="kw">return(class="kw">false); if(!CreateTimeEdit(m_time_edit2,m_window[class="num">2],class="num">200,class="num">200,"Time",class="num">2)) class="kw">return(class="kw">false); class=class="str">"cmt">//--- class="kw">return(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 在 Analyze 选卡里品种变化 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CProgram::ChangeSymbol1(class="kw">const class="type">long id) { class=class="str">"cmt">//--- 检查元素 ID if(id!=m_symb_table1.Id()) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 如果未选择该行,则退出 if(m_symb_table1.SelectedItem()==WRONG_VALUE) { class=class="str">"cmt">//--- 在状态栏中显示完整的品种说明
选卡切换品种时的空值与形态触发逻辑
在 MT5 面板的两个选卡(手动表 m_symb_table1 与自动搜索表 m_symb_table2)里,品种切换都由各自的 ChangeSymbol 函数接管。若用户没选中任何行(SelectedItem() 返回 WRONG_VALUE),状态栏第 0 项会写入 "No symbol selected for analysis" 并强制 Update(true),函数直接 return(false),不往下跑形态识别。 手动选卡拿到 symbol 后,先用 m_lang_index 判断语种:0 显示俄文前缀 "Выбранный символ: ",否则英文 "Selected symbol: ",再拼上 SymbolInfoString(symbol,SYMBOL_DESCRIPTION) 的品种说明,随后调用 GetPatternType(symbol) 做单品种形态扫描。 自动搜索选卡多一道校验:调用 GetCandleCombitation() 确认已选蜡烛数不小于待研形态尺寸,否则按语种弹 MessageBox 报错(俄文提示“所选蜡烛数小于形态尺寸”,英文同理)并返回 false。通过后带 m_total_combination 参数跑 GetPatternType(symbol,m_total_combination)。 形态识别函数开头就声明了 9 套评分数组:hummer_coef、invert_hummer_coef、handing_man_coef、shooting_star_coef、engulfing_bull_coef、engulfing_bear_coef、harami_cross_bull_coef、harami_cross_bear_coef、harami_bull_coef,对应锤子、倒锤、吊人、流星、看涨/看跌吞没、十字孕线及普通孕线。外汇与贵金属波动剧烈,形态识别仅提供概率倾向,实盘须自担高风险。
m_status_bar.SetValue(class="num">0,"No symbol selected for analysis"); m_status_bar.GetItemPointer(class="num">0).Update(true); class="kw">return(class="kw">false); } class=class="str">"cmt">//--- 获取品种 class="type">class="kw">string symbol=m_symb_table1.GetValue(class="num">0,m_symb_table1.SelectedItem()); class=class="str">"cmt">//--- 在状态栏中显示完整的品种说明 class="type">class="kw">string val=(m_lang_index==class="num">0)?"Выбранный символ: ":"Selected symbol: "; m_status_bar.SetValue(class="num">0,val+::SymbolInfoString(symbol,SYMBOL_DESCRIPTION)); m_status_bar.GetItemPointer(class="num">0).Update(true); class=class="str">"cmt">//--- GetPatternType(symbol); class="kw">return(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 在 AutoSearch 选卡里品种变化 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CProgram::ChangeSymbol2(class="kw">const class="type">long id) { class=class="str">"cmt">//--- 检查元素 ID if(id!=m_symb_table2.Id()) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 如果未选择该行,则退出 if(m_symb_table2.SelectedItem()==WRONG_VALUE) { class=class="str">"cmt">//--- 在状态栏中显示完整的品种说明 m_status_bar.SetValue(class="num">0,"No symbol selected for analysis"); m_status_bar.GetItemPointer(class="num">0).Update(true); class="kw">return(class="kw">false); } class=class="str">"cmt">//--- 获取品种 class="type">class="kw">string symbol=m_symb_table2.GetValue(class="num">0,m_symb_table2.SelectedItem()); class=class="str">"cmt">//--- 在状态栏中显示完整的品种说明 class="type">class="kw">string val=(m_lang_index==class="num">0)?"Выбранный символ: ":"Selected symbol: "; m_status_bar.SetValue(class="num">0,val+::SymbolInfoString(symbol,SYMBOL_DESCRIPTION)); m_status_bar.GetItemPointer(class="num">0).Update(true); class=class="str">"cmt">//--- if(!GetCandleCombitation()) { if(m_lang_index==class="num">0) MessageBox("Число выбранных свечей меньше размера исследуемого паттерна!","Ошибка",MB_OK); else if(m_lang_index==class="num">1) MessageBox("The number of selected candles is less than the size of the studied pattern!","Error",MB_OK); class="kw">return(class="kw">false); } class=class="str">"cmt">//--- GetPatternType(symbol,m_total_combination); class="kw">return(true); } class="type">bool GetPatternType(class="kw">const class="type">class="kw">string symbol); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 形态识别 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CProgram::GetPatternType(class="kw">const class="type">class="kw">string symbol) { CANDLE_STRUCTURE cand1,cand2; class=class="str">"cmt">//--- RATING_SET hummer_coef[]; RATING_SET invert_hummer_coef[]; RATING_SET handing_man_coef[]; RATING_SET shooting_star_coef[]; RATING_SET engulfing_bull_coef[]; RATING_SET engulfing_bear_coef[]; RATING_SET harami_cross_bull_coef[]; RATING_SET harami_cross_bear_coef[]; RATING_SET harami_bull_coef[];
「时间帧与日期范围校验后再跑形态扫描」
这段逻辑是形态扫描前的闸门:先通过 GetTimeframes 拿到用户勾选的时间帧数组,用 ArraySize 取总数 total。若 total<1 说明没选任何周期,直接弹窗报错并返回 false,俄文与英文提示由 m_lang_index 切换。 日期范围由两只日历和时间编辑框拼出:start 取日历1日期加 time_edit1 的时分,end 取日历2日期加 time_edit2 的时分,均转成 datetime。若 start>end 或 end>TimeCurrent()(当前服务器时间),同样弹窗并返回 false,避免扫到未来时间。 过闸后 m_table1.DeleteAllRows() 清空旧表,m_total_row 与 m_table_number 归零。以锤子形态为例,勾选 m_patterns[0] 后会对每个时间帧用 CopyRates 拷贝 start~end 的 MqlRates,再逐根调用 GetCandleType 取 cand1,并判断 cand1.trend==DOWN 才计入——也就是说,下跌趋势中的锤子才被认定为多头信号。外汇与贵金属波动剧烈,这类形态仅为概率倾向,实盘前务必在 MT5 策略测试器用历史数据验证命中率。 让小布替你跑这套 把 m_patterns[0] 的勾选逻辑复制到自己的 EA,改 cand1.trend==UP 就能反向扫顶部锤子;周期数组留空会直接拦截,省得空跑浪费 tick。
RATING_SET harami_bear_coef[]; RATING_SET doji_star_bull_coef[]; RATING_SET doji_star_bear_coef[]; RATING_SET piercing_line_coef[]; RATING_SET dark_cloud_cover_coef[]; class=class="str">"cmt">//--- 接收所选时间帧的数据 GetTimeframes(m_timeframes,m_cur_timeframes1); class="type">int total=ArraySize(m_cur_timeframes1); class=class="str">"cmt">//--- 检查至少一个选定的时间帧 if(total<class="num">1) { if(m_lang_index==class="num">0) MessageBox("Вы не выбрали рабочий таймфрейм!","Ошибка",MB_OK); else if(m_lang_index==class="num">1) MessageBox("You have not selected a working timeframe!","Error",MB_OK); class="kw">return(class="kw">false); } class="type">int count=class="num">0; m_total_row=class="num">0; m_table_number=class="num">1; class=class="str">"cmt">//--- 删除所有行 m_table1.DeleteAllRows(); class=class="str">"cmt">//--- 获取日期范围 class="type">class="kw">datetime start=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"); class="type">class="kw">datetime end=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">//--- 检查指定的日期 if(start>end || end>TimeCurrent()) { if(m_lang_index==class="num">0) MessageBox("Неправильно выбран диапазон дат!","Ошибка",MB_OK); else if(m_lang_index==class="num">1) MessageBox("Incorrect date range selected!","Error",MB_OK); class="kw">return(class="kw">false); } class=class="str">"cmt">//--- 锤子,多头形态 if(m_patterns[class="num">0].IsPressed()) { ArrayResize(m_hummer_total,total); ArrayResize(hummer_coef,total); ZeroMemory(m_hummer_total); ZeroMemory(hummer_coef); ZeroMemory(cand1); count++; class=class="str">"cmt">//--- 按时间帧计算 for(class="type">int j=class="num">0;j<total;j++) { class="type">MqlRates rt[]; ZeroMemory(rt); class="type">int copied=CopyRates(symbol,m_cur_timeframes1[j],start,end,rt); for(class="type">int i=class="num">0;i<copied;i++) { GetCandleType(symbol,cand1,m_cur_timeframes1[j],i); class=class="str">"cmt">// 当前烛条 if(cand1.trend==DOWN && class=class="str">"cmt">// 检查趋势方向