MQL5交易管理面板开发(第九部分):代码组织(4):交易管理面板类·进阶篇
🧩

MQL5交易管理面板开发(第九部分):代码组织(4):交易管理面板类·进阶篇

(2/3)· 从散装函数到CTradeManagementPanel类,解决大型面板集成与维护的两大难题

含代码示例实战向 第 2/3 篇
很多交易者把面板逻辑全塞进EA主文件,改一个按钮就要翻几百行。类边界不清,挂单和风控混在一起,后期加分析界面基本等于重写。本文接着上篇,把交易管理收进独立类,少踩坑。

◍ 挂单面板的控件坐标怎么排

这段逻辑在搭一个自定义交易面板的 UI,先把市价单的成交量编辑框和卖出按钮画出来,再往下排 TP/SL 输入行,最后切到挂单区画表头。坐标全靠 curX、curY 和 GAP 这几个变量递推,改一个间距常数,整块布局会跟着挪。 成交量编辑框创建完之后,代码会去读 SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN),把当前品种的最小交易量塞进框里并保留 2 位小数。不同品种这个值差很多,比如 XAUUSD 常见是 0.01,而某些外汇对可能是 0.0001 量级,面板初始化时显示的数字直接受它控制。 TP 和 SL 的编辑框默认写死 "0.00000",旁边各挂一个报错标签(初始空串),放在输入框下方一行的 DEFAULT_LABEL_HEIGHT 处。注意 slX 被算成 2*EDIT_WIDTH,但实际创建时又加了 4*curX 偏移,这种混合坐标写法在后续微调列宽时很容易算错,建议开 MT5 把这几个宏打印出来核对。 挂单区表头用 headerX = curX + BUTTON_WIDTH + GAP 起头,后面 TP、SL、过期时间标签各自再叠 EDIT_WIDTH+GAP 的步长。最后用 TimeCurrent() 取当前时间,给挂单过期时间准备默认值——外汇和贵金属杠杆高,挂单过期设错可能让你在波动中吃到意外成交,验证前先把这段跑一遍看日期格式。

MQL5 / C++
  class="kw">return class="kw">false;
  if(!CreateEdit(m_volumeEdit, "VolumeEdit", volX, curY, EDIT_WIDTH, EDIT_HEIGHT))
    class="kw">return class="kw">false;
  class="type">class="kw">double minVolume = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN);
  m_volumeEdit.Text(DoubleToString(minVolume, class="num">2));
  class="type">int sellBtnX = volX + EDIT_WIDTH + GAP;
  if(!CreateButton(m_sellButton, "Sell", sellBtnX, curY, BUTTON_WIDTH, BUTTON_HEIGHT, clrRed))
    class="kw">return class="kw">false;
  curY += BUTTON_HEIGHT + GAP;

  class=class="str">"cmt">// Next row: TP and SL for market orders
  if(!CreateEdit(m_tpEdit, "TPEdit", class="num">4*curX+ GAP , curY, EDIT_WIDTH, EDIT_HEIGHT))
    class="kw">return class="kw">false;
  m_tpEdit.Text("class="num">0.00000");
  if(!CreateLabelEx(m_tpRightLabel, curX  + INPUT_LABEL_GAP, curY, DEFAULT_LABEL_HEIGHT, "TP", "TP:", clrBlack))
    class="kw">return class="kw">false;
  if(!CreateLabelEx(m_tpErrorLabel, curX, curY + DEFAULT_LABEL_HEIGHT, DEFAULT_LABEL_HEIGHT, "TPError", "", clrBlack))
    class="kw">return class="kw">false;
  class="type">int slX = class="num">2*EDIT_WIDTH ;
  if(!CreateEdit(m_slEdit, "SLEdit", slX + class="num">4*curX, curY, EDIT_WIDTH, EDIT_HEIGHT))
    class="kw">return class="kw">false;
  m_slEdit.Text("class="num">0.00000");
  if(!CreateLabelEx(m_slRightLabel, slX , curY, DEFAULT_LABEL_HEIGHT, "SL", "SL:", clrBlack))
    class="kw">return class="kw">false;
  if(!CreateLabelEx(m_slErrorLabel, slX, curY + DEFAULT_LABEL_HEIGHT, DEFAULT_LABEL_HEIGHT, "SLError", "", clrBlack))
    class="kw">return class="kw">false;
  curY += EDIT_HEIGHT + GAP*class="num">2;

  class=class="str">"cmt">// Section class="num">2: Pending Orders
  if(!CreateLabelEx(m_secPendingLabel, curX, curY, DEFAULT_LABEL_HEIGHT, "SecPend", "Pending Orders:", clrBlack))
    class="kw">return class="kw">false;
  curY += DEFAULT_LABEL_HEIGHT + GAP;

  class=class="str">"cmt">// Column headers for pending orders(each label includes a colon)
  class="type">int headerX = curX + BUTTON_WIDTH + GAP;
  if(!CreateLabelEx(m_pendingPriceHeader, headerX, curY, DEFAULT_LABEL_HEIGHT, "PendPrice", "Price:", clrBlack))
    class="kw">return class="kw">false;
  if(!CreateLabelEx(m_pendingTPHeader, headerX + EDIT_WIDTH + GAP, curY, DEFAULT_LABEL_HEIGHT, "PendTP", "TP:", clrBlack))
    class="kw">return class="kw">false;
  if(!CreateLabelEx(m_pendingSLHeader, headerX + class="num">2*(EDIT_WIDTH + GAP), curY, DEFAULT_LABEL_HEIGHT, "PendSL", "SL:", clrBlack))
    class="kw">return class="kw">false;
  if(!CreateLabelEx(m_pendingExpHeader, headerX + class="num">3*(EDIT_WIDTH + GAP), curY, DEFAULT_LABEL_HEIGHT, "PendExp", "Expiration Date:", clrBlack))
    class="kw">return class="kw">false;
  curY += DEFAULT_LABEL_HEIGHT + GAP;

  class=class="str">"cmt">// Prepare class="kw">default expiration value as current day end time.
  class="type">class="kw">datetime now = TimeCurrent();

「挂单面板的逐行坐标拼装」

在 MT5 自定义面板里铺挂单控件,核心是用横纵偏移量把按钮和编辑框排成一行。下面这段把 Buy Limit / Buy Stop / Sell Limit 的控件生成逻辑直接落到了坐标算术上,每行间距由 GAP 常量控制,垂直方向每铺完一行就令 curY 累加 BUTTON_HEIGHT+GAP。 到期时间编辑框宽度是普通编辑框的两倍(2*EDIT_WIDTH),因为它要塞进「2024.05.20 23:59:59」这种日期字符串;价格、TP、SL 编辑框统一用 EDIT_WIDTH。初始值里价格框直接读 SYMBOL_ASK 或 SYMBOL_BID 并格式化为 5 位小数,TP/SL 先填 0.00000,到期填当日 23:59:59。 [CODE] 里的 CreateButton / CreateEdit 若返回 false 会直接令函数 return false,意味着任一个控件资源申请失败,整面板就不建了——实盘前应在 OnInit 里检查返回值,避免半截 UI 挂上去。外汇与贵金属杠杆高,挂单参数填错可能瞬时成交并放大回撤,先在策略测试器里用可视化面板验证坐标无重叠再上真仓。

MQL5 / C++
  class="type">class="kw">string exp_default = TimeToString(now, TIME_DATE) + " class="num">23:class="num">59:class="num">59";

  class=class="str">"cmt">// --- Buy Pending Orders ---
  class=class="str">"cmt">// Buy Limit Order row:
  if(!CreateButton(m_buyLimitButton, "Buy Limit", curX, curY, BUTTON_WIDTH, BUTTON_HEIGHT, clrBlue))
    class="kw">return class="kw">false;
  class="type">int pendingX = curX + BUTTON_WIDTH + GAP;
  if(!CreateEdit(m_buyLimitPriceEdit, "BuyLimitPrice", pendingX, curY, EDIT_WIDTH, EDIT_HEIGHT))
    class="kw">return class="kw">false;
  class="type">class="kw">string askStr = DoubleToString(SymbolInfoDouble(Symbol(), SYMBOL_ASK), class="num">5);
  m_buyLimitPriceEdit.Text(askStr);
  class="type">int pending2X = pendingX + EDIT_WIDTH + GAP;
  if(!CreateEdit(m_buyLimitTPEdit, "BuyLimitTP", pending2X, curY, EDIT_WIDTH, EDIT_HEIGHT))
    class="kw">return class="kw">false;
  m_buyLimitTPEdit.Text("class="num">0.00000");
  class="type">int pending3X = pending2X + EDIT_WIDTH + GAP;
  if(!CreateEdit(m_buyLimitSLEdit, "BuyLimitSL", pending3X, curY, EDIT_WIDTH, EDIT_HEIGHT))
    class="kw">return class="kw">false;
  m_buyLimitSLEdit.Text("class="num">0.00000");
  class="type">int pending4X = pending3X + EDIT_WIDTH + GAP; class=class="str">"cmt">// Double width for expiration input
  if(!CreateEdit(m_buyLimitExpEdit, "BuyLimitExp", pending4X, curY, class="num">2*EDIT_WIDTH, EDIT_HEIGHT))
    class="kw">return class="kw">false;
  m_buyLimitExpEdit.Text(exp_default);
  curY += BUTTON_HEIGHT + GAP;

  class=class="str">"cmt">// Buy Stop Order row:
  if(!CreateButton(m_buyStopButton, "Buy Stop", curX, curY, BUTTON_WIDTH, BUTTON_HEIGHT, clrBlue))
    class="kw">return class="kw">false;
  pendingX = curX + BUTTON_WIDTH + GAP;
  if(!CreateEdit(m_buyStopPriceEdit, "BuyStopPrice", pendingX, curY, EDIT_WIDTH, EDIT_HEIGHT))
    class="kw">return class="kw">false;
  m_buyStopPriceEdit.Text(askStr);
  pending2X = pendingX + EDIT_WIDTH + GAP;
  if(!CreateEdit(m_buyStopTPEdit, "BuyStopTP", pending2X, curY, EDIT_WIDTH, EDIT_HEIGHT))
    class="kw">return class="kw">false;
  m_buyStopTPEdit.Text("class="num">0.00000");
  pending3X = pending2X + EDIT_WIDTH + GAP;
  if(!CreateEdit(m_buyStopSLEdit, "BuyStopSL", pending3X, curY, EDIT_WIDTH, EDIT_HEIGHT))
    class="kw">return class="kw">false;
  m_buyStopSLEdit.Text("class="num">0.00000");
  pending4X = pending3X + EDIT_WIDTH + GAP;
  if(!CreateEdit(m_buyStopExpEdit, "BuyStopExp", pending4X, curY, class="num">2*EDIT_WIDTH, EDIT_HEIGHT))
    class="kw">return class="kw">false;
  m_buyStopExpEdit.Text(exp_default);
  curY += BUTTON_HEIGHT + GAP;

  class=class="str">"cmt">// --- Sell Pending Orders ---
  class=class="str">"cmt">// Sell Limit Order row:
  if(!CreateButton(m_sellLimitButton, "Sell Limit", curX, curY, BUTTON_WIDTH, BUTTON_HEIGHT, clrRed))
    class="kw">return class="kw">false;
  pendingX = curX + BUTTON_WIDTH + GAP;
  if(!CreateEdit(m_sellLimitPriceEdit, "SellLimitPrice", pendingX, curY, EDIT_WIDTH, EDIT_HEIGHT))
    class="kw">return class="kw">false;
  class="type">class="kw">string bidStr = DoubleToString(SymbolInfoDouble(Symbol(), SYMBOL_BID), class="num">5);
  m_sellLimitPriceEdit.Text(bidStr);
  pending2X = pendingX + EDIT_WIDTH + GAP;
  if(!CreateEdit(m_sellLimitTPEdit, "SellLimitTP", pending2X, curY, EDIT_WIDTH, EDIT_HEIGHT))

挂单面板与批量平仓按钮的布局落点

这段逻辑紧接着 Sell Limit 行之后,把 Sell Stop 挂单的输入控件也按同样的横向间距排好。每个编辑框创建失败就直接 return false,保证面板不完整时不继续往下画。 Sell Stop 行的 price 编辑框默认填入 bidStr(即当前卖价字符串),而 TP、SL 编辑框初始写死为 "0.00000",有效期编辑框宽度是普通编辑框的两倍(2*EDIT_WIDTH),默认文本取 exp_default。 随后进入“All Order Operations”区段,先放一个标签,再在两行内铺完 9 个操作按钮:Close All / Close Profit 用红绿配色,Delete Limits 和 Delete Stops 放在 deleteX 位置(curX + 2*BUTTON_WIDTH + 2*GAP),Delete Stops 按钮宽度比 Delete Limits 多 10 像素。 Reset 按钮高度设为 2*BUTTON_HEIGHT 且纵向占两行空间,其 X 坐标用 deleteX + 2*BUTTON_WIDTH - 3*curX 算出来,这种写法在 curX 非 0 时容易偏出预期,建议开 MT5 把 curX 打印出来核对实际落点。外汇与贵金属杠杆交易风险高,面板逻辑错误可能误触平仓。

MQL5 / C++
   class="kw">return class="kw">false;
   m_sellLimitTPEdit.Text("class="num">0.00000");
   pending3X = pending2X + EDIT_WIDTH + GAP;
   if(!CreateEdit(m_sellLimitSLEdit, "SellLimitSL", pending3X, curY, EDIT_WIDTH, EDIT_HEIGHT))
      class="kw">return class="kw">false;
   m_sellLimitSLEdit.Text("class="num">0.00000");
   pending4X = pending3X + EDIT_WIDTH + GAP;
   if(!CreateEdit(m_sellLimitExpEdit, "SellLimitExp", pending4X, curY, class="num">2*EDIT_WIDTH, EDIT_HEIGHT))
      class="kw">return class="kw">false;
   m_sellLimitExpEdit.Text(exp_default);
   curY += BUTTON_HEIGHT + GAP;

   class=class="str">"cmt">// Sell Stop Order row:
   if(!CreateButton(m_sellStopButton, "Sell Stop", curX, curY, BUTTON_WIDTH, BUTTON_HEIGHT, clrRed))
      class="kw">return class="kw">false;
   pendingX = curX + BUTTON_WIDTH + GAP;
   if(!CreateEdit(m_sellStopPriceEdit, "SellStopPrice", pendingX, curY, EDIT_WIDTH, EDIT_HEIGHT))
      class="kw">return class="kw">false;
   m_sellStopPriceEdit.Text(bidStr);
   pending2X = pendingX + EDIT_WIDTH + GAP;
   if(!CreateEdit(m_sellStopTPEdit, "SellStopTP", pending2X, curY, EDIT_WIDTH, EDIT_HEIGHT))
      class="kw">return class="kw">false;
   m_sellStopTPEdit.Text("class="num">0.00000");
   pending3X = pending2X + EDIT_WIDTH + GAP;
   if(!CreateEdit(m_sellStopSLEdit, "SellStopSL", pending3X, curY, EDIT_WIDTH, EDIT_HEIGHT))
      class="kw">return class="kw">false;
   m_sellStopSLEdit.Text("class="num">0.00000");
   pending4X = pending3X + EDIT_WIDTH + GAP;
   if(!CreateEdit(m_sellStopExpEdit, "SellStopExp", pending4X, curY, class="num">2*EDIT_WIDTH, EDIT_HEIGHT))
      class="kw">return class="kw">false;
   m_sellStopExpEdit.Text(exp_default);
   curY += BUTTON_HEIGHT + GAP;

   class=class="str">"cmt">// Section class="num">3: All Order Operations
   if(!CreateLabelEx(m_secAllOpsLabel, curX, curY, DEFAULT_LABEL_HEIGHT, "SecAllOps", "All Order Operations:", clrBlack))
      class="kw">return class="kw">false;
   curY += DEFAULT_LABEL_HEIGHT + GAP;   

   class="type">int deleteX = curX + class="num">2*BUTTON_WIDTH + class="num">2*GAP;
   class="type">int deleteY = curY  ;       
   CreateButton(m_closeAllButton, "Close All", curX, curY, BUTTON_WIDTH, BUTTON_HEIGHT, clrRed);
   CreateButton(m_closeProfitButton, "Close Profit", curX + BUTTON_WIDTH + GAP, curY, BUTTON_WIDTH, BUTTON_HEIGHT, clrGreen);
   CreateButton(m_deleteLimitOrdersButton, "Delete Limits", deleteX, curY, DELETE_BUTTON_WIDTH, BUTTON_HEIGHT, clrRed);
   curY += BUTTON_HEIGHT + GAP;

   CreateButton(m_closeLossButton, "Close Loss", curX, curY, BUTTON_WIDTH, BUTTON_HEIGHT, clrRed);
   CreateButton(m_closeBuyButton, "Close Buy", curX + BUTTON_WIDTH + GAP, curY, BUTTON_WIDTH, BUTTON_HEIGHT, clrGreen);
   CreateButton(m_deleteStopOrdersButton, "Delete Stops", deleteX, curY , DELETE_BUTTON_WIDTH+class="num">10, BUTTON_HEIGHT, clrRed);
   CreateButton(m_resetButton, "Reset", deleteX+ class="num">2*BUTTON_WIDTH-class="num">3*curX, curY, DELETE_BUTTON_WIDTH, class="num">2*BUTTON_HEIGHT, clrRed);
   curY += BUTTON_HEIGHT + GAP;

   CreateButton(m_closeSellButton, "Close Sell", curX, curY, BUTTON_WIDTH, BUTTON_HEIGHT, clrBlue);
   CreateButton(m_deleteAllOrdersButton, "Delete All", curX + BUTTON_WIDTH + GAP, curY, BUTTON_WIDTH, BUTTON_HEIGHT, clrRed);
   CreateButton(m_deleteStopLimitOrdersButton, "Delete StopLimits", deleteX, curY , DELETE_BUTTON_WIDTH+class="num">10, BUTTON_HEIGHT, clrDarkRed);
   curY += BUTTON_HEIGHT + GAP;

   class="kw">return true;

◍ 面板控件封装与点击事件分发

在 MT5 自定义面板里,把文字标签、按钮、输入框的创建逻辑收进三个 Helper,能省掉主流程里大量重复的坐标与资源判断。下面这组函数都返回 bool,任意一步 Create 或 Add 失败就直接 false,调用方不用再逐个查句柄。 CreateLabelEx 先拼出全局唯一名(m_name + label_name),再按 (x, y) 到 (x, y+height) 的矩形建标签;文本和颜色在最后赋值,意味着标签底色默认走控件基类,你想改必须显式调 Color()。 按钮和编辑框的 Helper 给了默认宽高参(BUTTON_WIDTH / EDIT_WIDTH 等),实际写死在宏里,改尺寸要去头部调宏而不是传参覆盖。CreateButton 里 Text(name) 直接把按钮名当显示文字,若想显示中文交易指令,得在调用前改 name 或单独setText。 OnEvent 只认 CHARTEVENT_OBJECT_CLICK:用 sparam 比对六个下单键加两个平仓键的 Name(),命中即转发到对应的 OnClickXxx。外汇与贵金属波动剧烈,这类一键发单面板若没做二次确认,滑点可能吞掉你预设的止损空间,上线前务必在策略测试器里用历史 tick 跑一遍点击链路。

MQL5 / C++
class="type">bool CreateLabelEx(CLabel &label, class="type">int x, class="type">int y, class="type">int height, class="type">class="kw">string label_name, class="type">class="kw">string text, class="type">color clr)
  {
     class="type">class="kw">string unique_name = m_name + label_name;
     if(!label.Create(m_chart, unique_name, m_subwin, x, y, x, y+height))
        class="kw">return class="kw">false;
     if(!Add(label))
        class="kw">return class="kw">false;
     if(!label.Text(text))
        class="kw">return class="kw">false;
     label.Color(clr);
     class="kw">return true;
  }

  class="type">bool CreateButton(CButton &button, class="type">class="kw">string name, class="type">int x, class="type">int y, class="type">int w = BUTTON_WIDTH, class="type">int h = BUTTON_HEIGHT, class="type">color clr = clrWhite)
  {
     if(!button.Create(m_chart, name, m_subwin, x, y, x+w, y+h))
        class="kw">return class="kw">false;
     button.Text(name);
     button.Color(clr);
     if(!Add(button))
        class="kw">return class="kw">false;
     class="kw">return true;
  }

  class="type">bool CreateEdit(CEdit &edit, class="type">class="kw">string name, class="type">int x, class="type">int y, class="type">int w = EDIT_WIDTH, class="type">int h = EDIT_HEIGHT)
  {
     if(!edit.Create(m_chart, name, m_subwin, x, y, x+w, y+h))
        class="kw">return class="kw">false;
     if(!Add(edit))
        class="kw">return class="kw">false;
     class="kw">return true;
  }

class="type">bool CTradeManagementPanel::OnEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam)
{
   if(id == CHARTEVENT_OBJECT_CLICK)
   {
      if(sparam == m_buyButton.Name())            OnClickBuy();
      else if(sparam == m_sellButton.Name())      OnClickSell();
      else if(sparam == m_buyLimitButton.Name())  OnClickBuyLimit();
      else if(sparam == m_buyStopButton.Name())   OnClickBuyStop();
      else if(sparam == m_sellLimitButton.Name()) OnClickSellLimit();
      else if(sparam == m_sellStopButton.Name())  OnClickSellStop();
      else if(sparam == m_closeAllButton.Name())  OnClickCloseAll();
      else if(sparam == m_closeProfitButton.Name()) OnClickCloseProfit();

「按钮路由与买卖单参数校验的逻辑落点」

面板上的每一个功能按钮,在 OnClick 事件里都靠 sparam 与按钮 Name() 比对来分流。下面这段链式 else if 把平仓、删单、重置等动作一一映射到各自处理函数,最后 return true 表示事件已消化。 else if(sparam == m_closeLossButton.Name()) OnClickCloseLoss(); else if(sparam == m_closeBuyButton.Name()) OnClickCloseBuy(); else if(sparam == m_closeSellButton.Name()) OnClickCloseSell(); else if(sparam == m_deleteAllOrdersButton.Name()) OnClickDeleteAllOrders(); else if(sparam == m_deleteLimitOrdersButton.Name()) OnClickDeleteLimitOrders(); else if(sparam == m_deleteStopOrdersButton.Name()) OnClickDeleteStopOrders(); else if(sparam == m_deleteStopLimitOrdersButton.Name()) OnClickDeleteStopLimitOrders(); else if(sparam == m_resetButton.Name()) OnClickReset(); // Handle reset button click 买单校验 ValidateBuyParameters 接收 volume、tp、sl、ask 四个量:volume<=0 直接标红 Invalid volume;tp!=0 且 tp<=ask 判为 Invalid TP;sl!=0 且 sl>=ask 判为 Invalid SL。任一不满足 valid 置 false,标签变红。 卖单校验逻辑镜像对称:tp!=0 且 tp>=bid 才非法,sl!=0 且 sl<=bid 才非法。外汇与贵金属杠杆高,滑点可能让 ask/bid 瞬间越界,这类前置拦截能减少错单概率,但无法消除执行风险。 开 MT5 把这段塞进你的面板类,故意填 sl=ask 的买单,应该看到 SL 标签立刻标红且 valid 返回 false。

MQL5 / C++
else if(sparam == m_closeLossButton.Name())            OnClickCloseLoss();
else if(sparam == m_closeBuyButton.Name())             OnClickCloseBuy();
else if(sparam == m_closeSellButton.Name())            OnClickCloseSell();
else if(sparam == m_deleteAllOrdersButton.Name())      OnClickDeleteAllOrders();
else if(sparam == m_deleteLimitOrdersButton.Name())    OnClickDeleteLimitOrders();
else if(sparam == m_deleteStopOrdersButton.Name())     OnClickDeleteStopOrders();
else if(sparam == m_deleteStopLimitOrdersButton.Name()) OnClickDeleteStopLimitOrders();
else if(sparam == m_resetButton.Name())                OnClickReset(); class=class="str">"cmt">// Handle reset button click
}
class="kw">return true;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Validate Buy order parameters(Market orders)                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CTradeManagementPanel::ValidateBuyParameters(class="type">class="kw">double volume, class="type">class="kw">double tp, class="type">class="kw">double sl, class="type">class="kw">double ask)
{
   class="type">bool valid = true;
   m_tpErrorLabel.Text("");
   m_slErrorLabel.Text("");
   if(volume <= class="num">0)
   {
      m_tpErrorLabel.Text("Invalid volume");
      m_tpErrorLabel.Color(clrRed);
      valid = class="kw">false;
   }
   if(tp != class="num">0 && tp <= ask)
   {
      m_tpErrorLabel.Text("Invalid TP");
      m_tpErrorLabel.Color(clrRed);
      valid = class="kw">false;
   }
   if(sl != class="num">0 && sl >= ask)
   {
      m_slErrorLabel.Text("Invalid SL");
      m_slErrorLabel.Color(clrRed);
      valid = class="kw">false;
   }
   class="kw">return valid;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Validate Sell order parameters(Market orders)                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CTradeManagementPanel::ValidateSellParameters(class="type">class="kw">double volume, class="type">class="kw">double tp, class="type">class="kw">double sl, class="type">class="kw">double bid)
{
   class="type">bool valid = true;
   m_tpErrorLabel.Text("");
   m_slErrorLabel.Text("");
   if(volume <= class="num">0)
   {
      m_tpErrorLabel.Text("Invalid volume");
      m_tpErrorLabel.Color(clrRed);
      valid = class="kw">false;
   }
   if(tp != class="num">0 && tp >= bid)
   {
      m_tpErrorLabel.Text("Invalid TP");
      m_tpErrorLabel.Color(clrRed);
      valid = class="kw">false;
   }
   if(sl != class="num">0 && sl <= bid)
   {
      m_slErrorLabel.Text("Invalid SL");
      m_slErrorLabel.Color(clrRed);
      valid = class="kw">false;
   }
   class="kw">return valid;
}
class=class="str">"cmt">//+------------------------------------------------------------------+

挂单面板的到期解析与四类触发函数

面板把限价/止损挂单的录入逻辑收敛成四个点击处理函数,BuyLimit、BuyStop、SellLimit、SellStop 各占一个,结构完全对称。每个函数先从对应编辑框取价格、TP、SL、手数和到期字符串,再统一交给 ParseExpiration 判定时间类型。 ParseExpiration 只做一件事:如果输入字符串转大写后等于 GTC,就把 type_time 设为 ORDER_TIME_GTC 且 expiration 填 0;否则视为指定时间,调用 StringToTime 把字符串转成 datetime 并置为 ORDER_TIME_SPECIFIED。这样用户在框里填 GTC 或 2024.08.01 12:00 都能被正确处理。 外汇与贵金属挂单受点差和跳空影响,指定过期时间的挂单可能在非活跃时段未被触发而直接作废,属于正常概率风险。建议先在策略测试器用历史数据跑一遍四个函数,确认 GTC 与具体时间两种分支都不会抛 INVALID_PARAMETER。

MQL5 / C++
class="type">void ParseExpiration(class="type">class="kw">string sExp, ENUM_ORDER_TYPE_TIME &type_time, class="type">class="kw">datetime &expiration)
{
   if(StringCompare(StringToUpper(sExp), "GTC") == class="num">0)
   {
      type_time = ORDER_TIME_GTC;
      expiration = class="num">0;
   }
   else
   {
      type_time = ORDER_TIME_SPECIFIED;
      expiration = StringToTime(sExp);
   }
}
class="type">void CTradeManagementPanel::OnClickBuyLimit()
{
   class="type">class="kw">double price  = StringToDouble(m_buyLimitPriceEdit.Text());
   class="type">class="kw">double tp     = StringToDouble(m_buyLimitTPEdit.Text());
   class="type">class="kw">double sl     = StringToDouble(m_buyLimitSLEdit.Text());
   class="type">class="kw">double volume = StringToDouble(m_volumeEdit.Text());
   class="type">class="kw">string sExp   = m_buyLimitExpEdit.Text();
   ENUM_ORDER_TYPE_TIME type_time;
   class="type">class="kw">datetime expiration;
   ParseExpiration(sExp, type_time, expiration);
   m_trade.BuyLimit(volume, price, Symbol(), sl, tp, type_time, expiration, "");
}
class="type">void CTradeManagementPanel::OnClickBuyStop()
{
   class="type">class="kw">double price  = StringToDouble(m_buyStopPriceEdit.Text());
   class="type">class="kw">double tp     = StringToDouble(m_buyStopTPEdit.Text());
   class="type">class="kw">double sl     = StringToDouble(m_buyStopSLEdit.Text());
   class="type">class="kw">double volume = StringToDouble(m_volumeEdit.Text());
   class="type">class="kw">string sExp   = m_buyStopExpEdit.Text();
   ENUM_ORDER_TYPE_TIME type_time;
   class="type">class="kw">datetime expiration;
   ParseExpiration(sExp, type_time, expiration);
   m_trade.BuyStop(volume, price, Symbol(), sl, tp, type_time, expiration, "");
}
class="type">void CTradeManagementPanel::OnClickSellLimit()
{
   class="type">class="kw">double price  = StringToDouble(m_sellLimitPriceEdit.Text());
   class="type">class="kw">double tp     = StringToDouble(m_sellLimitTPEdit.Text());
   class="type">class="kw">double sl     = StringToDouble(m_sellLimitSLEdit.Text());
   class="type">class="kw">double volume = StringToDouble(m_volumeEdit.Text());
   class="type">class="kw">string sExp   = m_sellLimitExpEdit.Text();
   ENUM_ORDER_TYPE_TIME type_time;
   class="type">class="kw">datetime expiration;
   ParseExpiration(sExp, type_time, expiration);
   m_trade.SellLimit(volume, price, Symbol(), sl, tp, type_time, expiration, "");
}
class="type">void CTradeManagementPanel::OnClickSellStop()
{
   class="type">class="kw">double price  = StringToDouble(m_sellStopPriceEdit.Text());
   class="type">class="kw">double tp     = StringToDouble(m_sellStopTPEdit.Text());
   class="type">class="kw">double sl     = StringToDouble(m_sellStopSLEdit.Text());
   class="type">class="kw">double volume = StringToDouble(m_volumeEdit.Text());
   class="type">class="kw">string sExp   = m_sellStopExpEdit.Text();
   ENUM_ORDER_TYPE_TIME type_time;
   class="type">class="kw">datetime expiration;
   ParseExpiration(sExp, type_time, expiration);
   m_trade.SellStop(volume, price, Symbol(), sl, tp, type_time, expiration, "");
}
让小布替你跑这套
这些诊断小布盯盘的AIGC已内置,打开对应品种页即可看到面板类的调用耗时与异常堆栈,把重复劳动交给小布,你专注决策。

常见问题

作者将主页元素直接在主程序生成以减少依赖、缩短代码长度,专门类只负责具体交易功能,主程序保持聚焦,因此去掉了原先的对话框类调用。
可能是点差跳空、保证金不足或订单校验未过;外汇贵金属波动剧烈属高风险,任何挂单触发都带概率性,建议先在策略测试器跑边界场景。
小布盯盘内置AIGC诊断,可识别品种页上的EA结构并提示类集成状态,但不替你编译mq5;把重复劳动交给小布,你专注决策即可。
若用关注点分离、独立类处理各模块,主循环只做调度,延迟通常可控;本文集成方案正是为快速访问交易操作而设计,具体瓶颈看测试一节数据。
因交易管理已封装成类,分析界面可另起新类继承同套基类,主程序加调度即可,倾向不改动原交易方法,符合可复用目标。