让手动回测变得简单:为MQL5策略测试器构建自定义工具包·综合运用
🧰

让手动回测变得简单:为MQL5策略测试器构建自定义工具包·综合运用

(3/3)·手动测速慢、自动化又太死?用 MQL5 自建按钮式 EA,在测试器里按你的节奏下单平仓

案例拆解新手友好 第 3/3 篇
很多人卡在回测这一步:写全自动 EA 费时,纯手动又追不上 K 线。其实策略测试器能跑交互式程序——挂上按钮就能边看边点,速度还是加速的。

◍ 用按钮微调止盈与一键清场

在 MT5 面板里,止盈线不是死值。点一下小加仓按钮 BTN_TP1P,HL_TP 水平线就沿 slow_pts*_Point 上移一档;点大加仓按钮 BTN_TP2P 则按 fast_pts*_Point 跳一截,两者都只是改 OBJPROP_PRICE 后 ChartRedraw 刷新,不改底层逻辑。 DeleteObjects_SLTP 这个函数把 15 个图形对象一次性 ObjectDelete 清掉:从 HL_SL / HL_TP 两条价格线,到 BTN_SL1M~BTN_TP2P 十二个加减按钮,再到 BTN_YES / BTN_NO / BTN_IDLE 确认与闲置控件,最后 ChartRedraw 让图表回归干净。 实盘触发用 GetState(BTN_BUY) && GetState(BTN_YES) 做双确认:obj_Trade.Buy 吃进当前 LOT、Ask、SL、TP 四个值,下单后立刻 DeleteObjects_SLTP 并置 isHaveTradeLevels=false。外汇与贵金属杠杆高,这种面板误操作可能瞬间成交,建议在策略测试器里先跑通按钮状态机再上真仓。

MQL5 / C++
if (GetState(BTN_TP1P)){ class=class="str">"cmt">//--- Check if the small Take Profit increase button is clicked
      ObjectSetDouble(class="num">0,HL_TP,OBJPROP_PRICE,GetValueHL(HL_TP)+slow_pts*_Point); class=class="str">"cmt">//--- Move the Take Profit up by a small amount
      ObjectSetInteger(class="num">0,BTN_TP1P,OBJPROP_STATE,false); class=class="str">"cmt">//--- Turn off the button press state
      ChartRedraw(class="num">0); class=class="str">"cmt">//--- Refresh the chart to show the change
  }
  if (GetState(BTN_TP2P)){ class=class="str">"cmt">//--- Check if the large Take Profit increase button is clicked
      ObjectSetDouble(class="num">0,HL_TP,OBJPROP_PRICE,GetValueHL(HL_TP)+fast_pts*_Point); class=class="str">"cmt">//--- Move the Take Profit up by a large amount
      ObjectSetInteger(class="num">0,BTN_TP2P,OBJPROP_STATE,false); class=class="str">"cmt">//--- Turn off the button press state
      ChartRedraw(class="num">0); class=class="str">"cmt">//--- Refresh the chart to show the change
  }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Delete objects function                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void DeleteObjects_SLTP(){
   ObjectDelete(class="num">0,HL_SL); class=class="str">"cmt">//--- Remove the Stop Loss line from the chart
   ObjectDelete(class="num">0,HL_TP); class=class="str">"cmt">//--- Remove the Take Profit line from the chart
   ObjectDelete(class="num">0,BTN_SL); class=class="str">"cmt">//--- Remove the Stop Loss display button
   ObjectDelete(class="num">0,BTN_TP); class=class="str">"cmt">//--- Remove the Take Profit display button
   ObjectDelete(class="num">0,BTN_SL1M); class=class="str">"cmt">//--- Remove the small Stop Loss decrease button
   ObjectDelete(class="num">0,BTN_SL2M); class=class="str">"cmt">//--- Remove the large Stop Loss decrease button
   ObjectDelete(class="num">0,BTN_SL1P); class=class="str">"cmt">//--- Remove the small Stop Loss increase button
   ObjectDelete(class="num">0,BTN_SL2P); class=class="str">"cmt">//--- Remove the large Stop Loss increase button
   ObjectDelete(class="num">0,BTN_TP1P); class=class="str">"cmt">//--- Remove the small Take Profit increase button
   ObjectDelete(class="num">0,BTN_TP2P); class=class="str">"cmt">//--- Remove the large Take Profit increase button
   ObjectDelete(class="num">0,BTN_TP2M); class=class="str">"cmt">//--- Remove the large Take Profit decrease button
   ObjectDelete(class="num">0,BTN_TP1M); class=class="str">"cmt">//--- Remove the small Take Profit decrease button
   ObjectDelete(class="num">0,BTN_YES); class=class="str">"cmt">//--- Remove the confirm trade button
   ObjectDelete(class="num">0,BTN_NO); class=class="str">"cmt">//--- Remove the cancel trade button
   ObjectDelete(class="num">0,BTN_IDLE); class=class="str">"cmt">//--- Remove the idle button

   ChartRedraw(class="num">0); class=class="str">"cmt">//--- Refresh the chart to show all objects removed
}
class=class="str">"cmt">// BUY ORDER PLACEMENT
if (GetState(BTN_BUY) && GetState(BTN_YES)){ class=class="str">"cmt">//--- Check if both Buy and Yes buttons are clicked
   obj_Trade.Buy(class="type">class="kw">double(GetValue(BTN_LOT)),_Symbol,Ask,GetValueHL(HL_SL),GetValueHL(HL_TP)); class=class="str">"cmt">//--- Place a Buy order with set lot size, SL, and TP
   DeleteObjects_SLTP(); class=class="str">"cmt">//--- Remove all trade level objects from the chart
   isHaveTradeLevels = false; class=class="str">"cmt">//--- Mark that trade levels are no longer present

「卖单触发与手数微调的按钮逻辑」

在 MT5 面板里,卖单的落地依赖于 Sell 与 Yes 两个按钮同时处于按下态:GetState(BTN_SELL) && GetState(BTN_YES) 成立后才调用 obj_Trade.Sell(),以 GetValue(BTN_LOT) 的手数、Bid 报价、GetValueHL 算出的止损止盈挂单。下单后立刻 DeleteObjects_SLTP() 清掉图表上的辅助线,并把 isHaveTradeLevels 置 false,避免重复识别。 取消操作走 BTN_NO:一旦按下,同样删除 SL/TP 对象、复位 Buy/Sell/No 三个按钮的 OBJPROP_STATE,tradeInAction 标为 false,ChartRedraw(0) 刷新。这里有个细节,No 不会单独清 Yes,所以 Yes 的复位要靠前面卖/买分支或手动逻辑兜底,实盘前建议在 MT5 里点一遍看是否残留高亮。 手数加按钮 BTN_P 的逻辑值得单独看:它取 SYMBOL_VOLUME_STEP 作为最小步进,newLot += lotStep 后 NormalizeDouble 到 2 位小数。但那行 newLot = newLot > 0.1 ? lotStep : newLot 写法有点反直觉——手数一旦超过 0.1 就直接弹回 step 值,相当于把上限卡死在 0.1 附近,外汇与贵金属杠杆高,这种硬上限可能不符合你的仓位管理,复制代码时最好改成自己账户允许的最大值。 手数减按钮 BTN_M 分支开头与 BTN_P 对称,也是先 (double)GetValue(BTN_LOT) 读出当前值,后续通常做减法并做下限保护,上机跑一下能把边界表现摸清楚。

MQL5 / C++
  ObjectSetInteger(class="num">0,BTN_YES,OBJPROP_STATE,false); class=class="str">"cmt">//--- Turn off the Yes button press state
  ObjectSetInteger(class="num">0,BTN_BUY,OBJPROP_STATE,false); class=class="str">"cmt">//--- Turn off the Buy button press state
  tradeInAction = false; class=class="str">"cmt">//--- Mark the trade setup as complete
  ChartRedraw(class="num">0); class=class="str">"cmt">//--- Refresh the chart to reflect changes
}
class=class="str">"cmt">// SELL ORDER PLACEMENT
else if (GetState(BTN_SELL) && GetState(BTN_YES)){ class=class="str">"cmt">//--- Check if both Sell and Yes buttons are clicked
  obj_Trade.Sell(class="type">class="kw">double(GetValue(BTN_LOT)),_Symbol,Bid,GetValueHL(HL_SL),GetValueHL(HL_TP)); class=class="str">"cmt">//--- Place a Sell order with set lot size, SL, and TP
  DeleteObjects_SLTP(); class=class="str">"cmt">//--- Remove all trade level objects from the chart
  isHaveTradeLevels = false; class=class="str">"cmt">//--- Mark that trade levels are no longer present
  ObjectSetInteger(class="num">0,BTN_YES,OBJPROP_STATE,false); class=class="str">"cmt">//--- Turn off the Yes button press state
  ObjectSetInteger(class="num">0,BTN_SELL,OBJPROP_STATE,false); class=class="str">"cmt">//--- Turn off the Sell button press state
  tradeInAction = false; class=class="str">"cmt">//--- Mark the trade setup as complete
  ChartRedraw(class="num">0); class=class="str">"cmt">//--- Refresh the chart to reflect changes
}
else if (GetState(BTN_NO)){ class=class="str">"cmt">//--- Check if the No button is clicked to cancel
  DeleteObjects_SLTP(); class=class="str">"cmt">//--- Remove all trade level objects from the chart
  isHaveTradeLevels = false; class=class="str">"cmt">//--- Mark that trade levels are no longer present
  ObjectSetInteger(class="num">0,BTN_NO,OBJPROP_STATE,false); class=class="str">"cmt">//--- Turn off the No button press state
  ObjectSetInteger(class="num">0,BTN_BUY,OBJPROP_STATE,false); class=class="str">"cmt">//--- Turn off the Buy button press state
  ObjectSetInteger(class="num">0,BTN_SELL,OBJPROP_STATE,false); class=class="str">"cmt">//--- Turn off the Sell button press state
  tradeInAction = false; class=class="str">"cmt">//--- Mark the trade setup as canceled
  ChartRedraw(class="num">0); class=class="str">"cmt">//--- Refresh the chart to reflect changes
}

if (GetState(BTN_P)==true){ class=class="str">"cmt">//--- Check if the lot size increase button is clicked
  class="type">class="kw">double newLot = (class="type">class="kw">double)GetValue(BTN_LOT); class=class="str">"cmt">//--- Get the current lot size as a number
  class="type">class="kw">double lotStep = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP); class=class="str">"cmt">//--- Get the minimum lot size change allowed
  newLot += lotStep; class=class="str">"cmt">//--- Increase the lot size by one step
  newLot = NormalizeDouble(newLot,class="num">2); class=class="str">"cmt">//--- Round the new lot size to class="num">2 decimal places
  newLot = newLot > class="num">0.1 ? lotStep : newLot; class=class="str">"cmt">//--- Ensure lot size doesn&class="macro">#x27;t exceed class="num">0.1, otherwise reset to step
  ObjectSetString(class="num">0,BTN_LOT,OBJPROP_TEXT,class="type">class="kw">string(newLot)); class=class="str">"cmt">//--- Update the lot size display with the new value
  ObjectSetInteger(class="num">0,BTN_P,OBJPROP_STATE,false); class=class="str">"cmt">//--- Turn off the increase button press state
  ChartRedraw(class="num">0); class=class="str">"cmt">//--- Refresh the chart to show the new lot size
}
if (GetState(BTN_M)==true){ class=class="str">"cmt">//--- Check if the lot size decrease button is clicked
  class="type">class="kw">double newLot = (class="type">class="kw">double)GetValue(BTN_LOT); class=class="str">"cmt">//--- Get the current lot size as a number

手调手数下限与一键清仓的落地写法

在 MT5 面板里做减仓按钮时,手数不能无限往下减。先取 SYMBOL_VOLUME_STEP 拿到该品种最小变动步长,newLot 减去一步后必须用 NormalizeDouble 规整到 2 位小数,否则有些经纪商报价精度会让挂单被拒。 下面这段把下限锁死在 lotStep:小于步长就强制拉回步长,同时把按钮文字和按下状态复位,ChartRedraw 刷新界面。外汇与贵金属杠杆高,手数误设可能放大回撤,实盘前建议在策略测试器用迷你账户验证。 double lotStep = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP); // 取当前品种最小手数步长 newLot -= lotStep; // 手数减一个步长 newLot = NormalizeDouble(newLot,2); // 规整到2位小数 newLot = newLot < lotStep ? lotStep : newLot; // 低于步长则取步长 ObjectSetString(0,BTN_LOT,OBJPROP_TEXT,string(newLot)); // 更新按钮显示 ObjectSetInteger(0,BTN_M,OBJPROP_STATE,false); // 解除减仓按钮按下态 ChartRedraw(0); // 重绘图表 } 清仓函数则从 PositionsTotal()-1 倒序遍历,用 PositionGetTicket 拿ticket,PositionSelectByTicket 选中后比对 POSITION_SYMBOL 是否为当前图符号,匹配才调 PositionClose。倒序是为了避免平仓后仓位索引错位漏单。 void closeAllPositions(){ for (int i=PositionsTotal()-1; i>=0; i--){ // 从末位向前遍历 ulong ticket = PositionGetTicket(i); // 取该位持仓ticket if (ticket > 0){ // ticket有效才继续 if (PositionSelectByTicket(ticket)){ // 按ticket选中持仓 if (PositionGetString(POSITION_SYMBOL)==_Symbol){ // 符号一致才关 obj_Trade.PositionClose(ticket); // 平掉该仓 } } } } } 外部按钮事件里判 GetState(BTN_CLOSE)==true 就跑 closeAllPositions,随后清状态并重绘。MT5 里若同时有挂单,这段代码不会动挂单,只清市价持仓,必要时要另写 OrdersTotal 循环。

MQL5 / C++
class="type">class="kw">double lotStep = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP); class=class="str">"cmt">//--- Get the minimum lot size change allowed
  newLot -= lotStep; class=class="str">"cmt">//--- Decrease the lot size by one step
  newLot = NormalizeDouble(newLot,class="num">2); class=class="str">"cmt">//--- Round the new lot size to class="num">2 decimal places
  newLot = newLot < lotStep ? lotStep : newLot; class=class="str">"cmt">//--- Ensure lot size doesn&class="macro">#x27;t go below minimum, otherwise set to step
  ObjectSetString(class="num">0,BTN_LOT,OBJPROP_TEXT,class="type">class="kw">string(newLot)); class=class="str">"cmt">//--- Update the lot size display with the new value
  ObjectSetInteger(class="num">0,BTN_M,OBJPROP_STATE,false); class=class="str">"cmt">//--- Turn off the decrease button press state
  ChartRedraw(class="num">0); class=class="str">"cmt">//--- Refresh the chart to show the new lot size
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Close all positions function                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void closeAllPositions(){
   for (class="type">int i=PositionsTotal()-class="num">1; i>=class="num">0; i--){ class=class="str">"cmt">//--- Loop through all open positions, starting from the last one
      class="type">ulong ticket = PositionGetTicket(i); class=class="str">"cmt">//--- Get the ticket number of the current position
      if (ticket > class="num">0){ class=class="str">"cmt">//--- Check if the ticket is valid
         if (PositionSelectByTicket(ticket)){ class=class="str">"cmt">//--- Select the position by its ticket number
            if (PositionGetString(POSITION_SYMBOL)==_Symbol){ class=class="str">"cmt">//--- Check if the position is for the current chart symbol
               obj_Trade.PositionClose(ticket); class=class="str">"cmt">//--- Close the selected position
            }
         }
      }
   }
}
if (GetState(BTN_CLOSE)==true){ class=class="str">"cmt">//--- Check if the close all positions button is clicked
   closeAllPositions(); class=class="str">"cmt">//--- Close all open trades
   ObjectSetInteger(class="num">0,BTN_CLOSE,OBJPROP_STATE,false); class=class="str">"cmt">//--- Turn off the close button press state
   ChartRedraw(class="num">0); class=class="str">"cmt">//--- Refresh the chart to reflect closed positions
}

◍ 策略测试器里跑一遍工具包

在 MT5 的策略测试器里直接加载这套工具包程序,选好预设参数后启动,能看到买卖与调节按钮以极高频率响应。实测中点击买入或卖出后,止损、止盈、手数均可即时微调,再用“是/否”确认或撤销,整个过程无明显延迟。 “恐慌”按钮在测试环境里可一键平掉所有挂单与持仓,适合极端波动时手动干预。外汇与贵金属杠杆高,回测顺滑不代表实盘无滑点,实盘前建议先用模拟盘验证按钮逻辑。 下方 GIF 记录的是测试器内的运行画面:按钮触发、参数修正、批量关闭均按预期执行,可作为你本地复现的参照。

「别急着下结论」

这个手动回测工具包把图表上的手动操作和策略测试器的快进能力接到了一起,核心文件 Manual_Backtest_in_Strategy_Tester.mq5 体积 42.72 KB,可直接在 MT5 里加载验证。它用按钮控制下单与参数微调,省掉了反复拖品种、手填时间的机械劳动。 但要注意,作者在讨论区明确回复过:当前版本只支持单一时间框架,多周期回测暂时做不到。你在 MT5 里改 lotsize、tp/sl 想让它在测试器自动发单,大概率会像评论区 zigooo 那样卡住——真实账户能跑,回测模式不认。 外汇与贵金属杠杆高、滑点跳空频繁,回测顺不代表实盘稳。先拿这个 mq5 跑通单周期模拟,再谈扩展。

把重复劳动交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到;回测时若想比对实盘盘口异动,小布盯盘帮你把多周期信号一并铺开,你专注决策。

常见问题

检查 ChartObjects 的创建权限与子窗口坐标,测试器可视化模式下需显式调用 ObjectCreate 并刷新图表。
可以,通过 BTN_P 与 BTN_M 微调,或在 input 里改 init_lot,但外汇贵金属波动大,小仓位试错概率上更稳。
用 CTrade 的 PositionClose 循环遍历当前账户持仓,正常情况下全平;极端滑点可能残留,需日志确认。
小布盯盘目前提供信号诊断与品种页视图,EA 需自行在终端编译部署;但回测结论可对照小布的多周期提示交叉验证。
能,按钮触发与具体入场逻辑解耦,你用形态或指标判断,点 BUY/SELL 即可,测试器照常加速推进。