让手动回测变得简单:为MQL5策略测试器构建自定义工具包·综合运用
(3/3)·手动测速慢、自动化又太死?用 MQL5 自建按钮式 EA,在测试器里按你的节奏下单平仓
◍ 用按钮微调止盈与一键清场
在 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。外汇与贵金属杠杆高,这种面板误操作可能瞬间成交,建议在策略测试器里先跑通按钮状态机再上真仓。
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) 读出当前值,后续通常做减法并做下限保护,上机跑一下能把边界表现摸清楚。
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 循环。
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 跑通单周期模拟,再谈扩展。