基于 Merill(梅里尔) 形态的策略构建器(基础篇)
📘

基于 Merill(梅里尔) 形态的策略构建器(基础篇)

第 1/3 篇

「从梅里尔形态搭一个策略构建器」

梅里尔(Merrill)形态把价格摆动归纳成若干可识别的几何结构,早期多用于股票日线,放到 MT5 的外汇与贵金属 5 分钟~1 小时周期上,同样能作为入场过滤的骨架。外汇与贵金属杠杆高、滑点跳空频繁,任何形态信号都只是概率倾向,不能直接当方向指令。 作者 Alexander Fedosov 在 2020 年 1 月 16 日发布了这篇原型文,原文页面显示 3028 次浏览、4 条评论,说明这类「可视化拼装策略」的思路在 MT5 社区有真实需求。原型目标不是直接出 EA,而是先做一个测试用构建器,让你把梅里尔形态的组合条件拖进去跑历史。 构建器核心只做三件事:识别摆动点、匹配形态模板、输出可被回测引擎读取的参数串。下一步才是把它接进 Strategy Tester,用真实报价验证哪种形态组合在 EURUSD 或 XAUUSD 上假信号更少。

◍ 从形态实验到策略工具箱

上篇把 Merrill 形态接到了 MT5 多个数据面上:价格本身,以及 ATR、WPR、CCI、RSI 这类标准指标。那时搭的图形界面主要用来试错和翻有效的用法,不是拿来直接跑账户。 本文的落点更实——要把验证过的形态配置固化成可回测的交易策略。做法是先开发一套基础工具箱,负责策略组装和测试,而不是继续在界面里手动翻组合。 外汇和贵金属波动大、杠杆高,任何形态配置在进实盘前都该先在 MT5 策略测试器里跑历史数据,确认样本外表现再谈下一步。

先把策略构造器的界面骨架定下来

动手写代码前,先把 MT5 里这套策略构造器要塞进哪些界面元素理清楚,不然后面改起来很费事。整体拆成两个选项卡:构造器(Builder)管策略拼装,设置(Settings)管指标参数和自定义指标挂载。 构造器选项卡里,左侧是品种表格,直接拉取“市场观察”里的全量品种列表,再配一个过滤器输入框做品种组速查。中间对称排两坨完全一样的信号生成区,分别管 Buy / Sell,每块都含:Merrill 形态选择、三个可单独开关的信号槽(只开一个就只认它,开多个则任一触发即可入场)、测试日期范围、当前时间帧下拉。底部是策略测试结果板块。 Merrill 形态既能套在价格上,也能套在指标上。信号槽里若选“自定义”,就要靠 iCustom 把第三方指标接进来——这一步路径写错最常见。自定义指标名称要带相对 MQL5/Indicators/ 的路径,若在子目录如 Examples,得写成 "Examples\\指标_名称",双反斜杠不能省。 设置选项卡除了标准指标参数,还留了“自定义指标设置”分区和输入字段,严格照 iCustom 语法填。原型阶段先把这两张选项卡的布局和元素落图,后面软件实现完再补完整用法。外汇和贵金属品种波动大、杠杆高,原型验证时建议先用模拟盘跑通界面逻辑。

「搭测试策略构造器的界面骨架」

做 MT5 面板类 EA 工具,第一步不是写策略,而是把界面骨架钉死。这个 Merrill 形态测试器就分两个窗口:主窗口带「构造器」和「设置」两个选项卡,另有一个独立的日期范围对话框。CreateGUI() 只干一件事——按顺序把两个窗口建出来,任一步返回 false 就整体中止。 主窗口的 CreateWindow() 摊得很开,先立窗口本体再用 CreateTabs() 塞进两个选项卡;构造器页里品种过滤器、时间帧、形态勾选、信号组合、止盈止损、报告区全是重复控件拼出来的,所以代码里把 CreateButton()、CreateTextLabel()、CreateEditValue() 写成通用方法,后面直接复用。设置页则管指标参数、界面语言、自定义指标名与逗号分隔参数。 交互逻辑有固定八步:选语言→设指标参→点搜索填品种表→点日期按钮设区间→选操作时间帧→启停买卖信号→配信号与盈亏点→左键点表触发 GetResult()。其中日期框若结束日晚于当前日或早于开始日,程序会直接报错返回,这一步最容易在手动输入时翻车。 信号触发按“或”逻辑:买入若挂了三个信号,任一触发即可能进场;GetResult() 里还会拦一道——不允许买卖两边设成同一个 Merrill 形态。BuySignal() 与 SellSignal() 结构对称,流程是先查启用状态、再取数据数组、调 GetPatternType() 搜形态,最后由 CalculateBuyDeals() 落统计、PrintReport() 出结果。外汇与贵金属测试涉及高杠杆与滑点,回测通过不代表实盘概率等同。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Creates the graphical interface of the program                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CProgram::CreateGUI(class="type">void)
{
class=class="str">"cmt">//--- Creating a panel
  if(!CreateWindow("Merrill Constructor"))
    class="kw">return(false);
class=class="str">"cmt">//--- Creating a dialog window
  if(!CreateDateSetting())
    class="kw">return(false);
class=class="str">"cmt">//--- Finishing the creation of GUI
  CWndEvents::CompletedGUI();
  class="kw">return(true);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Creates a date range selection dialog box                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CProgram::CreateDateSetting(class="type">void)
{
class=class="str">"cmt">//--- Add the pointer to the window array
  CWndContainer::AddWindow(m_window[class="num">1]);
class=class="str">"cmt">//--- Coordinates
  class="type">int x=m_date_range.X();
  class="type">int y=m_date_range.Y()+m_date_range.YSize();
class=class="str">"cmt">//--- Properties
  m_window[class="num">1].XSize(class="num">372);
  m_window[class="num">1].YSize(class="num">230);
  m_window[class="num">1].WindowType(W_DIALOG);
  m_window[class="num">1].IsMovable(true);
class=class="str">"cmt">//--- Creating the form

◍ 在主窗体里挂日历与时间编辑框

下面这段初始化逻辑把两个日历控件和两个时间编辑框都塞进了索引为 1 的子窗口:第一个日历锚定 2019.01.01,第二个直接用 Today() 取当前交易日,二者横向间距 191 像素(x 坐标 10 与 201)。 若任一 CreateCalendar 或 CreateTimeEdit 返回 false,整个初始化立即 return(false),MT5 终端会因此看不到面板——调试时优先查子窗口句柄 m_window[1] 是否先 CreateWindow 成功。 主窗体 m_window[0] 写死尺寸 900×600、字号 9、标题色 C'0,130,255',版本号宏 VERSION 为 " 1.0",实际窗体标题会变成传入 caption_text 加 " 1.0"。 标签页 m_tabs1 设了 TABS_LEFT 左对齐、自动拉伸且底部留 25 像素、标签高 40,循环用 ArraySize(m_tabs_names) 动态加页,少一个名字就少一个 tab。外汇与贵金属图表加载此类 EA 界面存在脚本阻塞风险,参数不对可能导致子窗口不渲染。

MQL5 / C++
  if(!m_window[class="num">1].CreateWindow(m_chart_id,m_subwin,"",x,y))
        class="kw">return(false);
class=class="str">"cmt">//---
  if(!CreateCalendar(m_calendar1,m_window[class="num">1],class="num">10,class="num">25,D&class="macro">#x27;class="num">01.01.class="num">2019&class="macro">#x27;,class="num">1))
        class="kw">return(false);
  if(!CreateCalendar(m_calendar2,m_window[class="num">1],class="num">201,class="num">25,m_calendar2.Today(),class="num">1))
        class="kw">return(false);
class=class="str">"cmt">//---
  if(!CreateTimeEdit(m_time_edit1,m_window[class="num">1],class="num">10,class="num">200,"Time",class="num">1))
        class="kw">return(false);
  if(!CreateTimeEdit(m_time_edit2,m_window[class="num">1],class="num">200,class="num">200,"Time",class="num">1))
        class="kw">return(false);
class=class="str">"cmt">//---
  class="kw">return(true);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Creates a form for controls                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CProgram::CreateWindow(const class="type">class="kw">string caption_text)
{
class="macro">#define VERSION " class="num">1.0"
  class="type">color caption=C&class="macro">#x27;class="num">0,class="num">130,class="num">255&class="macro">#x27;;
  class="type">int ygap=class="num">30;
class=class="str">"cmt">//--- Add the pointer to the window array
  CWndContainer::AddWindow(m_window[class="num">0]);
class=class="str">"cmt">//--- Properties
  m_window[class="num">0].XSize(class="num">900);
  m_window[class="num">0].YSize(class="num">600);
  m_window[class="num">0].FontSize(class="num">9);
  m_window[class="num">0].CloseButtonIsUsed(true);
  m_window[class="num">0].CollapseButtonIsUsed(true);
  m_window[class="num">0].CaptionColor(caption);
  m_window[class="num">0].CaptionColorHover(caption);
  m_window[class="num">0].CaptionColorLocked(caption);
class=class="str">"cmt">//--- Creating the form
  if(!m_window[class="num">0].CreateWindow(m_chart_id,m_subwin,caption_text+VERSION,class="num">10,class="num">20))
        class="kw">return(false);
class=class="str">"cmt">//--- Tabs
  if(!CreateTabs(class="num">150,class="num">20))
        class="kw">return(false);
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Create a group with tabs                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CProgram::CreateTabs(const class="type">int x_gap,const class="type">int y_gap)
{
class=class="str">"cmt">//--- Save the pointer to the main control
  m_tabs1.MainPointer(m_window[class="num">0]);
class=class="str">"cmt">//--- Properties
  m_tabs1.IsCenterText(true);
  m_tabs1.PositionMode(TABS_LEFT);
  m_tabs1.AutoXResizeMode(true);
  m_tabs1.AutoYResizeMode(true);
  m_tabs1.AutoYResizeBottomOffset(class="num">25);
  m_tabs1.TabsYSize(class="num">40);  
class=class="str">"cmt">//--- Add tabs with the specified properties
  for(class="type">int i=class="num">0; i<ArraySize(m_tabs_names); i++)
      m_tabs1.AddTab(m_tabs_names[i],class="num">150);
class=class="str">"cmt">//--- Creating a control
  if(!m_tabs1.CreateTabs(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_tabs1);

面板构造里的坐标与容错逻辑

这段构造函数尾部集中处理图形面板的初始化,所有控件都按窗口宽度动态算 X 坐标:以 m_window[0].XSize()-150 为基准,BUY 区取 0.35 倍偏移、SELL 区取 0.75 倍偏移,再各自减 100 像素定位。这种写法在 MT5 里能自适应不同图表宽度,但前提是主窗口对象已先于本段创建。 每一个 Create* 或 Pattern* 调用失败都会立刻 return(false),意味着只要符号过滤(CreateSymbolsFilter(10,10))、符号表(CreateSymbolsTable(10,45))或任一文本标签、按钮、勾选框初始化异常,整个面板构建直接中断。开 MT5 加载这类 EA 时,若面板空白,优先查前几个 if 的返回值而不是后面坐标。 时间框架标签固定在 (290,10),日期范围按钮在 (240,10),而 BUY/SELL 信号文字与形态选择框则依赖 ygap 做纵向堆叠。外汇与贵金属品种波动大、点差跳变频繁,这类面板若绑定实时信号,初始化失败可能让你漏掉一段高概率形态,实盘前务必在策略测试器里跑一遍构造流程。

MQL5 / C++
  class="kw">return(true);
}
class=class="str">"cmt">//---- Constructor tab
class=class="str">"cmt">//--- Symbols filter
  if(!CreateSymbolsFilter(class="num">10,class="num">10))
    class="kw">return(false);
  if(!CreateSymbolsTable(class="num">10,class="num">45))
    class="kw">return(false);
class=class="str">"cmt">//--- Working timeframe
  if(!CreateTextLabel(m_text_labels1[class="num">2],class="num">290,class="num">10,"Timeframe",class="num">0))
    class="kw">return(false);
  if(!CreateTimeframe1(class="num">440,class="num">10))
    class="kw">return(false);
class=class="str">"cmt">//--- Date range
  if(!CreateButton(m_date_range,class="num">240,class="num">10))
    class="kw">return(false);
class=class="str">"cmt">//--- Text labels
  if(!CreateTextLabel(m_text_labels1[class="num">0],class="type">int(class="num">0.35*(m_window[class="num">0].XSize()-class="num">150)-class="num">100),class="num">10+ygap,"BUY—Signal",class="num">0))
    class="kw">return(false);
  if(!CreateTextLabel(m_text_labels1[class="num">1],class="type">int(class="num">0.75*(m_window[class="num">0].XSize()-class="num">150)-class="num">100),class="num">10+ygap,"SELL—Signal",class="num">0))
    class="kw">return(false);
class=class="str">"cmt">//--- Pattern selection
  if(!PatternType1(class="type">int(class="num">0.35*(m_window[class="num">0].XSize()-class="num">150)-class="num">100),class="num">40+ygap,class="num">0))
    class="kw">return(false);
  if(!CreateCheckBox(m_checkbox[class="num">0],class="type">int(class="num">0.35*(m_window[class="num">0].XSize()-class="num">150)-class="num">120),class="num">45+ygap,"Pattern"))
    class="kw">return(false);
  if(!PatternType2(class="type">int(class="num">0.75*(m_window[class="num">0].XSize()-class="num">150)-class="num">100),class="num">40+ygap,class="num">0))
    class="kw">return(false);
  if(!CreateCheckBox(m_checkbox[class="num">1],class="type">int(class="num">0.75*(m_window[class="num">0].XSize()-class="num">150)-class="num">120),class="num">45+ygap,"Pattern"))
    class="kw">return(false);
class=class="str">"cmt">//--- Selecting the application of patterns
  if(!AppliedType1(class="type">int(class="num">0.35*(m_window[class="num">0].XSize()-class="num">150)-class="num">100),class="num">80+ygap))
    class="kw">return(false);
  if(!AppliedType2(class="type">int(class="num">0.35*(m_window[class="num">0].XSize()-class="num">150)-class="num">100),class="num">50+class="num">33*class="num">2+ygap))
    class="kw">return(false);
  if(!AppliedType3(class="type">int(class="num">0.35*(m_window[class="num">0].XSize()-class="num">150)-class="num">100),class="num">50+class="num">33*class="num">3+ygap))
    class="kw">return(false);

常见问题

先把主窗体、日历和时间编辑框的摆放坐标写死在初始化函数里,留好容错判断空输入,后面加形态规则只动逻辑层。
检查控件 left 和 top 是否用了统一基准变量,别硬编码像素;加一行打印当前坐标的日志,肉眼比对面板即可。
小布可替你跑形态识别与参数组合验证,把重复劳动交给它,你专注决策界面和规则设计。
先固化输入容错和界面骨架,再逐步把实验过的形态写成可调参数模块,避免每次重画面板。
会,空值进计算直接异常;在读取编辑框后加长度判断,为空就给默认当前时间并提示用户。