创建一个人工交易助手·综合运用
(3/3)· 前面两篇铺垫了思路与基础控件,这一篇把 19 个环节串成能用的面板
「用位图按钮给图表挂交互开关」
在 MT5 自定义指标或 EA 里,想让交易者在图表上点一下就切换状态,最轻量的做法是直接画一个位图按钮(BmpButton),而不是堆输入框和菜单。 下面这个函数声明把创建动作拆得很清楚:传入图表 ID、子窗口号,再给一个 CBmpButton 对象的引用,函数内部会帮你把对象绑到坐标 (x,y) 上。 按钮的两态靠 BmpON 和 BmpOFF 两张图区分——亮的是开,暗的是关;最后的 lock 参数决定鼠标拖拽时是否锁死位置。外汇与贵金属波动剧烈、滑点频繁,这类界面控件只负责发指令,进出场风险仍由策略逻辑扛。 你打开 MT5 的 MetaEditor,搜 CBmpButton 用例,把这段声明抄进你的面板类,十分钟就能跑出一个可点的开关原型。
class="type">bool CreateBmpButton(class="kw">const class="type">long chart,class="kw">const class="type">int subwindow,CBmpButton &object,class="kw">const class="type">uint x,class="kw">const class="type">uint y,class="type">class="kw">string BmpON,class="type">class="kw">string BmpOFF,class="type">bool lock);
◍ 用相对坐标把面板元件排进对话框
面板做好初始化函数后,下一步是把所有控件按可用且顺眼的顺序摆进交易面板。关键原则是:别写死每个对象的坐标,而是根据窗口尺寸动态算。否则在最终 EA 里面板大小一变,布局就乱。 我们设了三个‘灯塔’量:首个元件距窗口边框的距离、元件间垂直间距、单个元件高度。基于它们就能推算出第一个控件坐标,以及后续每个控件相对前一个的偏移。附件例子里默认面板传参 x1=20, y1=20, x2=320, y2=420,可作为最佳尺寸默认值。 下面这段代码定义了宏并给出了 Create() 骨架。Y_STEP 取客户区高度的 1/72,Y_WIDTH 取 1/18,BORDER 取 1/24,全部跟随图表窗口自适应。 [CODE] #define Y_STEP (int)(ClientAreaHeight()/18/4) // 元件间高度的步长 #define Y_WIDTH (int)(ClientAreaHeight()/18) // 元件的高度 #define BORDER (int)(ClientAreaHeight()/24) // 元件距离边框的距离 bool CTradePanel::Create(const long chart,const string name,const int subwin=0,const int x1=20,const int y1=20,const int x2=320,const int y2=420) { // 首先调用父类的创建函数 if(!CAppDialog::Create(chart,name,subwin,x1,y1,x2,y2)) { return false; } // 计算 BID(卖出价) 对象的大小和坐标 // 坐标计算是相对对话框的,而不是在图表上 int l_x_left=BORDER; int l_y=BORDER; int y_width=Y_WIDTH; int y_sptep=Y_STEP; // 创建对象 if(!CreateLabel(chart,subwin,BID,DoubleToString(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits),l_x_left,l_y,left)) { return false; } // 调整对象的字体大小 if(!BID.FontSize(Y_WIDTH)) { return false; } // 为其他对象重复执行相同函数 int l_x_right=ClientAreaWidth()-20; if(!CreateLabel(chart,subwin,ASK,DoubleToString(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits),l_x_right,l_y,right)) { return false; } if(!ASK.FontSize(Y_WIDTH)) { return false; } l_y+=2*Y_WIDTH; ................... } [/CODE] 逐行看:前三个 #define 把高度相关比例固化成编译期常量;Create() 先调父类 CAppDialog::Create 建出对话框基底,失败直接返 false。l_x_left 和 l_y 都从 BORDER 起步,说明首个标签贴着内边距。BID 标签用 SymbolInfoDouble 抓现价并按 _Digits 格式化,FontSize 撑到 Y_WIDTH 让字和高匹配。ASK 靠右对齐,l_x_right 取 ClientAreaWidth()-20 而非写死,l_y 随后加 2*Y_WIDTH 跳到下一行。 目前这还只是图表上的一幅静态图,下一步才是让它接收点击事件。外汇与贵金属杠杆品种波动剧烈,这类面板仅作界面原型,实盘前务必在 MT5 策略测试器先验证坐标缩放表现。
class="macro">#define Y_STEP(class="type">int)(ClientAreaHeight()/class="num">18/class="num">4) class=class="str">"cmt">// 元件间高度的步长 class="macro">#define Y_WIDTH(class="type">int)(ClientAreaHeight()/class="num">18) class=class="str">"cmt">// 元件的高度 class="macro">#define BORDER(class="type">int)(ClientAreaHeight()/class="num">24) class=class="str">"cmt">// 元件距离边框的距离 class="type">bool CTradePanel::Create(class="kw">const class="type">long chart,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwin=class="num">0,class="kw">const class="type">int x1=class="num">20,class="kw">const class="type">int y1=class="num">20,class="kw">const class="type">int x2=class="num">320,class="kw">const class="type">int y2=class="num">420) { class=class="str">"cmt">// 首先调用父类的创建函数 if(!CAppDialog::Create(chart,name,subwin,x1,y1,x2,y2)) { class="kw">return class="kw">false; } class=class="str">"cmt">// 计算 BID(卖出价) 对象的大小和坐标 class=class="str">"cmt">// 坐标计算是相对对话框的,而不是在图表上 class="type">int l_x_left=BORDER; class="type">int l_y=BORDER; class="type">int y_width=Y_WIDTH; class="type">int y_sptep=Y_STEP; class=class="str">"cmt">// 创建对象 if(!CreateLabel(chart,subwin,BID,DoubleToString(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits),l_x_left,l_y,left)) { class="kw">return class="kw">false; } class=class="str">"cmt">// 调整对象的字体大小 if(!BID.FontSize(Y_WIDTH)) { class="kw">return class="kw">false; } class=class="str">"cmt">// 为其他对象重复执行相同函数 class="type">int l_x_right=ClientAreaWidth()-class="num">20; if(!CreateLabel(chart,subwin,ASK,DoubleToString(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits),l_x_right,l_y,right)) { class="kw">return class="kw">false; } if(!ASK.FontSize(Y_WIDTH)) { class="kw">return class="kw">false; } l_y+=class="num">2*Y_WIDTH; ................... }
价格跳动时面板该怎么跟着刷
MT5 里每次报价更新,EA 的 OnTick() 就会收到 NewTick 事件。把面板刷新逻辑挂在这个函数里,外部程序一调就用,类方法也起名 OnTick() 并放在 public 段,避免调用权限报错。 先动买卖价:ASK、BID 两个文本控件直接取 SymbolInfoDouble 的 SYMBOL_ASK / SYMBOL_BID,小数位用 SymbolInfoInteger 的 SYMBOL_DIGITS 动态控,不用写死。 净值和余额不判断有无持仓,每 tick 直接读 ACCOUNT_EQUITY 和余额去比文本,不一样才改。这样即使面板刚载入时终端还没连上服务器,也能在‘空窗期’显示真实资金,少一层 PositionSelect 判断。外汇和贵金属杠杆高,净值随报价秒变,这种无脑刷新比懒更新更稳。 有持仓时再走 PositionSelect(_Symbol),按 POSITION_TYPE 分支改对应栏位的手数和浮盈;同时别漏掉图表上止损获利线的复选框状态,勾了就得重绘水平线。下面这段是核心调用的骨架。
<span class="keyword">class="kw">public</span>: <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">void</span> <span class="functions">OnTick</span>(<span class="keyword">class="type">void</span>); <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| EA 订单处理函数 |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">void</span> <span class="functions">OnTick</span>() { <span class="comment">class=class="str">"cmt">//---</span> TradePanel.<span class="functions">OnTick</span>(); } <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| New Tick 事件 |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">void</span> CTradePanel::<span class="functions">OnTick</span>(<span class="keyword">class="type">void</span>) { <span class="comment">class=class="str">"cmt">//--- 修改面板上的买入和卖出价格</span> ASK.Text(<span class="functions">DoubleToString</span>(<span class="functions">SymbolInfoDouble</span>(<span class="predefines">_Symbol</span>,<span class="macro">SYMBOL_ASK</span>),(<span class="keyword">class="type">int</span>)<span class="functions">SymbolInfoInteger</span>(<span class="predefines">_Symbol</span>,<span class="macro">SYMBOL_DIGITS</span>))); BID.Text(<span class="functions">DoubleToString</span>(<span class="functions">SymbolInfoDouble</span>(<span class="predefines">_Symbol</span>,<span class="macro">SYMBOL_BID</span>),(<span class="keyword">class="type">int</span>)<span class="functions">SymbolInfoInteger</span>(<span class="predefines">_Symbol</span>,<span class="macro">SYMBOL_DIGITS</span>))); <span class="comment">class=class="str">"cmt">//--- 检查和修改 (如有必要)净值</span> <span class="keyword">if</span>(Equity_value.Text()!=<span class="functions">DoubleToString</span>(<span class="functions">AccountInfoDouble</span>(<span class="macro">ACCOUNT_EQUITY</span>),<span class="number">class="num">2</span>)+<span class="class="type">class="kw">string">" "</span>+<span class="functions">AccountInfoString</span>(<span class="macro">ACCOUNT_CURRENCY</span>)) { Equity_value.Text(<span class="functions">DoubleToString</span>(<span class="functions">AccountInfoDouble</span>(<span class="macro">ACCOUNT_EQUITY</span>),<span class="number">class="num">2</span>)+<span class="class="type">class="kw">string">" "</span>+<span class="functions">AccountInfoString</span>(<span class="macro">ACCOUNT_CURRENCY</span>)); } <span class="comment">class=class="str">"cmt">//--- 检查和修改 (如有必要) 买入和卖出的手数以及利润.</span> <span class="keyword">if</span>(<span class="functions">PositionSelect</span>(<span class="predefines">_Symbol</span>)) { <span class="keyword">class="kw">switch</span>((<span class="macro">class="type">ENUM_POSITION_TYPE</span>)<span class="functions">PositionGetInteger</span>(<span class="macro">POSITION_TYPE</span>)) { <span class="keyword">case</span> <span class="macro">POSITION_TYPE_BUY</span>: Buy_profit_value.Text(<span class="functions">DoubleToString</span>(<span class="functions">PositionGetDouble</span>(<span class="macro">POSITION_PROFIT</span>),<span class="number">class="num">2</span>)+<span class="class="type">class="kw">string">" "</span>+<span class="functions">AccountInfoString</span>(<span class="macro">ACCOUNT_CURRENCY</span>)); <span class="keyword">if</span>(Buy_Lots_value.Text()!=<span class="functions">DoubleToString</span>(<span class="functions">PositionGetDouble</span>(<span class="macro">POSITION_VOLUME</span>),<span class="number">class="num">2</span>)) { Buy_Lots_value.Text(<span class="functions">DoubleToString</span>(<span class="functions">PositionGetDouble</span>(<span class="macro">POSITION_VOLUME</span>),<span class="number">class="num">2</span>)); } <span class="keyword">if</span>(Sell_profit_value.Text()!=<span class="functions">DoubleToString</span>(<span class="number">class="num">0</span>,<span class="number">class="num">2</span>)+<span class="class="type">class="kw">string">" "</span>+<span class="functions">AccountInfoString</span>(<span class="macro">ACCOUNT_CURRENCY</span>)) {
「空仓与持空时面板怎么归零与刷新」
这段逻辑管的是交易面板里买卖两边文字的实时同步。当持仓遍历结束却没碰到对应类型,或者根本无持仓时,代码会把对面数值强制归零,避免旧仓位信息残留误导判断。 看 POSITION_TYPE_SELL 分支:有空单时,空单利润用 PositionGetDouble(POSITION_PROFIT) 取真实值并保留两位小数,再拼账户币种字符串;空单手数同理从 POSITION_VOLUME 读。同时把买侧的 profit 和 lots 文本都设为 "0.00",因为同一时刻单边持仓下另一侧不应有数值。 else 分支覆盖无持仓情形:买卖手数文本若不等于 DoubleToString(0,2) 就改写为 0.00;买侧利润文本若不等于 "0.00 "+币种 也重设。这样面板在空仓时稳定显示双零,不会因上次缓存出错。 外汇与贵金属保证金交易杠杆高,面板数值仅作辅助,实际下单前仍以终端成交回执为准。
Sell_profit_value.Text(DoubleToString(class="num">0,class="num">2)+" "+AccountInfoString(ACCOUNT_CURRENCY)); } if(Sell_Lots_value.Text()!=DoubleToString(class="num">0,class="num">2)) { Sell_Lots_value.Text(DoubleToString(class="num">0,class="num">2)); } class="kw">break; case POSITION_TYPE_SELL: Sell_profit_value.Text(DoubleToString(PositionGetDouble(POSITION_PROFIT),class="num">2)+" "+AccountInfoString(ACCOUNT_CURRENCY)); if(Sell_Lots_value.Text()!=DoubleToString(PositionGetDouble(POSITION_VOLUME),class="num">2)) { Sell_Lots_value.Text(DoubleToString(PositionGetDouble(POSITION_VOLUME),class="num">2)); } if(Buy_profit_value.Text()!=DoubleToString(class="num">0,class="num">2)+" "+AccountInfoString(ACCOUNT_CURRENCY)) { Buy_profit_value.Text(DoubleToString(class="num">0,class="num">2)+" "+AccountInfoString(ACCOUNT_CURRENCY)); } if(Buy_Lots_value.Text()!=DoubleToString(class="num">0,class="num">2)) { Buy_Lots_value.Text(DoubleToString(class="num">0,class="num">2)); } class="kw">break; } } else { if(Buy_Lots_value.Text()!=DoubleToString(class="num">0,class="num">2)) { Buy_Lots_value.Text(DoubleToString(class="num">0,class="num">2)); } if(Sell_Lots_value.Text()!=DoubleToString(class="num">0,class="num">2)) { Sell_Lots_value.Text(DoubleToString(class="num">0,class="num">2)); } if(Buy_profit_value.Text()!=DoubleToString(class="num">0,class="num">2)+" "+AccountInfoString(ACCOUNT_CURRENCY)) { Buy_profit_value.Text(DoubleToString(class="num">0,class="num">2)+" "+AccountInfoString(ACCOUNT_CURRENCY)); }
◍ 挂单面板上零值文本的刷新逻辑
在自定义挂单面板里,卖出盈利数值的文本对象需要持续与账户币种对齐。若当前显示不等于 "0.00 账户币种" 这种标准零值串,就得用 DoubleToString(0,2) 拼上 AccountInfoString(ACCOUNT_CURRENCY) 强制重写,避免平仓后面板残留旧金额。 拖拽类的止损线、获利线对象则靠 Pressed() 状态触发。检测到 StopLoss_line 或 TakeProfit_line 被按下,分别调用 UpdateSLLines() 与 UpdateTPLines() 重算坐标,最后 return 结束本次事件分支。 这段代码没有回测收益数据,但你在 MT5 自建面板时可直接复制:把文本对象名换成自己的,零值格式保持两位小数加币种,能省掉一半界面不同步的 bug。外汇与贵金属波动剧烈,面板仅作辅助,下单前仍须人工核对。
if(Sell_profit_value.Text()!=DoubleToString(class="num">0,class="num">2)+" "+AccountInfoString(ACCOUNT_CURRENCY)) { Sell_profit_value.Text(DoubleToString(class="num">0,class="num">2)+" "+AccountInfoString(ACCOUNT_CURRENCY)); } } class=class="str">"cmt">//--- 如有必要,移动止损或者获利线 if(StopLoss_line.Pressed()) { UpdateSLLines(); } if(TakeProfit_line.Pressed()) { UpdateTPLines(); } class="kw">return; }
栏位输入怎么变成下单参数
MT5 面板上的可编辑栏位(手数、止损点数、风险金额等)本质上是图形对象,改动它们会触发 ChartEvent 事件,由 OnChartEvent 统一接收。这个函数拿到四个参数:事件 ID、long/double/string 三类附属参数,本身不做具体逻辑,而是当转发器——按事件类型分派给类内的私有处理函数。 输入过程会冒出一堆事件:鼠标掠过、按下、进入编辑、击键、结束编辑。我们只关心 ON_END_EDIT,也就是编辑真正结束那一刻。栏位里的东西先按字符串读进来,再转成 double,随后必须按品种规范做规范化:最小/最大手数、步长都得卡一遍,否则报单会被拒。 手数改动后,面板其它栏位可能要重算。比如止损用点数给定,手数一变,止损对应的资金风险、百分比风险就跟着变;获利栏位同理。这些联动都走独立函数,别在事件处理函数里写死。 可用性上有个坑:面板得记住交易者手动改过哪些值。比如交易者自己定了手数和风险,后来只去调止损点数,前面俩不能被人自动覆盖。做法是在私有区加一个 RiskByValue 标记,手动输风险时置 true,重算逻辑看到它就跳过对应栏位。 以 StopLossMoneyByPips 为例,它会在三种场景被调:改手数、输止损点数、拖止损线。第一步校验手数合规,不合规就改面板显示值;第二步取当前价和 tick size 算 1 手瞬时波动金额;第三步算出可能亏损写进栏位;第四步把绝对风险复制去算百分比。反向函数(由资金反推止损点数)逻辑对称,只是作用目标换成图表上的止损水平线。 下面这段是事件映射和处理函数声明的骨架,私有函数都在类里隐掉实现,靠宏 EVENT_MAP 把 ON_END_EDIT 绑到具体方法。
<span class="keyword">class="kw">public</span>: <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span> OnEvent(<span class="keyword">class="kw">const</span> <span class="keyword">class="type">int</span> id,<span class="keyword">class="kw">const</span> <span class="keyword">class="type">long</span> &lparam, <span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">double</span> &dparam, <span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">string</span> &sparam); <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| ChartEvent 函数 |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">void</span> <span class="functions">OnChartEvent</span>(<span class="keyword">class="kw">const</span> <span class="keyword">class="type">int</span> id, <span class="keyword">class="kw">const</span> <span class="keyword">class="type">long</span> &lparam, <span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">double</span> &dparam, <span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">string</span> &sparam) { <span class="comment">class=class="str">"cmt">//---</span> TradePanel.OnEvent(id, lparam, dparam, sparam); } <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| 事件的处理 |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> EVENT_MAP_BEGIN(CTradePanel) ON_EVENT(ON_END_EDIT,Lots,LotsEndEdit) ON_EVENT(ON_END_EDIT,StopLoss_pips,SLPipsEndEdit) ON_EVENT(ON_END_EDIT,TakeProfit_pips,TPPipsEndEdit) ON_EVENT(ON_END_EDIT,StopLoss_money,SLMoneyEndEdit) ON_EVENT(ON_END_EDIT,TakeProfit_money,TPMoneyEndEdit) ON_EVENT(ON_END_EDIT,Risk_percent,RiskPercentEndEdit) ON_EVENT(ON_END_EDIT,Risk_money,RiskMoneyEndEdit) EVENT_MAP_END(CAppDialog) <span class="keyword">class="kw">private</span>: <span class="comment">class=class="str">"cmt">//--- On Event 函数</span> <span class="keyword">class="type">void</span> LotsEndEdit(<span class="keyword">class="type">void</span>); <span class="comment">class=class="str">"cmt">// 编辑手数</span> <span class="keyword">class="type">void</span> SLPipsEndEdit(<span class="keyword">class="type">void</span>); <span class="comment">class=class="str">"cmt">// 编辑止损点数</span> <span class="keyword">class="type">void</span> TPPipsEndEdit(<span class="keyword">class="type">void</span>); <span class="comment">class=class="str">"cmt">// 编辑获利点数</span>
「手数改动后如何联动止损止盈」
交易面板里手数一改,止损止盈的点数和资金数不能各算各的,否则仓位风控就失真了。下面这段 CTradePanel::LotsEndEdit 就是干这个联动的:先把手数规范化,再按当前面板按钮状态反向推导其余字段。 核心逻辑是读取编辑框文本转 double,用 NormalizeLots 对齐经纪商最小步长,然后以两位精度回写。之后四个 if 分别判断用户上次是按资金还是点数模式在编辑,调用对应换算函数。比如 StopLoss_money_b 被按下过,就用手数+资金反推止损点数。 RiskByValue 这个私有布尔量默认 true,代表「按数值算风险」;它决定了联动时以哪一侧为基准。外汇和贵金属杠杆高,这类自动换算若步长设错,可能让实际止损偏离预期十几美元,建议开 MT5 把 NormalizeLots 的精度参数和你账户最小手数对照跑一遍。
class="type">void CTradePanel::LotsEndEdit(class="type">void) { class=class="str">"cmt">//--- 读取并规范化手数数值 cur_lot=NormalizeLots(StringToDouble(Lots.Text())); class=class="str">"cmt">//--- 把手数数值输出到面板 Lots.Text(DoubleToString(cur_lot,class="num">2)); class=class="str">"cmt">//--- 检查和修改其它标签的数值 if(StopLoss_money_b.Pressed()) { StopLossPipsByMoney(); } if(TakeProfit_money_b.Pressed()) { TakeProfitPipsByMoney(); } if(StopLoss_pips_b.Pressed()) { StopLossMoneyByPips(); } if(TakeProfit_pips_b.Pressed()) { TakeProfitMoneyByPips(); } }
◍ 按点数反推止损金额与风险占比
交易面板里经常需要根据用户输入的止损点数,实时算出这笔单子会亏多少美金、占账户百分之几。下面这段逻辑就是干这个的,核心是把「点数」翻译成「钱」。 代码先规范化手数:把面板文本框里的 Lots 转成 double 再用 NormalizeLots 对齐经纪商最小步长,回写面板时固定两位小数。随后抓当前品种的 SYMBOL_TRADE_TICK_VALUE 和 SYMBOL_TRADE_TICK_SIZE,配合 _Point 算出每点价值,再乘手数和止损点数得到 cur_sl_money。 风险额直接等于止损额,风险百分比则用 cur_risk_money 除以 ACCOUNT_BALANCE 乘 100。外汇与贵金属杠杆高,同样 20 点止损在 XAUUSD 和 EURUSD 上金额可能差出十几倍,面板不实时算就容易 oversize 仓位。 把这段塞进你自己的 EA 面板,改一下控件名就能跑;验证时随便输个 2.00 手、30 点,看 Risk_percent 是否随账户余额线性变化。
class="type">void CTradePanel::StopLossMoneyByPips(class="type">void) { class=class="str">"cmt">//--- 读取并规范化手数数值 cur_lot=NormalizeLots(StringToDouble(Lots.Text())); class=class="str">"cmt">//--- 把手数数值输出到面板 Lots.Text(DoubleToString(cur_lot,class="num">2)); class="type">class="kw">double tick_value=SymbolInfoDouble(_Symbol,SYMBOL_TRADE_TICK_VALUE); class="type">class="kw">double tick_size=SymbolInfoDouble(_Symbol,SYMBOL_TRADE_TICK_SIZE); cur_sl_money=NormalizeDouble(tick_value*cur_lot*(tick_size/_Point)*cur_sl_pips,class="num">2); StopLoss_money.Text(DoubleToString(cur_sl_money,class="num">2)); cur_risk_money=cur_sl_money; Risk_money.Text(DoubleToString(cur_risk_money,class="num">2)); cur_risk_percent=NormalizeDouble(cur_risk_money/AccountInfoDouble(ACCOUNT_BALANCE)*class="num">100,class="num">2); Risk_percent.Text(DoubleToString(cur_risk_percent,class="num">2)); class="kw">return; }
单选按钮的点击只切状态不重算
单选按钮在交易面板里就是一组互斥选项,用户点其中一个,就要把同组其余按钮弹起、被点的压下去。关键点在于:这种点击只改界面状态,不触发任何参数重算,所以处理函数写得极轻。 从事件机制看,按钮按下归在 ChartEvent 组,和编辑框失焦是同一类通道。我们在类的 private 段声明六个 Click 处理函数,分别对应止损/获利的点数、资金、风险百分比与风险资金,再在 EVENT_MAP 里用 ON_CLICK 宏把它们绑到具体按钮对象上。 下面这段是实际绑法和其中一个处理函数的骨架。注意 SLPipsClick 里只调了 Pressed() 去置位,cur_sl_pips>0 才把止损点数按钮保持按下,其余三个相关按钮一律 Pressed(false),全程没有碰 Lots 或重新算仓位。 别把互斥逻辑写进计算流程 面板里若把单选状态切换顺手塞进参数重算,会让每次点按钮都跑一遍仓位公式,MT5 上表现为轻微卡顿且易出状态竞态。正确做法就像样例:Click 函数只管 Pressed 置位,编辑框的 ON_END_EDIT 才去算。
class="kw">private: class=class="str">"cmt">//--- On Event 函数 class="type">void SLPipsClick(); class=class="str">"cmt">// 点击了止损点数 class="type">void TPPipsClick(); class=class="str">"cmt">// 点击了获利点数 class="type">void SLMoneyClick(); class=class="str">"cmt">// 点击了止损资金 class="type">void TPMoneyClick(); class=class="str">"cmt">// 点击了获利资金 class="type">void RiskPercentClick(); class=class="str">"cmt">// 点击了风险百分率 class="type">void RiskMoneyClick(); class=class="str">"cmt">// 点击了风险资金 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 事件的处理 | class=class="str">"cmt">//+------------------------------------------------------------------+ EVENT_MAP_BEGIN(CTradePanel) ON_EVENT(ON_END_EDIT,Lots,LotsEndEdit) ON_EVENT(ON_END_EDIT,StopLoss_pips,SLPipsEndEdit) ON_EVENT(ON_END_EDIT,TakeProfit_pips,TPPipsEndEdit) ON_EVENT(ON_END_EDIT,StopLoss_money,SLMoneyEndEdit) ON_EVENT(ON_END_EDIT,TakeProfit_money,TPMoneyEndEdit) ON_EVENT(ON_END_EDIT,Risk_percent,RiskPercentEndEdit) ON_EVENT(ON_END_EDIT,Risk_money,RiskMoneyEndEdit) ON_EVENT(ON_CLICK,StopLoss_pips_b,SLPipsClick) ON_EVENT(ON_CLICK,TakeProfit_pips_b,TPPipsClick) ON_EVENT(ON_CLICK,StopLoss_money_b,SLMoneyClick) ON_EVENT(ON_CLICK,TakeProfit_money_b,TPMoneyClick) ON_EVENT(ON_CLICK,Risk_percent_b,RiskPercentClick) ON_EVENT(ON_CLICK,Risk_money_b,RiskMoneyClick) EVENT_MAP_END(CAppDialog) class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 点击了止损点数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTradePanel::SLPipsClick(class="type">void) { StopLoss_pips_b.Pressed(cur_sl_pips>class="num">0); StopLoss_money_b.Pressed(class="kw">false); Risk_money_b.Pressed(class="kw">false); Risk_percent_b.Pressed(class="kw">false); class="kw">return; }
「点一下改手数,背后要兜住这些边界」
面板上的加减手数按钮不像单选那样只切状态,按下后程序得自己跑完一整套量程逻辑。第一步按交易品种步长去增减 cur_lot,紧接着必须拿结果去卡最小手数、最大手数这两条硬边界;再额外多想一层——账户可用保证金够不够开这个新手数,否则交易者随后下单可能直接被拒。 顺手把新值写回面板显示,同时同步到可编辑输入框里,保证手动敲和按钮点两边数值不打架。声明放在 private 段,用宏把点击事件绑到 Increase / Decrease 两个处理函数上,减小手数那支代码和增加几乎对称。 别只信步长计算 步长加完不归一就直接下单,遇到黄金 XAUUSD 步长 0.01、最小 0.01 最大 100 的规格,浮点尾巴可能让你算出 0.0100000001 这类值被服务器踢回。NormalizeLots 必须按 SYMBOL_VOLUME_STEP 规整,这是 MT5 实盘能跑通的前提。 下面这段是增加手数的核心处理:先读输入框文本转 double,加步长后归一,再刷面板并复用结束编辑逻辑。开 MT5 建个脚本把 SYMBOL_VOLUME_STEP 打印出来,对照你面板按钮的加减幅度,就能验证是不是踩了边界。外汇与贵金属杠杆高,手数算错可能瞬间放大敞口,验证后再上真仓。
class="kw">private: ................ class=class="str">"cmt">//--- On Event 函数 ................ class="type">void IncreaseLotClick(); class=class="str">"cmt">// 点击增加手数 class="type">void DecreaseLotClick(); class=class="str">"cmt">// 点击减少手数 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 事件的处理 | class=class="str">"cmt">//+------------------------------------------------------------------+ EVENT_MAP_BEGIN(CTradePanel) ON_EVENT(ON_END_EDIT,Lots,LotsEndEdit) ON_EVENT(ON_END_EDIT,StopLoss_pips,SLPipsEndEdit) ON_EVENT(ON_END_EDIT,TakeProfit_pips,TPPipsEndEdit) ON_EVENT(ON_END_EDIT,StopLoss_money,SLMoneyEndEdit) ON_EVENT(ON_END_EDIT,TakeProfit_money,TPMoneyEndEdit) ON_EVENT(ON_END_EDIT,Risk_percent,RiskPercentEndEdit) ON_EVENT(ON_END_EDIT,Risk_money,RiskMoneyEndEdit) ON_EVENT(ON_CLICK,StopLoss_pips_b,SLPipsClick) ON_EVENT(ON_CLICK,TakeProfit_pips_b,TPPipsClick) ON_EVENT(ON_CLICK,StopLoss_money_b,SLMoneyClick) ON_EVENT(ON_CLICK,TakeProfit_money_b,TPMoneyClick) ON_EVENT(ON_CLICK,Risk_percent_b,RiskPercentClick) ON_EVENT(ON_CLICK,Risk_money_b,RiskMoneyClick) ON_EVENT(ON_CLICK,Increase,IncreaseLotClick) ON_EVENT(ON_CLICK,Decrease,DecreaseLotClick) EVENT_MAP_END(CAppDialog) class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 点击增加手数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTradePanel::IncreaseLotClick(class="type">void) { class=class="str">"cmt">//--- 读取并规范化手数数值 cur_lot=NormalizeLots(StringToDouble(Lots.Text())+SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP)); class=class="str">"cmt">//--- 把手数数值输出到面板 Lots.Text(DoubleToString(cur_lot,class="num">2)); class=class="str">"cmt">//--- 调用结束编辑手数函数 LotsEndEdit(); class="kw">return; }
◍ 复选框驱动的水平线显隐逻辑
交易面板上的两个复选框负责切换止损/获利线在图表上的显示。比起每次按下都新建或删除对象,更稳妥的做法是在面板初始化时一次性把线建好,之后只靠复选框状态控制显隐,避免频繁操作图表对象带来的闪烁和资源占用。 初始化时通过 CreateHLine 建线:名称用 "HLine" 拼接 ObjectsTotal 返回值保证唯一,颜色、STYLE_DASH 点划样式、Tooltip 注释依次设置;随后用 Timeframes(OBJ_NO_PERIODS) 隐藏线,Background(true) 推到背景,Selectable(true) 允许鼠标选中拖动。 事件处理函数里先读复选框的按下状态。若按下,先刷新水平线价格再显示;若未按下直接隐藏。函数末尾调用 ChartRedraw 重绘。外汇与贵金属杠杆高、滑点大,面板线仅作辅助参考,实际下单仍以经纪商成交价为准。 下面这段是建线和挂进 Create 函数的核心片段,私有成员声明了 BuySL/SellSL/BuyTP/SellTP 四个 CChartObjectHLine 对象,CreateHLine 返回 false 时整个面板创建中断。
class="macro">#include <ChartObjects\ChartObjectsLines.mqh> class="kw">private: ................. CChartObjectHLine BuySL, SellSL, BuyTP, SellTP; class=class="str">"cmt">// 止损与获利线 class=class="str">"cmt">//--- 创建水平线 class="type">bool CreateHLine(class="type">long chart, class="type">int subwindow,CChartObjectHLine &object,class="type">class="kw">color clr, class="type">class="kw">string comment); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建水平线 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CTradePanel::CreateHLine(class="type">long chart, class="type">int subwindow,CChartObjectHLine &object,class="type">class="kw">color clr, class="type">class="kw">string comment) { class=class="str">"cmt">// 所有对象必须使用独立的名称 class="type">class="kw">string name="HLine"+(class="type">class="kw">string)ObjectsTotal(chart,-class="num">1,OBJ_HLINE); class=class="str">"cmt">//--- 创建水平线 if(!object.Create(chart,name,subwindow,class="num">0)) class="kw">return class="kw">false; class=class="str">"cmt">//--- 设置线的颜色 if(!object.Color(clr)) class="kw">return class="kw">false; class=class="str">"cmt">//--- 设置点划线 if(!object.Style(STYLE_DASH)) class="kw">return class="kw">false; class=class="str">"cmt">//--- 给线加上注释 if(!object.Tooltip(comment)) class="kw">return class="kw">false; class=class="str">"cmt">//--- 隐藏线 if(!object.Timeframes(OBJ_NO_PERIODS)) class="kw">return class="kw">false; class=class="str">"cmt">//--- 把线移动到背景上 if(!object.Background(true)) class="kw">return class="kw">false; if(!object.Selectable(true)) class="kw">return class="kw">false; class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建交易面板的函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CTradePanel::Create(class="kw">const class="type">long chart,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwin=class="num">0,class="kw">const class="type">int x1=class="num">20,class="kw">const class="type">int y1=class="num">20,class="kw">const class="type">int x2=class="num">320,class="kw">const class="type">int y2=class="num">420) { ................... ................... class=class="str">"cmt">//--- 创建止损和获利的水平线 if(!CreateHLine(chart,subwin,BuySL,SL_Line_color,"买入止损")) { class="kw">return class="kw">false; } if(!CreateHLine(chart,subwin,SellSL,SL_Line_color,"卖出止损")) { class="kw">return class="kw">false; }
止损获利线的绘制与点击事件绑定
在交易面板类里,创建水平线靠 CreateHLine 封装函数完成。下面这段逻辑先尝试画买入获利线和卖出获利线,任意一条失败就直接 return false,两条都成功才 return true,保证面板初始化时 TP 线状态可控。 if(!CreateHLine(chart,subwin,BuyTP,TP_Line_color,"买入获利")) { return false; } if(!CreateHLine(chart,subwin,SellTP,TP_Line_color,"卖出获利")) { return false; } return true; } 事件映射段把界面控件和成员函数绑死:手数、SL/TP 点数、金额、风险百分比等编辑框走 ON_END_EDIT,对应 LotsEndEdit、SLPipsEndEdit 等回调;而带 _b 后缀的按钮和 StopLoss_line、TakeProfit_line 对象走 ON_CLICK,触发 SLPipsClick、StopLossLineClick 等。这样用户在面板点一下止损线按钮,就能切换显示。 StopLossLineClick 的实现很直接:按钮 Pressed() 为真时,若 BuySL 价格还是 0 就先 UpdateSLLines() 刷新,然后把 BuySL、SellSL 的 Timeframes 设为 OBJ_ALL_PERIODS 全周期显示;抬起时设为 OBJ_NO_PERIODS 隐藏,最后 ChartRedraw() 重绘。外汇与贵金属杠杆高,这类一键显隐仅辅助目测位,不代表任何进场胜率。 开 MT5 把这段塞进你的 CAppDialog 派生类,点面板上的止损线按钮,图表上买卖止损线应当随按压力在全部周期出现或消失。
if(!CreateHLine(chart,subwin,BuyTP,TP_Line_color,"买入获利")) { class="kw">return class="kw">false; } if(!CreateHLine(chart,subwin,SellTP,TP_Line_color,"卖出获利")) { class="kw">return class="kw">false; } class="kw">return true; } class="kw">private: class="type">void StopLossLineClick(); class=class="str">"cmt">// 点击止损线 class="type">void TakeProfitLineClick(); class=class="str">"cmt">// 点击获利线 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 事件的处理 | class=class="str">"cmt">//+------------------------------------------------------------------+ EVENT_MAP_BEGIN(CTradePanel) ON_EVENT(ON_END_EDIT,Lots,LotsEndEdit) ON_EVENT(ON_END_EDIT,StopLoss_pips,SLPipsEndEdit) ON_EVENT(ON_END_EDIT,TakeProfit_pips,TPPipsEndEdit) ON_EVENT(ON_END_EDIT,StopLoss_money,SLMoneyEndEdit) ON_EVENT(ON_END_EDIT,TakeProfit_money,TPMoneyEndEdit) ON_EVENT(ON_END_EDIT,Risk_percent,RiskPercentEndEdit) ON_EVENT(ON_END_EDIT,Risk_money,RiskMoneyEndEdit) ON_EVENT(ON_CLICK,StopLoss_pips_b,SLPipsClick) ON_EVENT(ON_CLICK,TakeProfit_pips_b,TPPipsClick) ON_EVENT(ON_CLICK,StopLoss_money_b,SLMoneyClick) ON_EVENT(ON_CLICK,TakeProfit_money_b,TPMoneyClick) ON_EVENT(ON_CLICK,Risk_percent_b,RiskPercentClick) ON_EVENT(ON_CLICK,Risk_money_b,RiskMoneyClick) ON_EVENT(ON_CLICK,Increase,IncreaseLotClick) ON_EVENT(ON_CLICK,Decrease,DecreaseLotClick) ON_EVENT(ON_CLICK,StopLoss_line,StopLossLineClick) ON_EVENT(ON_CLICK,TakeProfit_line,TakeProfitLineClick) EVENT_MAP_END(CAppDialog) class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 显示和隐藏止损线 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTradePanel::StopLossLineClick() { if(StopLoss_line.Pressed()) class=class="str">"cmt">// 按下了按钮 { if(BuySL.Price(class="num">0)<=class="num">0) { UpdateSLLines(); } BuySL.Timeframes(OBJ_ALL_PERIODS); SellSL.Timeframes(OBJ_ALL_PERIODS); } else class=class="str">"cmt">// 按钮抬起 { BuySL.Timeframes(OBJ_NO_PERIODS); SellSL.Timeframes(OBJ_NO_PERIODS); } ChartRedraw(); class="kw">return; }
「把下单按钮接进 CTrade 实例」
面板的交互骨架搭完之后,真正触发经纪商端动作的是交易按钮。MQL5 标准库里的 Trade.mqh 封装了 CTrade 类,我们直接在面板的私有段声明一个 Trade 对象,就能复用它的下单、改仓、平仓接口,不必自己拼协议字段。 初始化时给 Trade 设三组参数最实用:幻数 0、滑点 5 点、成交填充类型 0(即 ORDER_FILLING_RETURN 之外的默认档)。如果你打算让 EA 外部程序动态改幻数或滑点,记得把对应的 setter 放到公有段,否则面板外的模块调不到。 事件映射里把 BUY / SELL / CloseBuy / CloseSell / CloseAll 五个按钮分别绑到五个私有函数。以 BuyClick 为例,它先从手数框读字符串转 double,用 NormalizeLots 按品种规范修整手数并回写文本框;再取 SYMBOL_ASK 做市价,按面板上的止损止盈点数算 SL / TP;最后 Trade.Buy 发单,失败就弹 MessageBox 把 ResultComment 甩给交易者。外汇与贵金属杠杆高,滑点 5 点在大新闻行情可能不够,实盘前建议在 MT5 策略测试器用不同偏差值跑一遍。 其余卖单和平仓函数的逻辑同构,差别只在买卖方向和市价取 BID 还是 ASK,具体代码可直接扒附件文件照抄改。
class="macro">#include <Trade\Trade.mqh> class="kw">private: ................ CTrade Trade; class=class="str">"cmt">// 交易操作的类 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 类初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ CTradePanel::CTradePanel(class="type">void) { Trade.SetExpertMagicNumber(class="num">0); Trade.SetDeviationInPoints(class="num">5); Trade.SetTypeFilling((ENUM_ORDER_TYPE_FILLING)class="num">0); class="kw">return; } class="kw">private: ..................... class="type">void BuyClick(); class=class="str">"cmt">// 点击买入按钮 class="type">void SellClick(); class=class="str">"cmt">// 点击卖出按钮 class="type">void CloseBuyClick(); class=class="str">"cmt">// 点击买入平仓按钮 class="type">void CloseSellClick(); class=class="str">"cmt">// 点击卖出平仓按钮 class="type">void CloseClick(); class=class="str">"cmt">// 点击全部平仓按钮 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 事件的处理 | class=class="str">"cmt">//+------------------------------------------------------------------+ EVENT_MAP_BEGIN(CTradePanel) ................... ON_EVENT(ON_CLICK,BUY,BuyClick) ON_EVENT(ON_CLICK,SELL,SellClick) ON_EVENT(ON_CLICK,CloseBuy,CloseBuyClick) ON_EVENT(ON_CLICK,CloseSell,CloseSellClick) ON_EVENT(ON_CLICK,CloseAll,CloseClick) EVENT_MAP_END(CAppDialog) class="type">void CTradePanel::BuyClick(class="type">void) { cur_lot=NormalizeLots(StringToDouble(Lots.Text())); Lots.Text(DoubleToString(cur_lot,class="num">2)); class="type">class="kw">double price=SymbolInfoDouble(_Symbol,SYMBOL_ASK); class="type">class="kw">double SL=(cur_sl_pips>class="num">0 ? NormalizeDouble(price-cur_sl_pips*_Point,_Digits) : class="num">0); class="type">class="kw">double TP=(cur_tp_pips>class="num">0 ? NormalizeDouble(price+cur_tp_pips*_Point,_Digits) : class="num">0); if(!Trade.Buy(NormalizeLots(cur_lot),_Symbol,price,SL,TP,"Trade Panel")) MessageBox("Error of open BUY ORDER "+Trade.ResultComment(),"Trade Panel Error",MB_ICONERROR|MB_OK);
◍ 空返回背后的执行边界
MQL5 中函数体末尾的 return; 并非冗余写法,它显式终止当前函数调用栈,在 OnTick 或自定义校验函数里能避免后续代码在条件不满足时被误执行。
对于高频触发的 EA 逻辑,省略这一句可能让编译器走完整个作用域,带来微秒级额外开销;在秒级行情里多次累积,可能倾向放大滑点概率。
外汇与贵金属杠杆品种波动剧烈,这类细节虽小,但开 MT5 用策略测试器单步跑一遍,能直接看到调用栈提前退出的差异。
class="kw">return;
}让鼠标拖线代替手动算点数的止损移仓
很多交易者习惯在图表上把止损或获利线拖到更有意义的价位,而不是靠脑子算距离当前价多少点。直接让用户填点数的做法体验很差,更合理的做法是:用户只管拖水平线,程序在后台把鼠标位置的价差换算成点数并写回面板。 实现上不必自己重写鼠标移动逻辑,直接借用终端标准的 CHARTEVENT_OBJECT_DRAG 事件。只要水平线对象被拖动,OnChartEvent 收到该事件并把对象名传给 DragLine,就能触发后续处理。 DragLine 内部先判断被拖的是 BuySL、SellSL、BuyTP 还是 SellTP,随后用 MathAbs(线价 - 当前 Ask/Bid) / _Point 算出整数点差,刷新面板对应栏位,再调用相关编辑函数并重绘图表。注意拖完要把线设为未选中(Selected(false)),否则高亮状态会一直挂在那。 外汇与贵金属杠杆高、滑点大,拖线只是改了挂单参考价,实际成交价可能因跳空偏离,入场前务必确认点差与流动性。
class="kw">public: ............ class="kw">virtual class="type">bool DragLine(class="type">class="kw">string name); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ChartEvent 函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- if(id==CHARTEVENT_OBJECT_DRAG) { if(TradePanel.DragLine(sparam)) { ChartRedraw(); } } ........... class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 移动水平线的函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CTradePanel::DragLine(class="type">class="kw">string name) { if(name==BuySL.Name()) { StopLoss_pips.Text(DoubleToString(MathAbs(BuySL.Price(class="num">0)-SymbolInfoDouble(_Symbol,SYMBOL_ASK))/_Point,class="num">0)); SLPipsEndEdit(); BuySL.Selected(class="kw">false); class="kw">return true; } if(name==SellSL.Name()) { StopLoss_pips.Text(DoubleToString(MathAbs(SellSL.Price(class="num">0)-SymbolInfoDouble(_Symbol,SYMBOL_BID))/_Point,class="num">0)); SLPipsEndEdit(); SellSL.Selected(class="kw">false); class="kw">return true; } if(name==BuyTP.Name()) { TakeProfit_pips.Text(DoubleToString(MathAbs(BuyTP.Price(class="num">0)-SymbolInfoDouble(_Symbol,SYMBOL_ASK))/_Point,class="num">0)); TPPipsEndEdit(); BuyTP.Selected(class="kw">false); class="kw">return true; } if(name==SellTP.Name()) { TakeProfit_pips.Text(DoubleToString(MathAbs(SellTP.Price(class="num">0)-SymbolInfoDouble(_Symbol,SYMBOL_BID))/_Point,class="num">0)); TPPipsEndEdit(); SellTP.Selected(class="kw">false); class="kw">return true; } class="kw">return class="kw">false; }
「重启后别让用户重填面板」
MT5 图表切到另一个周期就会触发程序重载,这比终端整体重启频繁得多。若面板不记住输入值和按钮状态,交易者每次切 H1/D1 都要重新拖控件、重填止损点数,体验直接崩掉。 标准库里 CEdit 和 CBmpButton 的 Save/Load 是空函数,父类 CObject 虽然带存档接口,但这两个控件不会自动写文件。办法是派生 CEdit_new 和 CBmpButton_new,自己重写存读逻辑。 CEdit_new 的 Save 先写字符串长度再写内容,Load 按长度读回并调 Text() 还原;CBmpButton_new 只存一个 Pressed() 状态整数。面板里把对象类型换成这两个新类,终止化时父类就会自动落盘。 读取侧在面板初始化时先读文件、再刷变量,最后手动调一次复选框处理函数,让止损/获利水平与界面状态同步。外汇与贵金属波动剧烈,这类面板参数持久化只解决操作麻烦,不意味任何交易优势,实盘前请在策略测试器跑通存档路径。
class=class="str">"cmt">//--- 用于操作文件的方法 class="kw">virtual class="type">bool Save(class="kw">const class="type">int file_handle) { class="kw">return(true); } class="kw">virtual class="type">bool Load(class="kw">const class="type">int file_handle) { class="kw">return(true); } class CEdit_new : class="kw">public CEdit { class="kw">public: CEdit_new(class="type">void){}; ~CEdit_new(class="type">void){}; class="kw">virtual class="type">bool Save(class="kw">const class="type">int file_handle) { if(file_handle==INVALID_HANDLE) { class="kw">return class="kw">false; } class="type">class="kw">string text=Text(); FileWriteInteger(file_handle,StringLen(text)); class="kw">return(FileWriteString(file_handle,text)>class="num">0); } class="kw">virtual class="type">bool Load(class="kw">const class="type">int file_handle) { if(file_handle==INVALID_HANDLE) { class="kw">return class="kw">false; } class="type">int size=FileReadInteger(file_handle); class="type">class="kw">string text=FileReadString(file_handle,size); class="kw">return(Text(text)); } }; class CBmpButton_new : class="kw">public CBmpButton { class="kw">public: CBmpButton_new(class="type">void){}; ~CBmpButton_new(class="type">void){}; class="kw">virtual class="type">bool Save(class="kw">const class="type">int file_handle) { if(file_handle==INVALID_HANDLE) { class="kw">return class="kw">false; } class="kw">return(FileWriteInteger(file_handle,Pressed())); } class="kw">virtual class="type">bool Load(class="kw">const class="type">int file_handle) { if(file_handle==INVALID_HANDLE) { class="kw">return class="kw">false; }
◍ 面板控件绑定与运行入口
这套自定义交易面板的下半个声明段,把界面元素和内部变量一一挂上了钩。CEdit_new 系列负责把交易量、止损/获利的点数与资金数、风险百分比与资金数直接显示到编辑框;CBmpButton_new 系列则做成复选或单选按钮,控制止损线、获利线是否绘制,以及当前用点数还是资金数来定义止损获利。 Run() 是面板启动后的主入口。它先调 IniFileLoad() 从配置文件恢复上次的参数,再用 StringToDouble / StringToInteger 把编辑框里的文本转成数值写进 cur_lot、cur_sl_pips、cur_sl_money 等成员。注意 cur_sl_pips 和 cur_tp_pips 做了 (int) 强转,意味着面板只接受整数点,小数点输入会被截断。 Run() 里把 RiskByValue 置为 true,说明默认按资金规模算风险,然后触发 StopLossLineClick() 与 TakeProfitLineClick() 刷新图形层,最后交回 CAppDialog::Run() 维持消息循环。想在 MT5 里改默认风控逻辑,从这几行入手最直接。外汇与贵金属波动剧烈,面板参数仅辅助决策,实际成交风险仍可能远超显示值。
class="kw">return(Pressed((class="type">bool)FileReadInteger(file_handle))); } }; CEdit_new Lots; class=class="str">"cmt">// 显示下个订单的交易量 CEdit_new StopLoss_pips; class=class="str">"cmt">// 显示止损点数 CEdit_new StopLoss_money; class=class="str">"cmt">// 显示止损资金数 CEdit_new TakeProfit_pips; class=class="str">"cmt">// 显示获利点数 CEdit_new TakeProfit_money; class=class="str">"cmt">// 显示获利资金数 CEdit_new Risk_percent; class=class="str">"cmt">// 显示风险占净值百分比 CEdit_new Risk_money; class=class="str">"cmt">// 显示风险资金数 CBmpButton_new StopLoss_line; class=class="str">"cmt">// 显示止损线的复选框 CBmpButton_new TakeProfit_line; class=class="str">"cmt">// 显示获利线的复选框 CBmpButton_new StopLoss_pips_b; class=class="str">"cmt">// 选择止损点数 CBmpButton_new StopLoss_money_b; class=class="str">"cmt">// 选择止损资金数 CBmpButton_new TakeProfit_pips_b; class=class="str">"cmt">// 选择获利点数 CBmpButton_new TakeProfit_money_b; class=class="str">"cmt">// 选择获利资金数 CBmpButton_new Risk_percent_b; class=class="str">"cmt">// 选择风险占净值百分比 CBmpButton_new Risk_money_b; class=class="str">"cmt">// 选择风险资金数 class="kw">public: ................. class="kw">virtual class="type">bool Run(class="type">void); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 运行交易面板 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CTradePanel::Run(class="type">void) { IniFileLoad(); cur_lot=StringToDouble(Lots.Text()); cur_sl_pips=(class="type">int)StringToInteger(StopLoss_pips.Text()); class=class="str">"cmt">// 止损点数 cur_sl_money=StringToDouble(StopLoss_money.Text()); class=class="str">"cmt">// 止损资金数 cur_tp_pips=(class="type">int)StringToInteger(TakeProfit_pips.Text()); class=class="str">"cmt">// 获利点数 cur_tp_money=StringToDouble(TakeProfit_money.Text()); class=class="str">"cmt">// 获利资金数 cur_risk_percent=StringToDouble(Risk_percent.Text()); class=class="str">"cmt">// 风险百分比 cur_risk_money=StringToDouble(Risk_money.Text()); class=class="str">"cmt">// 风险资金数 RiskByValue=true; StopLossLineClick(); TakeProfitLineClick(); class="kw">return(CAppDialog::Run()); }
退出前只拆自己的线
EA 被手动关闭、品种切换或重载时,终端会抛出 Deinit 事件并触发 OnDeinit(const int reason),reason 码能告诉你退出的具体诱因。这一步如果不自己做清除,图表上残留的水平线会一直挂到下一次加载,干扰后续判读。 正确做法是只删本程序画的物件,用户或第三方指标创建的对象原样保留。我们在 OnDeinit 里转调交易面板类的 Destroy(reason),由它精准拆除自有的止损止盈线,再交给父类收尾。 下面这段是实际可贴进 MT5 的骨架:OnDeinit 仅一行转发;类里 Destroy 先删四条水平线(BuySL/SellSL/BuyTP/SellTP),再调用 CAppDialog::Destroy 释放面板。外汇与贵金属波动剧烈,EA 异常退出后图表脏对象可能引发误判,上线前务必在策略测试器用不同 reason 码跑一遍退出逻辑。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA 终止化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- TradePanel.Destroy(reason); class="kw">return; } class="kw">public: ............. class="kw">virtual class="type">void Destroy(class="kw">const class="type">int reason); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 应用程序终止函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTradePanel::Destroy(class="kw">const class="type">int reason) { BuySL.Delete(); SellSL.Delete(); BuyTP.Delete(); SellTP.Delete(); CAppDialog::Destroy(reason); class="kw">return; }
「面板交流里藏着下一版方向」
这篇交易面板文章到这儿就结束了,但工具本身还能长。评论区里 SkyWalkerFX0305 提到 y2 设大后面板没按预期变大,sydongjsh 问最小化怎么贴右下角,kazu 则实测按下「关闭所有」只平掉一个仓、盈亏只算在首仓——这些都不是闲聊,是直接在告诉你 ex5 里哪段逻辑还没闭环。 如果你手头有 tradepanel.mq5(56.85 KB)和编译好的 tradepanel.ex5(319.32 KB),现在就能开 MT5 加载验证:多仓状态下点全部平仓,看是否只平一单;再把面板拖到屏幕边缘,看最小化管理是否符合预期。外汇和贵金属杠杆高,面板只是执行辅助,仓位错了它跑得越快亏得越狠。 作者留了口子说会采纳有趣想法,Xiang Zeng Chen 要的「按止损百分比全平盈/亏」、Cristobal 要的回测(BT)功能,目前源码里都没有。想改就从这两类评论对应的事件函数入手,比从零写省事得多。