如何使用 Controls 类创建交互式 MQL5 仪表盘/面板(第 2 部分):添加按钮响应。·进阶篇
「面板按钮的事件分支与下单前置取值」
在 MT5 自定义面板里,图表对象点击事件靠 sparam 匹配按钮名来分流。上面这段逻辑覆盖了信息按钮、关闭叉号以及卖单按钮三种分支,每一支都先 Print 出被点对象名,方便你在终端日志里确认命中情况。
信息按钮分支会把交易与平仓按钮背景刷成 clrSilver、边框也置银,自己保留黄底黄边,随后销毁另外两个区块、重建信息区块;叉号分支则连续调用 destroySection_Trade/Close/Information 再拆主面板,等于整块 UI 卸载。
卖单分支里先取 SYMBOL_ASK 和 SYMBOL_BID,用 NormalizeDouble(..., _Digits) 按品种精度截断,再从编辑框 obj_Edit_LOTS.Text() 经 StringToDouble 拿手填手数——这三行是后续发单的必填前奏,缺任一都会让 CTrade 报参数错。
外汇与贵金属杠杆高,面板点错方向或手数填错可能在秒级扩大浮亏,真机跑之前建议先把 Print 日志打开、用策略测试器点一遍按钮看对象名回显。
else if (sparam==obj_Btn_INFO.Name()){ Print("OBJECT CLICKED = ", obj_Btn_INFO.Name()); obj_Btn_TRADE.Pressed(false); obj_Btn_CLOSE.Pressed(false); obj_Btn_INFO.Pressed(false); obj_Btn_TRADE.ColorBackground(clrSilver); obj_Btn_CLOSE.ColorBackground(clrSilver); obj_Btn_INFO.ColorBackground(clrYellow); obj_Btn_TRADE.ColorBorder(clrSilver); obj_Btn_CLOSE.ColorBorder(clrSilver); obj_Btn_INFO.ColorBorder(clrYellow); destroySection_Trade(); destroySection_Close(); createSection_Information(); } else if (sparam==obj_Btn_X.Name()){ Print("OBJECT CLICKED = ", obj_Btn_X.Name()); destroySection_Trade(); destroySection_Close(); destroySection_Information(); destroySection_Main_Panel(); } else if (sparam==obj_Btn_SELL.Name()){ Print("OBJECT CLICKED = ",obj_Btn_SELL.Name()); class="type">class="kw">double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits); class="type">class="kw">double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits); class="type">class="kw">double lots = StringToDouble(obj_Edit_LOTS.Text()); }
挂单与市价单的按钮下单逻辑拆解
在 MT5 面板里点不同按钮,本质是把界面输入转成 CTrade 的对应方法调用。卖按钮先取 Bid 作入场,SL 放在 Ask 上方、TP 放在 Ask 下方,和买按钮的方向完全镜像。 double entry_price = Bid; // 卖单入场直接用当前买价 double stopLoss = Ask+StringToDouble(obj_Edit_SL.Text())*_Point; // 止损距用户输入点数,加在卖价之上 double takeprofit = Ask-StringToDouble(obj_Edit_TP.Text())*_Point; // 止盈距用户输入点数,减在卖价之下 Print("Lots = ",lots,", Entry = ",entry_price,", SL = ",stopLoss,", TP = ",takeprofit); // 先打印核对再发单 obj_Trade.Sell(lots,_Symbol,entry_price,stopLoss,takeprofit); // 执行市价卖 买按钮里 Ask、Bid 都做了 NormalizeDouble 到 _Digits 精度,避免 XAUUSD 这类小数位多的品种出现报价越界报错。SL 用 Bid 减点数、TP 用 Bid 加点数,和卖单计算式对称。 double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits); // 归一化卖价 double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits); // 归一化买价 double lots = StringToDouble(obj_Edit_LOTS.Text()); // 手数来自输入框 obj_Trade.Buy(lots,_Symbol,entry_price,stopLoss,takeprofit); // 执行市价买 SellStop 分支则额外读 obj_Edit_PRICE.Text() 作为挂单触发价,不再用实时 Bid/Ask 作 entry。外汇与贵金属杠杆高,这类一键发单在跳空时可能滑点放大,实盘前务必在策略测试器用 2023 年 XAUUSD 的 1 分钟数据跑一遍按钮事件。
class="type">class="kw">double entry_price = Bid; class=class="str">"cmt">//--- Set the entry price for selling to the current bid price class="type">class="kw">double stopLoss = Ask+StringToDouble(obj_Edit_SL.Text())*_Point; class=class="str">"cmt">//--- Calculate stop loss based on user input class="type">class="kw">double takeprofit = Ask-StringToDouble(obj_Edit_TP.Text())*_Point; class=class="str">"cmt">//--- Calculate take profit based on user input Print("Lots = ",lots,", Entry = ",entry_price,", SL = ",stopLoss,", TP = ",takeprofit); class=class="str">"cmt">//--- Log order details obj_Trade.Sell(lots,_Symbol,entry_price,stopLoss,takeprofit); class=class="str">"cmt">//--- Execute the sell order } else if (sparam==obj_Btn_BUY.Name()){ class=class="str">"cmt">//--- Check if the Buy button is clicked Print("OBJECT CLICKED = ",obj_Btn_BUY.Name()); class=class="str">"cmt">//--- Log the button click event class="type">class="kw">double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits); class=class="str">"cmt">//--- Get and normalize the ask price class="type">class="kw">double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits); class=class="str">"cmt">//--- Get and normalize the bid price class="type">class="kw">double lots = StringToDouble(obj_Edit_LOTS.Text()); class=class="str">"cmt">//--- Get the lot size from input field class="type">class="kw">double entry_price = Ask; class=class="str">"cmt">//--- Set the entry price for buying to the current ask price class="type">class="kw">double stopLoss = Bid-StringToDouble(obj_Edit_SL.Text())*_Point; class=class="str">"cmt">//--- Calculate stop loss based on user input class="type">class="kw">double takeprofit = Bid+StringToDouble(obj_Edit_TP.Text())*_Point; class=class="str">"cmt">//--- Calculate take profit based on user input Print("Lots = ",lots,", Entry = ",entry_price,", SL = ",stopLoss,", TP = ",takeprofit); class=class="str">"cmt">//--- Log order details obj_Trade.Buy(lots,_Symbol,entry_price,stopLoss,takeprofit); class=class="str">"cmt">//--- Execute the buy order } else if (sparam==obj_Btn_SELLSTOP.Name()){ class=class="str">"cmt">//--- Check if the Sell Stop button is clicked Print("OBJECT CLICKED = ",obj_Btn_SELLSTOP.Name()); class=class="str">"cmt">//--- Log the button click event class="type">class="kw">double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits); class=class="str">"cmt">//--- Get and normalize the ask price class="type">class="kw">double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits); class=class="str">"cmt">//--- Get and normalize the bid price class="type">class="kw">double user_price = StringToDouble(obj_Edit_PRICE.Text()); class=class="str">"cmt">//--- Get the user-defined price from input field
◍ 挂单前的止损位合规校验
在 MT5 里用面板下挂单,最容易被 broker 拒的单子往往是止损距离不够。每个品种都有 SYMBOL_TRADE_STOPS_LEVEL,返回的是以 point 为单位的整数,代表挂单价格离市价的最小合法距离。 卖stop要挂在 Bid 下方,合法价 = Bid - stopslevel*_Point;买stop要挂在 Ask 上方,合法价 = Ask + stopslevel*_Point。用户填的 price 若越界,直接 Print 报错而不发单,能避免 CTrade 返回『断线式』失败。 下面这段是卖stop分支的校验与发单逻辑,买stop仅把 valid_price 改成 Ask 加号、比较符号反向。外汇与贵金属杠杆高,stopslevel 在新闻市可能瞬间扩大,发单前必须实时取,不能缓存。
class="type">long stopslevel = SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL); class=class="str">"cmt">//--- 取当前品种最小止损级别(point数) class="type">class="kw">double valid_price = Bid - stopslevel*_Point; class=class="str">"cmt">//--- 计算卖stop允许的最低挂单价 if (user_price > valid_price){ class=class="str">"cmt">//--- 用户价高于合法价则无效 Print("ERROR: INVALID STOPS PRICE. ",user_price," > ",valid_price); class=class="str">"cmt">//--- 打印错误 } else if (user_price <= valid_price){ class=class="str">"cmt">//--- 价格合法才继续 class="type">class="kw">double lots = StringToDouble(obj_Edit_LOTS.Text()); class=class="str">"cmt">//--- 从编辑框读手数 class="type">class="kw">double entry_price = user_price; class=class="str">"cmt">//--- 挂单入场价 class="type">class="kw">double stopLoss = user_price+StringToDouble(obj_Edit_SL.Text())*_Point; class=class="str">"cmt">//--- 止损=挂单价+用户输入点数 class="type">class="kw">double takeprofit = user_price-StringToDouble(obj_Edit_TP.Text())*_Point; class=class="str">"cmt">//--- 止盈=挂单价-用户输入点数 Print("Lots = ",lots,", Entry = ",entry_price,", SL = ",stopLoss,", TP = ",takeprofit); class=class="str">"cmt">//--- 日志 obj_Trade.SellStop(lots,entry_price,_Symbol,stopLoss,takeprofit); class=class="str">"cmt">//--- 下卖stop } else if (sparam==obj_Btn_BUYSTOP.Name()){ class=class="str">"cmt">//--- 买stop按钮分支 Print("OBJECT CLICKED = ",obj_Btn_BUYSTOP.Name()); class=class="str">"cmt">//--- 记录点击 class="type">class="kw">double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits); class=class="str">"cmt">//--- 规范ask class="type">class="kw">double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits); class=class="str">"cmt">//--- 规范bid class="type">class="kw">double user_price = StringToDouble(obj_Edit_PRICE.Text()); class=class="str">"cmt">//--- 读用户价 class="type">long stopslevel = SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL); class=class="str">"cmt">//--- 再取一次止损级别 class="type">class="kw">double valid_price = Ask + stopslevel*_Point; class=class="str">"cmt">//--- 买stop合法价在ask上方 if (user_price < valid_price){ class=class="str">"cmt">//--- 用户价低于合法价无效 Print("ERROR: INVALID STOPS PRICE. ",user_price," < ",valid_price); class=class="str">"cmt">//--- 打印错误 }
「挂单按钮里的价格校验与 BuyStop 派发」
这段逻辑承接上一个按钮分支,处理用户点击 Sell Limit 之前的 Buy Stop 挂单。先判断用户输入价 user_price 是否大于等于 valid_price,valid_price 由当前 Bid 加上 SYMBOL_TRADE_STOPS_LEVEL 个点构成,低于它就直接 Print 报错不下单。 校验通过后,从三个输入框分别取手数、SL、TP 的字符值并转 double。SL 用 user_price 减去输入框点数乘 _Point,TP 用 user_price 加上点数乘 _Point,最后 Print 出 Lots / Entry / SL / TP 四组数供终端排查,再调 obj_Trade.BuyStop 发出挂单。 Sell Limit 分支开头先抓 Ask、Bid 并 NormalizeDouble 到 _Digits,valid_price 变成 Bid + stopslevel*_Point。若 user_price < valid_price 同样打印 INVALID STOPS PRICE;通过后才取 lots 与 entry_price,SL 改为 user_price 加点数乘 _Point——和 Buy Stop 方向相反,外汇与贵金属品种点值波动大,stopslevel 距离可能随时被券商调整,实盘前务必在 MT5 符号规格里核对一次。
else if (user_price >= valid_price){ class=class="str">"cmt">//--- If the user-defined price is valid class="type">class="kw">double lots = StringToDouble(obj_Edit_LOTS.Text()); class=class="str">"cmt">//--- Get the lot size from input field class="type">class="kw">double entry_price = user_price; class=class="str">"cmt">//--- Set the entry price for the buy stop order class="type">class="kw">double stopLoss = user_price-StringToDouble(obj_Edit_SL.Text())*_Point; class=class="str">"cmt">//--- Calculate stop loss based on user input class="type">class="kw">double takeprofit = user_price+StringToDouble(obj_Edit_TP.Text())*_Point; class=class="str">"cmt">//--- Calculate take profit based on user input Print("Lots = ",lots,", Entry = ",entry_price,", SL = ",stopLoss,", TP = ",takeprofit); class=class="str">"cmt">//--- Log order details obj_Trade.BuyStop(lots,entry_price,_Symbol,stopLoss,takeprofit); class=class="str">"cmt">//--- Execute the buy stop order } } else if (sparam==obj_Btn_SELLLIMIT.Name()){ class=class="str">"cmt">//--- Check if the Sell Limit button is clicked Print("OBJECT CLICKED = ",obj_Btn_SELLLIMIT.Name()); class=class="str">"cmt">//--- Log the button click event class="type">class="kw">double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits); class=class="str">"cmt">//--- Get and normalize the ask price class="type">class="kw">double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits); class=class="str">"cmt">//--- Get and normalize the bid price class="type">class="kw">double user_price = StringToDouble(obj_Edit_PRICE.Text()); class=class="str">"cmt">//--- Get the user-defined price from input field class="type">long stopslevel = SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL); class=class="str">"cmt">//--- Get the minimum stop level for the symbol class="type">class="kw">double valid_price = Bid + stopslevel*_Point; class=class="str">"cmt">//--- Calculate the valid price for placing a sell limit order if (user_price < valid_price){ class=class="str">"cmt">//--- Check if the user-defined price is valid Print("ERROR: INVALID STOPS PRICE. ",user_price," < ",valid_price); class=class="str">"cmt">//--- Log an error message } else if (user_price >= valid_price){ class=class="str">"cmt">//--- If the user-defined price is valid class="type">class="kw">double lots = StringToDouble(obj_Edit_LOTS.Text()); class=class="str">"cmt">//--- Get the lot size from input field class="type">class="kw">double entry_price = user_price; class=class="str">"cmt">//--- Set the entry price for the sell limit order class="type">class="kw">double stopLoss = user_price+StringToDouble(obj_Edit_SL.Text())*_Point; class=class="str">"cmt">//--- Calculate stop loss based on user input
挂单按钮背后的价格校验逻辑
在 MT5 面板里点 SellLimit 或 BuyLimit 按钮,本质是把输入框里的价格、手数、止损点数翻译成 CTrade 的下单调用。以 BuyLimit 为例,代码先取 SYMBOL_ASK 和 SYMBOL_BID 并 NormalizeDouble 到 _Digits 精度,再读 obj_Edit_PRICE 的文本作为挂单价。 关键不在下单,而在 stopslevel 校验:用 SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL) 拿到经纪商要求的最小止损距离(以点数计),valid_price = Ask - stopslevel*_Point。若 user_price > valid_price,直接 Print 报错不下单;只有 user_price <= valid_price 才继续。外汇与贵金属杠杆高,stopslevel 常随波动率跳变,忽略这步会被服务器拒单。 SellLimit 一侧镜像处理:takeprofit = user_price - 输入TP点数*_Point,stopLoss = user_price + 输入SL点数*_Point,与 BuyLimit 方向相反。开 MT5 把下面片段塞进你的面板回调,改 obj_Edit_* 名字就能跑。
class="type">class="kw">double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits); class=class="str">"cmt">//--- 取卖价并规整到品种小数位 class="type">class="kw">double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits); class=class="str">"cmt">//--- 取买价并规整到品种小数位 class="type">class="kw">double user_price = StringToDouble(obj_Edit_PRICE.Text()); class=class="str">"cmt">//--- 从输入框读用户挂单价 class="type">long stopslevel = SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL); class=class="str">"cmt">//--- 取最小止损级别(点) class="type">class="kw">double valid_price = Ask - stopslevel*_Point; class=class="str">"cmt">//--- 计算买限价单允许的最远挂单价 if (user_price > valid_price){ class=class="str">"cmt">//--- 用户价非法(离市价太近) Print("ERROR: INVALID STOPS PRICE. ",user_price," > ",valid_price); class=class="str">"cmt">//--- 打印错误 } else if (user_price <= valid_price){ class=class="str">"cmt">//--- 价格合法 class="type">class="kw">double lots = StringToDouble(obj_Edit_LOTS.Text()); class=class="str">"cmt">//--- 读手数 class="type">class="kw">double entry_price = user_price; class=class="str">"cmt">//--- 挂单入场价 class="type">class="kw">double stopLoss = user_price-StringToDouble(obj_Edit_SL.Text())*_Point; class=class="str">"cmt">//--- 挂单价减SL点数得止损 class="type">class="kw">double takeprofit = user_price+StringToDouble(obj_Edit_TP.Text())*_Point; class=class="str">"cmt">//--- 挂单价加TP点数得止盈 Print("Lots = ",lots,", Entry = ",entry_price,", SL = ",stopLoss,", TP = ",takeprofit); class=class="str">"cmt">//--- 日志 obj_Trade.BuyLimit(lots,entry_price,_Symbol,stopLoss,takeprofit); class=class="str">"cmt">//--- 发买限价单 }
◍ 一键平单方向的持仓逻辑
在图表按钮的点击事件里,除了全平之外,更实用的落点是按方向批量平仓。上面这段针对 Close All Sell 按钮的处理,先遍历当前账户所有持仓,再筛符号、再判多空,最后只对空单下手。 循环从 PositionsTotal()-1 倒序到 0,是因为平仓会改变持仓数组长度,倒序遍历能避免漏单或越界。PositionGetTicket(i) 拿到真实 ticket 后,必须用 PositionSelectByTicket 选中才能读属性,这是 MT5 持仓操作的固定顺序。 筛选靠 PositionGetString(POSITION_SYMBOL)==_Symbol 锁住当前图表品种,再用 PositionGetInteger(POSITION_TYPE) 强转成 ENUM_POSITION_TYPE,等于 POSITION_TYPE_SELL 才调用 obj_Trade.PositionClose。外汇与贵金属杠杆高,这类一键脚本若误触可能瞬间清空方向性敞口,实盘前务必在策略测试器用历史数据跑一遍确认只平目标单。
Print("OBJECT CLICKED = ",obj_Btn_CLOSE_ALL_SELL.Name()); class=class="str">"cmt">//--- Log the button click event for (class="type">int i = PositionsTotal() -class="num">1; i >= class="num">0; i--){ class=class="str">"cmt">//--- Loop through all positions class="type">ulong pos_ticket = PositionGetTicket(i); class=class="str">"cmt">//--- Get the ticket of the position if (pos_ticket > class="num">0){ class=class="str">"cmt">//--- Check if the position ticket is valid if (PositionSelectByTicket(pos_ticket)){ class=class="str">"cmt">//--- Select the position by ticket if (PositionGetString(POSITION_SYMBOL)==_Symbol){ class=class="str">"cmt">//--- Check if the position matches the symbol class="type">ENUM_POSITION_TYPE pos_type = (class="type">ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); class=class="str">"cmt">//--- Get the position type if (pos_type == POSITION_TYPE_SELL){ class=class="str">"cmt">//--- Check if the position is a sell position obj_Trade.PositionClose(pos_ticket); class=class="str">"cmt">//--- Close the sell position } } } } }