创建一个人工交易助手·综合运用
🛠️

创建一个人工交易助手·综合运用

(3/3)· 前面两篇铺垫了思路与基础控件,这一篇把 19 个环节串成能用的面板

含代码示例实战向 第 3/3 篇
很多交易者把面板写成一堆按钮堆在图表上,结果平仓时手忙脚乱找不对品种。缺少统一事件处理和参数持久化,重启终端后又要重填一遍,实盘里这种重复劳动会直接吃掉反应时间。

「用位图按钮给图表挂交互开关」

在 MT5 自定义指标或 EA 里,想让交易者在图表上点一下就切换状态,最轻量的做法是直接画一个位图按钮(BmpButton),而不是堆输入框和菜单。 下面这个函数声明把创建动作拆得很清楚:传入图表 ID、子窗口号,再给一个 CBmpButton 对象的引用,函数内部会帮你把对象绑到坐标 (x,y) 上。 按钮的两态靠 BmpON 和 BmpOFF 两张图区分——亮的是开,暗的是关;最后的 lock 参数决定鼠标拖拽时是否锁死位置。外汇与贵金属波动剧烈、滑点频繁,这类界面控件只负责发指令,进出场风险仍由策略逻辑扛。 你打开 MT5 的 MetaEditor,搜 CBmpButton 用例,把这段声明抄进你的面板类,十分钟就能跑出一个可点的开关原型。

MQL5 / C++
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 策略测试器先验证坐标缩放表现。

MQL5 / C++
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 分支改对应栏位的手数和浮盈;同时别漏掉图表上止损获利线的复选框状态,勾了就得重绘水平线。下面这段是核心调用的骨架。

MQL5 / C++
<span class="keyword">class="kw">public</span>:
&nbsp;&nbsp; <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">void</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<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 订单处理函数&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="keyword">class="type">void</span> <span class="functions">OnTick</span>()
&nbsp;&nbsp;{
<span class="comment">class=class="str">"cmt">//---</span>
&nbsp;&nbsp; TradePanel.<span class="functions">OnTick</span>();
&nbsp;&nbsp;}
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| New Tick 事件&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|</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>)
&nbsp;&nbsp;{
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//--- 修改面板上的买入和卖出价格</span>
&nbsp;&nbsp; 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>)));
&nbsp;&nbsp; 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>
&nbsp;&nbsp; <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>))
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;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>));
&nbsp;&nbsp;&nbsp;&nbsp; }

<span class="comment">class=class="str">"cmt">//--- 检查和修改 (如有必要) 买入和卖出的手数以及利润.</span>
&nbsp;&nbsp; <span class="keyword">if</span>(<span class="functions">PositionSelect</span>(<span class="predefines">_Symbol</span>))
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<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>))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">case</span> <span class="macro">POSITION_TYPE_BUY</span>:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 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>));
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <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>))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 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>));
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <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>))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{

「空仓与持空时面板怎么归零与刷新」

这段逻辑管的是交易面板里买卖两边文字的实时同步。当持仓遍历结束却没碰到对应类型,或者根本无持仓时,代码会把对面数值强制归零,避免旧仓位信息残留误导判断。 看 POSITION_TYPE_SELL 分支:有空单时,空单利润用 PositionGetDouble(POSITION_PROFIT) 取真实值并保留两位小数,再拼账户币种字符串;空单手数同理从 POSITION_VOLUME 读。同时把买侧的 profit 和 lots 文本都设为 "0.00",因为同一时刻单边持仓下另一侧不应有数值。 else 分支覆盖无持仓情形:买卖手数文本若不等于 DoubleToString(0,2) 就改写为 0.00;买侧利润文本若不等于 "0.00 "+币种 也重设。这样面板在空仓时稳定显示双零,不会因上次缓存出错。 外汇与贵金属保证金交易杠杆高,面板数值仅作辅助,实际下单前仍以终端成交回执为准。

MQL5 / C++
          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。外汇与贵金属波动剧烈,面板仅作辅助,下单前仍须人工核对。

MQL5 / C++
   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 绑到具体方法。

MQL5 / C++
<span class="keyword">class="kw">public</span>:
&nbsp;&nbsp; <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;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> &amp;lparam, <span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">double</span> &amp;dparam, <span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">string</span> &amp;sparam);
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| ChartEvent 函数&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|</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,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">const</span> <span class="keyword">class="type">long</span> &amp;lparam,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">double</span> &amp;dparam,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">string</span> &amp;sparam)
&nbsp;&nbsp;{
<span class="comment">class=class="str">"cmt">//---</span>
&nbsp;&nbsp; TradePanel.OnEvent(id, lparam, dparam, sparam);
&nbsp;&nbsp;}
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| 事件的处理&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
EVENT_MAP_BEGIN(CTradePanel)
&nbsp;&nbsp; ON_EVENT(ON_END_EDIT,Lots,LotsEndEdit)
&nbsp;&nbsp; ON_EVENT(ON_END_EDIT,StopLoss_pips,SLPipsEndEdit)
&nbsp;&nbsp; ON_EVENT(ON_END_EDIT,TakeProfit_pips,TPPipsEndEdit)
&nbsp;&nbsp; ON_EVENT(ON_END_EDIT,StopLoss_money,SLMoneyEndEdit)
&nbsp;&nbsp; ON_EVENT(ON_END_EDIT,TakeProfit_money,TPMoneyEndEdit)
&nbsp;&nbsp; ON_EVENT(ON_END_EDIT,Risk_percent,RiskPercentEndEdit)
&nbsp;&nbsp; ON_EVENT(ON_END_EDIT,Risk_money,RiskMoneyEndEdit)
EVENT_MAP_END(CAppDialog)
<span class="keyword">class="kw">private</span>:
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//--- On Event 函数</span>
&nbsp;&nbsp; <span class="keyword">class="type">void</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;LotsEndEdit(<span class="keyword">class="type">void</span>);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// 编辑手数</span>
&nbsp;&nbsp; <span class="keyword">class="type">void</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;SLPipsEndEdit(<span class="keyword">class="type">void</span>);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// 编辑止损点数</span>
&nbsp;&nbsp; <span class="keyword">class="type">void</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;TPPipsEndEdit(<span class="keyword">class="type">void</span>);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// 编辑获利点数</span>

「手数改动后如何联动止损止盈」

交易面板里手数一改,止损止盈的点数和资金数不能各算各的,否则仓位风控就失真了。下面这段 CTradePanel::LotsEndEdit 就是干这个联动的:先把手数规范化,再按当前面板按钮状态反向推导其余字段。 核心逻辑是读取编辑框文本转 double,用 NormalizeLots 对齐经纪商最小步长,然后以两位精度回写。之后四个 if 分别判断用户上次是按资金还是点数模式在编辑,调用对应换算函数。比如 StopLoss_money_b 被按下过,就用手数+资金反推止损点数。 RiskByValue 这个私有布尔量默认 true,代表「按数值算风险」;它决定了联动时以哪一侧为基准。外汇和贵金属杠杆高,这类自动换算若步长设错,可能让实际止损偏离预期十几美元,建议开 MT5 把 NormalizeLots 的精度参数和你账户最小手数对照跑一遍。

MQL5 / C++
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 是否随账户余额线性变化。

MQL5 / C++
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 才去算。

MQL5 / C++
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 打印出来,对照你面板按钮的加减幅度,就能验证是不是踩了边界。外汇与贵金属杠杆高,手数算错可能瞬间放大敞口,验证后再上真仓。

MQL5 / C++
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 时整个面板创建中断。

MQL5 / C++
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 派生类,点面板上的止损线按钮,图表上买卖止损线应当随按压力在全部周期出现或消失。

MQL5 / C++
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,具体代码可直接扒附件文件照抄改。

MQL5 / C++
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 用策略测试器单步跑一遍,能直接看到调用栈提前退出的差异。

MQL5 / C++
  class="kw">return;
}

让鼠标拖线代替手动算点数的止损移仓

很多交易者习惯在图表上把止损或获利线拖到更有意义的价位,而不是靠脑子算距离当前价多少点。直接让用户填点数的做法体验很差,更合理的做法是:用户只管拖水平线,程序在后台把鼠标位置的价差换算成点数并写回面板。 实现上不必自己重写鼠标移动逻辑,直接借用终端标准的 CHARTEVENT_OBJECT_DRAG 事件。只要水平线对象被拖动,OnChartEvent 收到该事件并把对象名传给 DragLine,就能触发后续处理。 DragLine 内部先判断被拖的是 BuySL、SellSL、BuyTP 还是 SellTP,随后用 MathAbs(线价 - 当前 Ask/Bid) / _Point 算出整数点差,刷新面板对应栏位,再调用相关编辑函数并重绘图表。注意拖完要把线设为未选中(Selected(false)),否则高亮状态会一直挂在那。 外汇与贵金属杠杆高、滑点大,拖线只是改了挂单参考价,实际成交价可能因跳空偏离,入场前务必确认点差与流动性。

MQL5 / C++
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() 状态整数。面板里把对象类型换成这两个新类,终止化时父类就会自动落盘。 读取侧在面板初始化时先读文件、再刷变量,最后手动调一次复选框处理函数,让止损/获利水平与界面状态同步。外汇与贵金属波动剧烈,这类面板参数持久化只解决操作麻烦,不意味任何交易优势,实盘前请在策略测试器跑通存档路径。

MQL5 / C++
  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 里改默认风控逻辑,从这几行入手最直接。外汇与贵金属波动剧烈,面板参数仅辅助决策,实际成交风险仍可能远超显示值。

MQL5 / C++
  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 码跑一遍退出逻辑。

MQL5 / C++
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)功能,目前源码里都没有。想改就从这两类评论对应的事件函数入手,比从零写省事得多。

把重复劳动交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到面板外的盘口异常提示,你专注决策就好。

常见问题

常用 CLabel、CEdit、CButton 和 CBmpButton 等标准库类,在派生类的 Create 函数里实例化并排布。
多半是图表对象拖动事件没有回写控件,需要在对应事件里抓取价格变化并刷新编辑框与标签显示。
小布盯盘提供品种页的 AIGC 诊断与提示,不托管第三方 EA 面板;你可以对照它的风险视图校验自己的平仓逻辑。
倾向用全局变量或写入 ini 文件,在 OnInit 读取并回填控件,避免每次手动重输,外汇贵金属波动快高风险需注意。
要在单选按钮事件处理函数里触发依赖参数的重算链,并调用更新函数刷新编辑框与按钮标签。