扩充策略构建器功能(基础篇)
📘

扩充策略构建器功能(基础篇)

第 1/3 篇

◍ 给策略构建器加料

MT5 自带的策略构建器(Strategy Builder)早期只能拼基础条件,2020 年 2 月 7 日官方补了一版功能扩充,让非编程用户也能搭出带多过滤层的 EA 雏形。 那次更新文档里标注了 1,993 次浏览、3 条讨论,作者 Alexander Fedosov 把工作拆成「概览—新功能实现—测试演示」三段推进,说明不是一次性甩功能,而是先定义边界再写逻辑。 对做实盘的交易者来说,重点不在界面多了几个按钮,而在于你能把「价格行为 + 指标过滤」的念头直接拖成可回测模块,省掉找人写 MQL5 的沟通成本。外汇和贵金属波动大、杠杆高,用构建器跑出的策略先开 MT5 策略测试器验证,别直接上真仓。

「这套形态研究要分几步走」

前面那篇把 Merrill 形态拆开验过,跑过价格本身,也跑过 ATR、CCI、WPR 这类由价格派生的振荡器,确认了形态在不同数据数组上都能被识别。本篇不重复验证,重点转到外汇和其他市场里用这套形态到底有没有搞头。 下一步会先写一个小节里要用的策略构建器,把前面散着的形态规则收拢成能直接跑的简单策略。再往后一篇才扩策略功能、做测试,并加上按点数控仓和看测试结果的可视化。 外汇和贵金属杠杆高、跳空频繁,Merrill 形态在这些品种上只是概率优势,不是信号一出就进场的理由,真要试先开 MT5 用策略测试器跑一遍 EURUSD 的 H1 再说。

把回测报告从点数换算成真金白银

MT5 默认摘要报告里的盈利亏损是按品种点数计的,这对评估策略真实潜力不够用。原版看不到以账户币种计的净头寸、回撤和恢复能力,只能粗略比点数。 改造思路是先让初始存款用账户币种表达,盈利计算增加“按点数 / 按存款币种”切换;选币种计利后,补两个输入:手数类型(常数或按余额)和手数大小(仅常数类型可用)。 交易报告里同步加了几个只在点位切到存款币种时才出现的字段:净利润总额、余额绝对回撤、余额最大回撤、恢复因子。另外多头空头交易除了数量,还直接给各自胜率百分比;策略盈利能力写成利润总额对亏损总额的比值。 报告区会多一个“打开图表”按钮,点开能看存款曲线和每笔交易落点,标题带品种、周期和测试区间。外汇和贵金属杠杆高,这类回测仅反映历史样本,实盘滑点和点差可能让回撤显著放大。

◍ 测试器界面的控件与数据写入落点

在 CreateGUI() 主流程里,新版本只多挂了三个创建入口:图表窗口 CreateGraphWindow()、加载窗 CreateLoading(),以及原本就有的 CreateWindow() 里补的构造器选项卡控件。打开 MT5 看附件工程,你能直接对照到 CreateWindow() 中报告区从两列扩成了三列,布局坐标用了 0.35/0.6/0.85 乘窗口宽减 150 的算法,y 方向按 35*N+ygap 往下排。 新增的下拉与输入框由五个小方法承担:CreateProfitType() 切存款币种或点数,CreateLotType() 切常数或按余额,CreateBaseLotValue() 填手数,CreateInitialDeposit() 仅在币种利润模式下出现,CreateIconButton() 负责点开图表窗。这些都不是孤立画布,而是挂在同一选项卡坐标体系下,改一处 ygap 会整体错位。 真正决定回测输出的是 AddDeal():它接收 points(平仓盈亏点数,含止盈或止损)和 index(交易序号),按利润模式写数组。点数模式下新元素 = 前值与本次交易结果之和;货币模式下要把点数按直盘、颠倒盘、交叉盘分别折算存款币种。外汇与贵金属杠杆高,折算错一组货币对类型,报表的恢复因子就可能偏向失真。 GetResult() 里补了形态与买卖信号匹配检查,CalculateBuyDeals() / CalculateSellDeals() 加了 profit_pips 与 loss_pips 到 REPORT 结构,止盈止损触发后转交 AddDeal() 落数。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">//--- Create a panel
   if(!CreateWindow("Merrill Constructor"))
      class="kw">return(false);
class=class="str">"cmt">//--- Create a dialog window
   if(!CreateDateSetting())
      class="kw">return(false);
class=class="str">"cmt">//--- Create a chart window
   if(!CreateGraphWindow())
      class="kw">return(false);
class=class="str">"cmt">//--- Create a load window
   if(!CreateLoading())
      class="kw">return(false);
class=class="str">"cmt">//--- Finish the creation of GUI
   CWndEvents::CompletedGUI();
   class="kw">return(true);
}
class=class="str">"cmt">//---- CONSTRUCTOR tab
....
class=class="str">"cmt">//---
   if(!CreateProfitType(class="type">int(class="num">0.35*(m_window[class="num">0].XSize()-class="num">150)-class="num">120),class="num">50+class="num">35*class="num">6+ygap))
      class="kw">return(false);
   if(!CreateLotType(class="type">int(class="num">0.6*(m_window[class="num">0].XSize()-class="num">150)-class="num">120),class="num">50+class="num">35*class="num">6+ygap))
      class="kw">return(false);
   if(!CreateBaseLotValue(class="type">int(class="num">0.85*(m_window[class="num">0].XSize()-class="num">150)-class="num">120),class="num">50+class="num">35*class="num">6+ygap))
      class="kw">return(false);
   if(!CreateInitialDeposit(class="type">int(class="num">0.35*(m_window[class="num">0].XSize()-class="num">150)-class="num">120),class="num">50+class="num">35*class="num">7+ygap))
      class="kw">return(false);
class=class="str">"cmt">//---
   if(!CreateReportFrame(m_frame[class="num">2],class="type">int(class="num">0.35*(m_window[class="num">0].XSize()-class="num">150)-class="num">120),class="num">110+class="num">35*class="num">7+ygap))
      class="kw">return(false);
class=class="str">"cmt">//--- Graph opening button
   if(!CreateIconButton(class="type">int(class="num">0.35*(m_window[class="num">0].XSize()-class="num">150)-class="num">50),class="num">100+class="num">35*class="num">7+ygap))
      class="kw">return(false);

「面板文本行与收益切换控件的布局落点」

这段逻辑负责把报告区的 11 行文本标签按窗口宽度做三列排布,并在 380 像素基准 Y 坐标上每行下压 25 像素。前 5 行贴左列(X 取窗口宽减 150 后的 0.37 倍再偏移 120),第 5~8 行走中列(0.63 倍),第 9~10 行走右列(0.89 倍),字号统一锁 10、左对齐。 [CODE] for(int i=0; i<11; i++) { if(i<5) if(!CreateTextLabel(m_report_text[i],int(0.37*(m_window[0].XSize()-150)-120),380+25*i+ygap,"",0)) return(false); if(i>=5 && i<9) if(!CreateTextLabel(m_report_text[i],int(0.63*(m_window[0].XSize()-150)-120),380+25*(i-5)+ygap,"",0)) return(false); if(i>=9) if(!CreateTextLabel(m_report_text[i],int(0.89*(m_window[0].XSize()-150)-120),380+25*(i-9)+ygap,"",0)) return(false); m_report_text[i].IsCenterText(false); m_report_text[i].FontSize(10); } [/CODE] 逐行拆解:循环变量 i 从 0 到 10 共 11 次;i<5 时调用 CreateTextLabel 生成左列标签,X 坐标由窗口宽动态算得;i 在 5~8 走中列,行号用 i-5 重算偏移;i>=9 走右列用 i-9。每次创建失败直接 return false 中断。最后两行关闭居中、设字号 10。 收益类型下拉框(Pips / Currency)由 CreateProfitType 封装:组合框宽 200、高 25,字体 12,标签色 C'0,100,255',按钮宽 100、背景 AliceBlue,列表视窗高 44、悬浮高亮开启,默认选中索引 1(即 Currency)。你在 MT5 里改 m_profit_type.SelectItem(1) 为 SelectItem(0) 就能让面板默认显示点数收益。外汇与贵金属波动剧烈,这类界面参数仅影响展示口径,不预示任何方向。

MQL5 / C++
for(class="type">int i=class="num">0; i<class="num">11; i++)
{
  if(i<class="num">5)
    if(!CreateTextLabel(m_report_text[i],class="type">int(class="num">0.37*(m_window[class="num">0].XSize()-class="num">150)-class="num">120),class="num">380+class="num">25*i+ygap,"",class="num">0))
      class="kw">return(false);
  if(i>=class="num">5 && i<class="num">9)
    if(!CreateTextLabel(m_report_text[i],class="type">int(class="num">0.63*(m_window[class="num">0].XSize()-class="num">150)-class="num">120),class="num">380+class="num">25*(i-class="num">5)+ygap,"",class="num">0))
      class="kw">return(false);
  if(i>=class="num">9)
    if(!CreateTextLabel(m_report_text[i],class="type">int(class="num">0.89*(m_window[class="num">0].XSize()-class="num">150)-class="num">120),class="num">380+class="num">25*(i-class="num">9)+ygap,"",class="num">0))
      class="kw">return(false);
  m_report_text[i].IsCenterText(false);
  m_report_text[i].FontSize(class="num">10);
}

class="type">bool CProgram::CreateProfitType(class="kw">const class="type">int x_gap,class="kw">const class="type">int y_gap)
{
  m_profit_type.MainPointer(m_tabs1);
  m_tabs1.AddToElementsArray(class="num">0,m_profit_type);
  class="type">class="kw">string pattern_names[class="num">2]=
  {
    "Pips","Currency"
  };
  m_profit_type.XSize(class="num">200);
  m_profit_type.YSize(class="num">25);
  m_profit_type.ItemsTotal(class="num">2);
  m_profit_type.FontSize(class="num">12);
  m_profit_type.LabelColor(C&class="macro">#x27;class="num">0,class="num">100,class="num">255&class="macro">#x27;);
  m_profit_type.GetButtonPointer().FontSize(class="num">10);
  m_profit_type.GetButtonPointer().XGap(class="num">80);
  m_profit_type.GetButtonPointer().XSize(class="num">100);
  m_profit_type.GetButtonPointer().BackColor(clrAliceBlue);
  m_profit_type.GetListViewPointer().FontSize(class="num">10);
  m_profit_type.GetListViewPointer().YSize(class="num">44);
  for(class="type">int i=class="num">0; i<class="num">2; i++)
    m_profit_type.SetValue(i,pattern_names[i]);
  CListView *lv=m_profit_type.GetListViewPointer();
  lv.LightsHover(true);
  m_profit_type.SelectItem(class="num">1);
  if(!m_profit_type.CreateComboBox("Profit Type",x_gap,y_gap))
    class="kw">return(false);
  CWndContainer::AddToElementsArray(class="num">0,m_profit_type);

手搭面板里的手数类型与基础仓位控件

在 MT5 自定义面板里,手数类型用组合框承载,代码里先塞进两个选项:Balance 与 Constant,对应按余额比例和固定手数两种下单逻辑。组合框本体宽 200 高 25,字体 12,标签色 C'0,100,255',下拉列表视图像素高 44、字号 10,按钮区宽 100 且距左 65,这些数值直接决定你在面板上看到的下拉控件占位。 基础手数控件走的是带上下箭头的文本编辑框(SpinEdit),宽 210 高 24。关键不在美观,而在三行 MaxValue / MinValue / StepValue 全部取自 SymbolInfoDouble 的 SYMBOL_VOLUME_MAX、SYMBOL_VOLUME_MIN、SYMBOL_VOLUME_STEP——意味着换到 XAUUSD 和 EURUSD 时,可输范围与步进会自动跟随品种合约规格,不用硬改常量。 外汇与贵金属杠杆高、点值波动大,面板允许的最大最小手数仅代表经纪商合约限制,不代表合适仓位;实际跟单或自启 EA 前,建议把基础手数先压到 MinValue 附近做几笔模拟验证。 下面这段是 CreateLotType 与 CreateBaseLotValue 两个方法的原文,逐行看更容易抄到自己的 CProgram 类里。

MQL5 / C++
class="type">bool CProgram::CreateLotType(class="kw">const class="type">int x_gap,class="kw">const class="type">int y_gap)
{
class=class="str">"cmt">//--- Pass the object to the panel
  m_lot_type.MainPointer(m_tabs1);
class=class="str">"cmt">//--- Attach to tab
  m_tabs1.AddToElementsArray(class="num">0,m_lot_type);
class=class="str">"cmt">//--- Array of the item values in the list view
  class="type">class="kw">string pattern_names[class="num">2]=
  {
    "Balance","Constant"
  };
class=class="str">"cmt">//--- Set properties before creation
  m_lot_type.XSize(class="num">200);
  m_lot_type.YSize(class="num">25);
  m_lot_type.ItemsTotal(class="num">2);
  m_lot_type.FontSize(class="num">12);
  m_lot_type.LabelColor(C&class="macro">#x27;class="num">0,class="num">100,class="num">255&class="macro">#x27;);
  m_lot_type.GetButtonPointer().FontSize(class="num">10);
  m_lot_type.GetButtonPointer().XGap(class="num">65);
  m_lot_type.GetButtonPointer().XSize(class="num">100);
  m_lot_type.GetButtonPointer().BackColor(clrAliceBlue);
  m_lot_type.GetListViewPointer().FontSize(class="num">10);
  m_lot_type.GetListViewPointer().YSize(class="num">44);
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_lot_type.SetValue(i,pattern_names[i]);
class=class="str">"cmt">//--- Get the list view pointer
  CListView *lv=m_lot_type.GetListViewPointer();
class=class="str">"cmt">//--- Set the list view properties
  lv.LightsHover(true);
  m_lot_type.SelectItem(class="num">1);
class=class="str">"cmt">//--- Create a control
  if(!m_lot_type.CreateComboBox("Lot Type",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_lot_type);
  class="kw">return(true);
}
class="type">bool CProgram::CreateBaseLotValue(class="kw">const class="type">int x_gap,class="kw">const class="type">int y_gap)
{
class=class="str">"cmt">//--- Save the pointer to the main control
  m_base_lot.MainPointer(m_tabs1);
class=class="str">"cmt">//--- Attach to tab
  m_tabs1.AddToElementsArray(class="num">0,m_base_lot);
class=class="str">"cmt">//--- Properties
  m_base_lot.XSize(class="num">210);
  m_base_lot.YSize(class="num">24);
  m_base_lot.LabelColor(C&class="macro">#x27;class="num">0,class="num">100,class="num">255&class="macro">#x27;);
  m_base_lot.FontSize(class="num">12);
  m_base_lot.MaxValue(SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MAX));
  m_base_lot.MinValue(SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN));
  m_base_lot.StepValue(SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_STEP));
  m_base_lot.SetDigits(class="num">2);
  m_base_lot.SpinEditMode(true);
  m_base_lot.GetTextBoxPointer().AutoSelectionMode(true);
  m_base_lot.GetTextBoxPointer().XGap(class="num">100);
class=class="str">"cmt">//--- Create a control
  if(!m_base_lot.CreateTextEdit("Base Lot Size",x_gap,y_gap))

常见问题

在测试器界面把点数乘以每点价值和当前手数即可;可在面板里加一个收益切换控件,一键在点数和金额视图间切换。
在手搭面板中单独留出手数类型下拉与基础仓位输入框,通常紧挨着品种与周期选择,避免和信号参数混排。
小布可以读取你的品种页设置,自动生成面板布局草稿并标好控件落点,你只需核对手数与仓位参数即可。
建议只放净利润、最大回撤、胜率三行;收益切换控件放在文本行右侧,方便对比点数与金额。
分三步:先定形态识别条件,再接回测写入落点,最后做面板布局;每步独立验证再拼装。