扩充策略构建器功能·综合运用
🛠️

扩充策略构建器功能·综合运用

(3/3)· 前两步搭好形态与构建器,这回把手数控制和测试报告缺口一次补上

实战向进阶 第 3/3 篇
很多人跑完策略只看点数盈亏,就以为摸透了品种表现,结果换币种一算回撤直接超标。把利润锁在点数维度,等于主动屏蔽了保证金和手数变动的真实风险。本篇要做的,正是把这部分盲区补回构建器。

「止损触发与报告输出的代码骨架」

这段逻辑处理两种退出:价格触及止损线时,系统按 SL 点数记账并回退起始时间。 具体看,当 deal_price 与 iLow 取到的低点之差折算成 point 数大于等于 SL,就判定为空头止损触发:计数加一、记一笔 -SL 的成交、亏损交易数加一、总盈利减 SL、亏损 pip 累加 SL,并把 m_start_date 顺着时间轴后移 j 根 K 线后直接 return。 报告打印函数 PrintReport 先按语言索引填 11 个标签,索引 0 走俄文、非 0 走英文;标签 0 到 10 覆盖总交易数、净利、pip 净利、绝对与最大回撤、多空胜率等。若 total_trades 为 0 直接 return,避免空跑;之后依据 profit_type 列表选中项是否为 1 走不同分支。 在 MT5 里把这段接进 EA 回测,能把「总交易=0 就跳过」和「SL 以低点差计」两处直接验证,外汇与贵金属波动大,此类硬编码止损对滑点敏感,实盘前务必用历史数据跑一遍。

MQL5 / C++
      class="kw">return;
      }
      class=class="str">"cmt">//--- Stop Loss trigger
      else if((deal_price-iLow(symbol,StringToTimeframe(tf),copied-j))/SymbolInfoDouble(symbol,SYMBOL_POINT)>=SL)
      {
         m_counter++;
         AddDeal(-SL,m_counter);
         m_report.loss_trades++;
         m_report.profit-=SL;
         m_report.loss_pips+=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=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CProgram::PrintReport(class="type">void)
{
   class="type">class="kw">string report_label[class="num">11];
   if(m_lang_index==class="num">0)
   {
      report_label[class="num">0]="Всего трейдов: ";
      report_label[class="num">1]="Чистая прибыль: ";
      report_label[class="num">2]="Прибыль в пунктах: ";
      report_label[class="num">3]="Абс.просадка баланса: ";
      report_label[class="num">4]="Макс.просадка баланса: ";
      report_label[class="num">5]="Корот.трейды/% выигр: ";
      report_label[class="num">6]="Приб.трейды/% от всех: ";
      report_label[class="num">7]="Прибыльность: ";
      report_label[class="num">8]="Фактор восстановления: ";
      report_label[class="num">9]="Длин.трейды/% выигр: ";
      report_label[class="num">10]="Убыт.трейды/% от всех: ";
   }
   else
   {
      report_label[class="num">0]="Total trades: ";
      report_label[class="num">1]="Total profit: ";
      report_label[class="num">2]="Total profit(pips): ";
      report_label[class="num">3]="Balance Drawdown Abs: ";
      report_label[class="num">4]="Balance Drawdown Max: ";
      report_label[class="num">5]="Short trades/won %: ";
      report_label[class="num">6]="Profit trades/% of all: ";
      report_label[class="num">7]="Profit Factor: ";
      report_label[class="num">8]="Recovery Factor: ";
      report_label[class="num">9]="Long trades/won %: ";
      report_label[class="num">10]="Loss trades/% of all: ";
   }
   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));
   class=class="str">"cmt">//---
   if(m_report.total_trades==class="num">0)
      class="kw">return;
   class=class="str">"cmt">//---
   if(m_profit_type.GetListViewPointer().SelectedItemIndex()==class="num">1)
   {

回测面板里的回撤与盈亏比怎么算

这段逻辑干的事很直接:遍历权益曲线数组,实时揪出历史最大利润和最大回撤。maxprofit 从 0 开始爬,每次遇到 data[i] 更大就刷新;maxdd 则用「峰值减当前」的最大值来定义,所以它抓的是水下回撤深度,不是单笔亏损。 代码里第 4 行标签把 maxdd/maxprofit*100 格式化成百分比输出,意味着回撤率相对的是「历史峰值权益」而非初始本金。若某品种 maxprofit 为 5000 点、maxdd 为 800 点,面板会显示 16.00%,这个比值偏低时策略可能倾向抗回撤能力较强,但外汇与贵金属杠杆高,实盘回撤可能偏离回测。 else 分支在没有完整曲线时只填盈亏比:profit_pips 除以 loss_pips,其余三项打「-」。注意它用 pip 而非金额算,点值波动大的贵金属交叉盘要自己换算风险。 最后 11 个文本对象统一 Update(true) 强制重绘,m_reported 置真避免重复刷。开 MT5 把这段塞进你的 CReport 类,改 ArraySize 边界或乘 100 的精度,面板数字立刻变样。

MQL5 / C++
  class="type">class="kw">double maxprofit=class="num">0.0,maxdd=class="num">0.0;
  for(class="type">int i=class="num">1; i<ArraySize(data); i++)
  {
     if(data[i]>maxprofit)
        maxprofit=data[i];
     if(maxdd<maxprofit-data[i])
        maxdd=maxprofit-data[i];
  }
  m_report_text[class="num">1].LabelText(report_label[class="num">1]+DoubleToString(data[ArraySize(data)-class="num">1],class="num">2));
  m_report_text[class="num">3].LabelText(report_label[class="num">3]+class="type">class="kw">string(data[class="num">0]-data[ArrayMinimum(data)]));
  m_report_text[class="num">4].LabelText(report_label[class="num">4]+DoubleToString(maxdd/maxprofit*class="num">100,class="num">2)+"%");
  m_report_text[class="num">7].LabelText(report_label[class="num">7]+DoubleToString(m_all_profit/m_all_losses,class="num">2));
  m_report_text[class="num">8].LabelText(report_label[class="num">8]+DoubleToString((data[ArraySize(data)-class="num">1]-data[class="num">0])/maxdd,class="num">2));
  }
  else
  {
    m_report_text[class="num">1].LabelText(report_label[class="num">1]+"-");
    m_report_text[class="num">3].LabelText(report_label[class="num">3]+"-");
    m_report_text[class="num">4].LabelText(report_label[class="num">4]+"-");
    m_report_text[class="num">7].LabelText(report_label[class="num">7]+DoubleToString(m_report.profit_pips/(class="type">class="kw">double)m_report.loss_pips,class="num">2));
  }
  m_report_text[class="num">2].LabelText(report_label[class="num">2]+class="type">class="kw">string(m_report.profit));
  m_report_text[class="num">5].LabelText(report_label[class="num">5]+class="type">class="kw">string(m_report.short_trades)+"/"+DoubleToString(m_report.profit_short/(class="type">class="kw">double)m_report.short_trades*class="num">100,class="num">1)+"%");
  m_report_text[class="num">6].LabelText(report_label[class="num">6]+class="type">class="kw">string(m_report.profit_trades)+"/"+DoubleToString(m_report.profit_trades/(class="type">class="kw">double)m_report.total_trades*class="num">100,class="num">1)+"%");
  m_report_text[class="num">9].LabelText(report_label[class="num">9]+class="type">class="kw">string(m_report.long_trades)+"/"+DoubleToString(m_report.profit_long/(class="type">class="kw">double)m_report.long_trades*class="num">100,class="num">1)+"%");
  m_report_text[class="num">10].LabelText(report_label[class="num">10]+class="type">class="kw">string(m_report.loss_trades)+"/"+DoubleToString(m_report.loss_trades/(class="type">class="kw">double)m_report.total_trades*class="num">100,class="num">1)+"%");
class=class="str">"cmt">//---
  for(class="type">int i=class="num">0; i<class="num">11; i++)
    m_report_text[i].Update(true);
  m_reported=true;
}

◍ 策略构建器跑通测试的实际操作点

应用更新后,用户手册要同步改,但跑测试的链路本身变化不大。界面语言可改可不改,指标参数有默认值,只动你需要的那几个;品种表默认把市场观察里名字带 USD 的筛出来,去掉勾选就能看全部可用品种。 时间周期和时间帧这两步没动。信号面板做了视觉收敛:禁掉买或卖其中一种信号,相关设置整组隐藏,不会占界面空间。止盈止损逻辑也没改。 利润类型切换是实测要留意的地方:选“点数”,报告里结果按品种点数显示;选“货币”会多出“手数类型 / 手数 / 初始存款”三项,手数类型可选常量或按余额算,直接影响测试里的仓位计算。 步骤 1–8 走完,在表里左键点品种开始测,结果进“报告”,再点开图形看细节。自定义指标参数别写错:缓冲区编号、名称、参数用逗号隔,只认数字值;名称是相对 MQL5/Indicators/ 的路径,子目录要写成 "Examples\\indicator_name",单斜杠换双反斜杠。 常见卡点都有提示弹窗:同一形态触发双向信号、没先测就开图形、测试起止日期填错。外汇与贵金属品种波动剧烈、杠杆风险高,下载对应历史数据再测,否则美林形态类策略可能直接跑不出结果。

「别急着下结论」

把前面几篇攒下的 MQL5 文件包解压后,根目录里必须出现 MQL5 文件夹,且直接丢进 MT5 终端根目录才能跑通。最快的打开方式是在终端里按 Ctrl+Shift+D,或者用右键关联菜单定位,图例里标的位置和实际路径一致。 整套压缩包体积约 1936.58 KB,涵盖语音通知、手动交易工具箱、多币种信号监视等模块的源码;外汇与贵金属行情受杠杆和滑点影响大,直接挂实盘前先在策略测试器里用历史数据验证一遍更稳妥。 作者留过话:半自动和全自动专家模式是后续计划,当前版本只解决文件落地和目录结构。装完别以为就能无脑跟单,先按自己品种调参再说。

把回测报告交给小布盯盘
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到按存款币种的回撤与恢复因子,不必自己拼原型界面。

常见问题

按点数只表达价格跳动盈亏,不反映保证金占用和手数影响;按存款币种则把净利润、绝对回撤、最大回撤和恢复因子都算进账户货币,更贴近实盘资金曲线,外汇贵金属波动大需重点看后者。
常数手数在震荡市容易放大回撤,基于余额的手数会随资金曲线伸缩,可能降低连续亏损期的暴露,但趋势市里会牺牲部分复利速度,倾向按风险预算选。
可以,打开图表后标题会标出所测品种、时间帧与测试区间,存款走势和每笔交易结果叠加显示,方便快速判断形态在哪些时段失效。
能,小布的品种页已集成按币种回测与图形摘要,你导入形态参数就能看到恢复因子和胜率分布,省去自己改 GUI 的成本。
合理,因为它用净利润除以回撤得到,而回撤在点数模式下没有账户货币基准,数值会失真,所以限定币种模式才能给出可比较的恢复能力。