MQL5交易管理面板开发(第十二部分):汇率计算器的集成·综合运用
🧮

MQL5交易管理面板开发(第十二部分):汇率计算器的集成·综合运用

(3/3)·从挂单布局改造到EA适配更新,13节拆解原生汇率计算器的全链路集成

实战向进阶 第 3/3 篇

切来切去在外部网站算点值、保证金和掉期,是多数手动交易者的日常。窗口一多就容易填错参数,风控动作反而被工具拖慢。把计算收进终端内的面板,不是为了炫技,是少一步切换就少一次出错。

控件挂载与结果刷新的内部链路

面板在 Create 里把静态控件(选项标签、下拉框、计算按钮、结果标签与结果框)全部建好后,由父 EA 或对话框调用 AddToDialog,把这些对象塞进内部控件列表;随后遍历 m_inputs[] 动态数组把每对标签—编辑框一并挂上。任意一次 Add 失败就返回 false,调用方据此知道计算器没完全接进界面。 UpdateResult 按当前下拉项去抓输入:仓位规模要 accountBalance、riskPercent、stopLossPips、symbol 四个值,有效才跑 CalculatePositionSize 并后缀“仓位规模(手数)”;风险回报比只取 takeProfitPips 和 stopLossPips 两个值。任意输入无效或品种取不到时,txt 直接写成“结果:输入无效”,但最终数值仍用 res 两位小数写进 m_resultField.Text,描述行由 m_resultLabel.Text 同步。 GetInputValue 靠 Name() 拼 m_name+"Inp_字段名" 在 m_inputs 里找编辑框并返回 Text 数值,找不到返 0.0;GetInputString 对 symbol、accountCurrency 这类返原文本,找不到返空串。OnEvent 只接两种:下拉变更走 CreateInputFields 重建动态对并刷新,按钮点击重读术语再算一次,其余事件交回父对话框,避免无谓重绘。 析构时遍历 m_inputs 删掉每对动态控件防泄漏;切换术语时 CreateInputFields 里 ArrayFree 清旧表,旧控件必须显式 delete,否则 EA 退出时面板关掉也会留内存尾。

MQL5 / C++
class="macro">#include <Controls\Dialog.mqh>
class="macro">#include <Controls\ComboBox.mqh>
class="macro">#include <Controls\Edit.mqh>
class="macro">#include <Controls\Label.mqh>
class="macro">#include <Controls\Button.mqh>
class=class="str">"cmt">// Forex Calculator Class
class CForexCalculator {
class="kw">private:
  CLabel      m_calcOptionLabel;   class=class="str">"cmt">// “Calculation Option:” label
  CComboBox   m_dropdown;          class=class="str">"cmt">// Dropdown for selecting calculation term
  CEdit       m_resultField;       class=class="str">"cmt">// Read-only field to display result
  CLabel      m_resultLabel;       class=class="str">"cmt">// Label preceding the result(e.g., “Result:”)
  CButton     m_calculateButton;   class=class="str">"cmt">// “Calculate” button
  CWnd       *m_inputs[];          class=class="str">"cmt">// Dynamically added label+edit pairs
  class="type">long        m_chart_id;          class=class="str">"cmt">// Chart identifier
  class="type">class="kw">string      m_name;              class=class="str">"cmt">// Prefix for control names
  class="type">int         m_originX;           class=class="str">"cmt">// X-coordinate origin for dynamic fields
  class="type">int         m_originY;           class=class="str">"cmt">// Y-coordinate origin for dynamic fields
  InputField  m_positionSizeInputs[class="num">4];
  InputField  m_riskAmountInputs[class="num">3];
  InputField  m_pipValueInputs[class="num">3];
  InputField  m_profitLossInputs[class="num">4];
  InputField  m_riskRewardInputs[class="num">2];
  class=class="str">"cmt">// … (other class="kw">private methods follow) …
class="kw">public:
  CForexCalculator();
  class="type">bool 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="kw">const class="type">int x, class="kw">const class="type">int y, class="kw">const class="type">int w, class="kw">const class="type">int h);
  class="type">bool AddToDialog(CAppDialog &dlg);
  class="type">void UpdateResult(class="kw">const class="type">class="kw">string term);
  class="type">class="kw">double GetInputValue(class="kw">const class="type">class="kw">string name);

「把计算器输入框预先登记进结构体」

写 MT5 面板类时,先把各计算模块的输入框名、标签、默认值塞进对应数组,后面画 UI 和取值都直接遍历,不用每换一个品种就手写一遍控件逻辑。下面这段 InitInputs 就是干这个的:它给仓位计算、风险金额、点值、盈亏四类输入分别挂了 name/label/defaultValue。 仓位类用了 4 个字段:accountBalance 标签会拼上 AccountInfoString(ACCOUNT_CURRENCY) 显示账户币种,默认 0 运行时再填;riskPercent 默认 1.0 即按 1% 净值冒险;stopLossPips 默认 20.0;symbol 默认 0 留空待选。风险金额类只要 positionSize 默认 0.1 手、stopLossPips 20.0、symbol 三项是够用的。 点值类登记了 lotSize 0.1、symbol、accountCurrency 三项;盈亏类目前只挂了 entryPrice 取当前 _Symbol 的 SYMBOL_BID 做预填,exitPrice 留空。注意 entryPrice 用 SymbolInfoDouble 实时取价,所以面板一开就带出市价,省得手动敲。 开 MT5 新建 EA 把这段抄进类方法,编译后如果在账户币种显示或预填价上不对,优先查 AccountInfoString 和 SymbolInfoDouble 的返回——这两处是外接环境依赖,最容易因品种权限返回 0。

MQL5 / C++
class="type">void InitInputs()
{
  class=class="str">"cmt">// Position Size inputs
  m_positionSizeInputs[class="num">0].name       = "accountBalance";
  m_positionSizeInputs[class="num">0].label      = "Account Balance(" + AccountInfoString(ACCOUNT_CURRENCY) + ")";
  m_positionSizeInputs[class="num">0].defaultValue = class="num">0.0;  class=class="str">"cmt">// updated at runtime
  m_positionSizeInputs[class="num">1].name       = "riskPercent";
  m_positionSizeInputs[class="num">1].label      = "Risk Percentage(%)";
  m_positionSizeInputs[class="num">1].defaultValue = class="num">1.0;
  m_positionSizeInputs[class="num">2].name       = "stopLossPips";
  m_positionSizeInputs[class="num">2].label      = "Stop Loss(Pips)";
  m_positionSizeInputs[class="num">2].defaultValue = class="num">20.0;
  m_positionSizeInputs[class="num">3].name       = "symbol";
  m_positionSizeInputs[class="num">3].label      = "Symbol";
  m_positionSizeInputs[class="num">3].defaultValue = class="num">0.0;
  class=class="str">"cmt">// Risk Amount inputs
  m_riskAmountInputs[class="num">0].name = "positionSize";
  m_riskAmountInputs[class="num">0].label = "Position Size(Lots)";
  m_riskAmountInputs[class="num">0].defaultValue = class="num">0.1;
  m_riskAmountInputs[class="num">1].name = "stopLossPips";
  m_riskAmountInputs[class="num">1].label = "Stop Loss(Pips)";
  m_riskAmountInputs[class="num">1].defaultValue = class="num">20.0;
  m_riskAmountInputs[class="num">2].name = "symbol";
  m_riskAmountInputs[class="num">2].label = "Symbol";
  m_riskAmountInputs[class="num">2].defaultValue = class="num">0.0;
  class=class="str">"cmt">// Pip Value inputs
  m_pipValueInputs[class="num">0].name = "lotSize";
  m_pipValueInputs[class="num">0].label = "Lot Size";
  m_pipValueInputs[class="num">0].defaultValue = class="num">0.1;
  m_pipValueInputs[class="num">1].name = "symbol";
  m_pipValueInputs[class="num">1].label = "Symbol";
  m_pipValueInputs[class="num">1].defaultValue = class="num">0.0;
  m_pipValueInputs[class="num">2].name = "accountCurrency";
  m_pipValueInputs[class="num">2].label = "Account Currency";
  m_pipValueInputs[class="num">2].defaultValue = class="num">0.0;
  class=class="str">"cmt">// Profit/Loss inputs
  m_profitLossInputs[class="num">0].name = "entryPrice";
  m_profitLossInputs[class="num">0].label = "Entry Price";
  m_profitLossInputs[class="num">0].defaultValue = SymbolInfoDouble(_Symbol, SYMBOL_BID);
  m_profitLossInputs[class="num">1].name = "exitPrice";
  m_profitLossInputs[class="num">1].label = "Exit Price";
}

◍ 把账户余额与pip价值接进仓位公式

盈利亏损输入组里写死了部分默认值:止损触发价取当前卖价加 0.0020,固定手数 0.1,风险回报组默认 TP 40 点、SL 20 点,盈亏比倾向 2:1。这些静态值在面板初始化时够用,但账户余额必须运行时拉真实数据,否则仓位算出来会偏离实际风险。 SetDynamicDefaults 做的事很直接:把持仓规模输入组的第 0 项 defaultValue 覆盖成 AccountInfoDouble(ACCOUNT_BALANCE) 的实时返回值。开 MT5 跑一下,能在面板里看到余额字段不再是 0 或写死数,而是跟随账户浮动。 CalculatePipValue 是整套仓位管理的核心。它先抓 tickSize 与 tickValue,再按品种名是否含 JPY 决定 pipSize 为 0.01 或 0.0001;若账户币种与品种盈利币种不同,就拼出交叉对查 SYMBOL_BID 做汇率换算。最后用 (tickValue/tickSize)*pipSize*lotSize*rate 归一化到 2 位小数,返回每 pip 价值。 CalculatePositionSize 把上面的零件拼起来:用余额×百分比除以(止损点数×单 pip 价值)得出原始手数,再按品种 VOLUME_STEP / MIN / MAX 截断并 NormalizeDouble。外汇与贵金属杠杆高,sl 或 pv 为 0 时函数直接返 0,避免误下单。

MQL5 / C++
  m_profitLossInputs[class="num">1].defaultValue = SymbolInfoDouble(_Symbol, SYMBOL_BID) + class="num">0.0020;
  m_profitLossInputs[class="num">2].name = "lotSize";
  m_profitLossInputs[class="num">2].label = "Lot Size";
  m_profitLossInputs[class="num">2].defaultValue = class="num">0.1;
  m_profitLossInputs[class="num">3].name = "symbol";
  m_profitLossInputs[class="num">3].label = "Symbol";
  m_profitLossInputs[class="num">3].defaultValue = class="num">0.0;
  class=class="str">"cmt">// Risk-to-Reward inputs
  m_riskRewardInputs[class="num">0].name = "takeProfitPips";
  m_riskRewardInputs[class="num">0].label = "Take Profit(Pips)";
  m_riskRewardInputs[class="num">0].defaultValue = class="num">40.0;
  m_riskRewardInputs[class="num">1].name = "stopLossPips";
  m_riskRewardInputs[class="num">1].label = "Stop Loss(Pips)";
  m_riskRewardInputs[class="num">1].defaultValue = class="num">20.0;
}
class="type">void SetDynamicDefaults()
{
  class=class="str">"cmt">// Overwrite the “Account Balance” class="kw">default with the real balance at runtime
  m_positionSizeInputs[class="num">0].defaultValue = AccountInfoDouble(ACCOUNT_BALANCE);
}
class="type">class="kw">double CalculatePipValue(class="kw">const class="type">class="kw">string symbol, class="kw">const class="type">class="kw">double lotSize, class="kw">const class="type">class="kw">string accountCurrency)
{
  class="type">class="kw">double tickSize  = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);
  class="type">class="kw">double tickValue = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE);
  class="type">class="kw">double pipSize   = (StringFind(symbol, "JPY") >= class="num">0) ? class="num">0.01 : class="num">0.0001;
  class="type">class="kw">double rate      = class="num">1.0;
  class="type">class="kw">string profitCcy = SymbolInfoString(symbol, SYMBOL_CURRENCY_PROFIT);
  if(accountCurrency != profitCcy)
  {
      class="type">class="kw">string pair = profitCcy + accountCurrency;
      if(SymbolSelect(pair, true))
          rate = SymbolInfoDouble(pair, SYMBOL_BID);
  }
  if(tickSize == class="num">0.0) class="kw">return class="num">0.0;
  class="kw">return NormalizeDouble((tickValue / tickSize) * pipSize * lotSize * rate, class="num">2);
}
class="type">class="kw">double CalculatePositionSize(class="type">class="kw">double bal, class="type">class="kw">double pct, class="type">class="kw">double sl, class="type">class="kw">string sym)
{
  class="type">class="kw">double pv = CalculatePipValue(sym, class="num">1.0, AccountInfoString(ACCOUNT_CURRENCY));
  if(bal <= class="num">0 || pct <= class="num">0 || sl <= class="num">0 || pv <= class="num">0) class="kw">return class="num">0.0;
  class="type">class="kw">double size = (bal * (pct / class="num">100.0)) / (sl * pv);
  class="type">class="kw">double step = SymbolInfoDouble(sym, SYMBOL_VOLUME_STEP);
  class="type">class="kw">double minL = SymbolInfoDouble(sym, SYMBOL_VOLUME_MIN);
  class="type">class="kw">double maxL = SymbolInfoDouble(sym, SYMBOL_VOLUME_MAX);
  class="type">int dp = (class="type">int)-MathLog10(step);
  class="kw">return NormalizeDouble(MathMax(minL, MathMin(maxL, size)), dp);
}

把仓位风险拆成可算的函数

要在 MT5 面板里实时算每笔外汇或贵金属交易的风险,核心是把计算拆成几个纯函数,而不是在按钮事件里写死逻辑。下面这组函数覆盖了风险金额、盈亏额、盈亏比,以及把输入控件挂到图表上的过程。 CalculateRiskAmount 接收仓位尺寸 ps、止损点数 sl 和品种 sym,先拦掉非正数,再借 CalculatePipValue 拿到该品种每点价值,最后用 ps*sl*pv 并保留两位小数。外汇与贵金属杠杆高,实际风险可能远超本金,参数填错会直接返回 0.0 而不是乱算。 CalculateProfitLoss 里有个细节:用 StringFind(sym,"JPY")>=0 判断直盘还是日元系,pipSz 取 0.01 或 0.0001,再把 (exit-entry)/pipSz 乘点值。你可以把 entry、exit 换成实盘开平仓价,立刻看到该笔浮动盈亏是多少。 CalculateRiskRewardRatio 最简单,tp/sl 归一化到两位小数;若任一为 0 返回 0.0,避免除零。AddField 负责在图表左侧缩进处建标签和编辑框,symbol 与 accountCurrency 会自动填 _Symbol 和账户币种,其余字段按 defaultValue 格式化为 %.2f。 复制这段代码到 EA 的 Calc 类里,挂上 EURUSD 或 XAUUSD 测试,改 sl 从 20 到 50 点,风险金额会线性跳变,验证函数没写错。

MQL5 / C++
class="type">class="kw">double CalculateRiskAmount(class="type">class="kw">double ps, class="type">class="kw">double sl, class="type">class="kw">string sym)
{
   if(ps <= class="num">0 || sl <= class="num">0) class="kw">return class="num">0.0;
   class="type">class="kw">double pv = CalculatePipValue(sym, ps, AccountInfoString(ACCOUNT_CURRENCY));
   class="kw">return NormalizeDouble(ps * sl * pv, class="num">2);
}
class="type">class="kw">double CalculateProfitLoss(class="type">class="kw">double entry, class="type">class="kw">double exit, class="type">class="kw">double lotSize, class="type">class="kw">string sym)
{
   if(entry <= class="num">0 || exit <= class="num">0 || lotSize <= class="num">0) class="kw">return class="num">0.0;
   class="type">class="kw">double pipSz = (StringFind(sym, "JPY") >= class="num">0) ? class="num">0.01 : class="num">0.0001;
   class="type">class="kw">double diff  = (exit - entry) / pipSz;
   class="kw">return NormalizeDouble(diff * CalculatePipValue(sym, lotSize, AccountInfoString(ACCOUNT_CURRENCY)), class="num">2);
}
class="type">class="kw">double CalculateRiskRewardRatio(class="type">class="kw">double tp, class="type">class="kw">double sl)
{
   if(tp <= class="num">0 || sl <= class="num">0) class="kw">return class="num">0.0;
   class="kw">return NormalizeDouble(tp / sl, class="num">2);
}
class="type">bool AddField(class="kw">const InputField &f, class="type">int &y)
{
   class="type">int x0 = m_originX + CALC_INDENT_LEFT;
   class=class="str">"cmt">// Create label
   CLabel *lbl = new CLabel();
   if(!lbl.Create(m_chart_id, m_name + "Lbl_" + f.name, class="num">0,
                  x0, y,
                  x0 + CALC_LABEL_WIDTH, y + CALC_EDIT_HEIGHT))
   {
      class="kw">delete lbl;
      class="kw">return class="kw">false;
   }
   lbl.Text(f.label);
   ArrayResize(m_inputs, ArraySize(m_inputs) + class="num">1);
   m_inputs[ArraySize(m_inputs) - class="num">1] = lbl;
   class=class="str">"cmt">// Create edit
   CEdit *edt = new CEdit();
   if(!edt.Create(m_chart_id, m_name + "Inp_" + f.name, class="num">0,
                  x0 + CALC_EDIT_OFFSET + RESULT_BUTTON_GAP, y,
                  x0 + CALC_EDIT_OFFSET + RESULT_BUTTON_GAP + CALC_EDIT_WIDTH,
                  y + CALC_EDIT_HEIGHT))
   {
      class="kw">delete edt;
      class="kw">return class="kw">false;
   }
   if(f.name == "symbol")
      edt.Text(_Symbol);
   else if(f.name == "accountCurrency")
      edt.Text(AccountInfoString(ACCOUNT_CURRENCY));
   else
      edt.Text(StringFormat("%.2f", f.defaultValue));
   ArrayResize(m_inputs, ArraySize(m_inputs) + class="num">1);
   m_inputs[ArraySize(m_inputs) - class="num">1] = edt;
   y += CALC_EDIT_HEIGHT + CALC_CONTROLS_GAP_Y;
   class="kw">return true;
}
class="type">bool CreateInputFields(class="kw">const class="type">class="kw">string term)
{
   ArrayFree(m_inputs);

「计算器面板的字段装配逻辑」

这段逻辑控制的是 MT5 自定义指标面板里「计算模块」的输入框动态挂载。根据下拉项 term 的不同,依次把对应数组里的编辑框推到纵向坐标 y 上;y 由顶部缩进、编辑框高度和控件间距常量算出,保证不重叠。 Position Size 与 Profit/Loss 各挂 4 个输入项,Risk Amount 与 Pip Value 各 3 个,Risk-to-Reward 仅 2 个——若 term 不匹配任何已知项直接返回 false,说明外部传入了非法计算类型。 AddField 任一调用失败就整体返回 false,意味着面板创建是原子性的:要么全套控件就位返回 true,要么一个都不留。你在改写这类 GUI 时,把循环上限和数组长度对齐,否则会出现下标越界但编译器不报错的坑。 Create 函数里下拉框右侧偏移用了 CALC_LABEL_WIDTH + DROPDOWN_LABEL_GAP,按钮则锚定在面板右下角再向左缩 120 像素、向上提 30 像素。外汇与贵金属 GUI 工具受点差跳空影响,这类面板仅作仓位参考,实际成交风险仍偏高。

MQL5 / C++
class="type">int y = m_originY + CALC_INDENT_TOP + CALC_EDIT_HEIGHT + CALC_CONTROLS_GAP_Y;
if(term == "Position Size")
    for(class="type">int i = class="num">0; i < class="num">4; i++)
        if(!AddField(m_positionSizeInputs[i], y)) class="kw">return class="kw">false;
else if(term == "Risk Amount")
    for(class="type">int i = class="num">0; i < class="num">3; i++)
        if(!AddField(m_riskAmountInputs[i], y)) class="kw">return class="kw">false;
else if(term == "Pip Value")
    for(class="type">int i = class="num">0; i < class="num">3; i++)
        if(!AddField(m_pipValueInputs[i], y)) class="kw">return class="kw">false;
else if(term == "Profit/Loss")
    for(class="type">int i = class="num">0; i < class="num">4; i++)
        if(!AddField(m_profitLossInputs[i], y)) class="kw">return class="kw">false;
else if(term == "Risk-to-Reward")
    for(class="type">int i = class="num">0; i < class="num">2; i++)
        if(!AddField(m_riskRewardInputs[i], y)) class="kw">return class="kw">false;
else
    class="kw">return class="kw">false;
class="kw">return true;
}
class="type">bool 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="kw">const class="type">int x, class="kw">const class="type">int y, class="kw">const class="type">int w, class="kw">const class="type">int h)
{
  m_chart_id = chart;
  m_name     = name + "_Calc_";
  m_originX  = x;
  m_originY  = y;
  class=class="str">"cmt">// class="num">1) “Calculation Option:” label
  if(!m_calcOptionLabel.Create(chart, m_name + "CalcOptLbl", subwin,
                               x, y, x + CALC_LABEL_WIDTH, y + CALC_EDIT_HEIGHT))
    class="kw">return class="kw">false;
  m_calcOptionLabel.Text("Calculation Option:");
  class=class="str">"cmt">// class="num">2) Dropdown immediately to the right
  class="type">int comboX = x + CALC_LABEL_WIDTH + DROPDOWN_LABEL_GAP;
  if(!m_dropdown.Create(chart, m_name + "Dropdown", subwin,
                        comboX, y, comboX + (w - CALC_LABEL_WIDTH - DROPDOWN_LABEL_GAP), y + CALC_EDIT_HEIGHT))
    class="kw">return class="kw">false;
  m_dropdown.AddItem("Position Size");
  m_dropdown.AddItem("Risk Amount");
  m_dropdown.AddItem("Pip Value");
  m_dropdown.AddItem("Profit/Loss");
  m_dropdown.AddItem("Risk-to-Reward");
  m_dropdown.Select(class="num">0);
  class=class="str">"cmt">// class="num">3) “Calculate” button near the bottom of this panel area
  class="type">int btnX = x + w - CALC_BUTTON_WIDTH - class="num">120;
  class="type">int btnY = y + h - CALC_BUTTON_HEIGHT + class="num">30;
  if(!m_calculateButton.Create(chart, m_name + "CalcBtn", subwin,

◍ 把计算器面板挂进对话框并实时算仓位

这段逻辑承接前面界面元素的创建,负责把自定义控件整组塞进 MT5 的 CAppDialog,并按下拉选项实时刷新结果框。AddToDialog 里逐个 Add 失败就直接 return false,顺序错了面板会缺控件,调试时优先查这里。 UpdateResult 是核心:选 Position Size 时,它从输入框取 accountBalance、riskPercent、stopLossPips 和 symbol,四个值都大于 0 且 SymbolSelect 成功才调 CalculatePositionSize。外汇和贵金属杠杆高,SL 点数填错会让算出的手数偏离实际风险好几个数量级。 代码里 RESULT_BUTTON_GAP、CALC_LABEL_WIDTH 这类宏决定了控件间距,btnY-20 把标签上移 20 像素避免和按钮重叠。改这些常量能适配不同 DPI 的图表,不用动布局逻辑。

MQL5 / C++
  m_calculateButton.Text("Calculate");
  m_calculateButton.ColorBackground(clrSteelBlue);
  m_calculateButton.Color(clrWhite);
  class=class="str">"cmt">// class="num">4) Result label and read-only field to the right of the button
  class="type">int blockX = btnX + CALC_BUTTON_WIDTH + RESULT_BUTTON_GAP;
  class="type">int lblY = btnY - class="num">20;
  if(!m_resultLabel.Create(chart, m_name + "ResultLbl", subwin,
                          blockX, lblY, blockX + CALC_LABEL_WIDTH, lblY + CALC_EDIT_HEIGHT))
    class="kw">return class="kw">false;
  m_resultLabel.Text("Result:");
  class="type">int fldY = lblY + CALC_EDIT_HEIGHT + RESULT_VERTICAL_GAP;
  if(!m_resultField.Create(chart, m_name + "ResultFld", subwin,
                          blockX, fldY, blockX + CALC_EDIT_WIDTH, fldY + CALC_EDIT_HEIGHT))
    class="kw">return class="kw">false;
  m_resultField.ReadOnly(true);
  class=class="str">"cmt">// class="num">5) Populate dynamic defaults and input rows
  SetDynamicDefaults();
  class="type">class="kw">string initialTerm = m_dropdown.Select();
  CreateInputFields(initialTerm);
  UpdateResult(initialTerm);
  class="kw">return true;
}
class="type">bool AddToDialog(CAppDialog &dlg)
{
  if(!dlg.Add(&m_calcOptionLabel)) class="kw">return class="kw">false;
  if(!dlg.Add(&m_dropdown))        class="kw">return class="kw">false;
  if(!dlg.Add(&m_calculateButton)) class="kw">return class="kw">false;
  if(!dlg.Add(&m_resultLabel))     class="kw">return class="kw">false;
  if(!dlg.Add(&m_resultField))     class="kw">return class="kw">false;
  for(class="type">int i = class="num">0; i < ArraySize(m_inputs); i++)
    if(!dlg.Add(m_inputs[i])) class="kw">return class="kw">false;
  class="kw">return true;
}
class="type">void UpdateResult(class="kw">const class="type">class="kw">string term)
{
  class="type">class="kw">double res = class="num">0.0;
  class="type">class="kw">string txt = "Result: ";
  if(term == "Position Size")
  {
    class="type">class="kw">double bal = GetInputValue("accountBalance");
    class="type">class="kw">double pct = GetInputValue("riskPercent");
    class="type">class="kw">double sl  = GetInputValue("stopLossPips");
    class="type">class="kw">string sym = GetInputString("symbol");
    if(bal > class="num">0 && pct > class="num">0 && sl > class="num">0 && SymbolSelect(sym, true))
    {
      res = CalculatePositionSize(bal, pct, sl, sym);
      txt += "Position Size(lots)";
    }
    else txt += "Invalid Input";
  }
  else if(term == "Risk Amount")
  {
    class="type">class="kw">double ps  = GetInputValue("positionSize");
    class="type">class="kw">double slp = GetInputValue("stopLossPips");

四类风控计算的分支实现

这段逻辑把交易前的核心风控指标拆成四个独立分支:风险金额、点值、盈亏额、风险回报比。每个分支先抓输入框的值,再做合法性校验,校验不过就回写「Invalid Input」,避免脏数据进计算函数。 风险金额分支要求仓位手数和止损点数都大于 0,且 SymbolSelect 成功把品种加到市场观察列表;点值分支只校验手数大于 0 和品种可见性,账户币种单独由字符串参数传入。 盈亏分支最严:进场价、出场价、手数三者均大于 0 且品种可选才放行,否则直接报错。风险回报比不依赖品种,只判断止盈止损点数大于 0 即可算出比值。 GetInputValue 用 m_name + "Inp_" + name 拼出控件名,遍历 m_inputs 数组匹配后把文本转 double;若没匹配到返回 0.0,这也是上层分支靠「>0」拦截无效输入的基础。开 MT5 把这几个函数挂到面板按钮事件里,就能在盯盘时实时算仓位风险。外汇与贵金属杠杆高,计算结果仅作概率参考,实际爆仓风险随波动放大。

MQL5 / C++
class="type">class="kw">string sym = GetInputString("symbol");
if(ps > class="num">0 && slp > class="num">0 && SymbolSelect(sym, true))
{
   res = CalculateRiskAmount(ps, slp, sym);
   txt += "Risk Amount(" + AccountInfoString(ACCOUNT_CURRENCY) + ")";
}
else txt += "Invalid Input";
 }
 else if(term == "Pip Value")
 {
   class="type">class="kw">double ls  = GetInputValue("lotSize");
   class="type">class="kw">string sym = GetInputString("symbol");
   class="type">class="kw">string cur = GetInputString("accountCurrency");
   if(ls > class="num">0 && SymbolSelect(sym, true))
   {
      res = CalculatePipValue(sym, ls, cur);
      txt += "Pip Value(" + cur + ")";
   }
   else txt += "Invalid Input";
 }
 else if(term == "Profit/Loss")
 {
   class="type">class="kw">double e   = GetInputValue("entryPrice");
   class="type">class="kw">double x   = GetInputValue("exitPrice");
   class="type">class="kw">double ls  = GetInputValue("lotSize");
   class="type">class="kw">string sym = GetInputString("symbol");
   if(e > class="num">0 && x > class="num">0 && ls > class="num">0 && SymbolSelect(sym, true))
   {
      res = CalculateProfitLoss(e, x, ls, sym);
      txt += "Profit/Loss(" + AccountInfoString(ACCOUNT_CURRENCY) + ")";
   }
   else txt += "Invalid Input";
 }
 else if(term == "Risk-to-Reward")
 {
   class="type">class="kw">double tp  = GetInputValue("takeProfitPips");
   class="type">class="kw">double slp = GetInputValue("stopLossPips");
   if(tp > class="num">0 && slp > class="num">0)
   {
      res = CalculateRiskRewardRatio(tp, slp);
      txt += "Risk-to-Reward Ratio";
   }
   else txt += "Invalid Input";
 }
 m_resultField.Text(StringFormat("%.2f", res));
 m_resultLabel.Text(txt);
}
class="type">class="kw">double GetInputValue(class="kw">const class="type">class="kw">string name)
{
   for(class="type">int i = class="num">0; i < ArraySize(m_inputs); i++)
      if(m_inputs[i].Name() == m_name + "Inp_" + name)
         class="kw">return StringToDouble(((CEdit*)m_inputs[i]).Text());
   class="kw">return class="num">0.0;
}
class="type">class="kw">string GetInputString(class="kw">const class="type">class="kw">string &name)
{
   for(class="type">int i = class="num">0; i < ArraySize(m_inputs); i++)

「下拉切换与按钮点击的事件分流」

这段逻辑负责把图表上的用户操作翻译成计算器的内部动作。下拉框变动时,先读出选中项索引,再取对应周期文本,随后重建输入控件并刷新结果;点「计算」按钮则跳过重建,只按当前周期重算。 事件函数靠 CHARTEVENT_OBJECT_CHANGE 和 CHARTEVENT_OBJECT_CLICK 两个常量区分来源,sparam 拼上控件名前缀 m_name 做精确匹配,避免和其他 EA 的图表对象打架。 析构里用 ArraySize(m_inputs) 循环 delete,防止 CEdit 指针数组漏释放。你在 MT5 里挂多个同类面板时,若发现切换下拉卡顿,优先查 CreateInputFields 是否重复建了同名字对象。

MQL5 / C++
if(m_inputs[i].Name() == m_name + "Inp_" + name)
     class="kw">return ((CEdit*)m_inputs[i]).Text();
  class="kw">return "";
}
class="type">bool OnEvent(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)
{
  if(id == CHARTEVENT_OBJECT_CHANGE && sparam == m_name + "Dropdown")
  {
    class="type">long idx = m_dropdown.Value();
    class="type">class="kw">string term = GetSelectedTerm();
    CreateInputFields(term);
    UpdateResult(term);
    class="kw">return true;
  }
  if(id == CHARTEVENT_OBJECT_CLICK && sparam == m_name + "CalcBtn")
  {
    class="type">class="kw">string term = GetSelectedTerm();
    UpdateResult(term);
    class="kw">return true;
  }
  class="kw">return class="kw">false;
}
~CForexCalculator()
{
  for(class="type">int i = class="num">0; i < ArraySize(m_inputs); i++)
    class="kw">delete m_inputs[i];
}

◍ 把汇率计算器塞进交易面板的正确姿势

在 CTradeManagementPanel 里挂一个 CForexCalculator,做法不是复制控件也不是改计算器源码,而是在面板类的保护成员里直接声明 m_calculator 实例。面板头文件只要包含了 ForexValuesCalculator.mqh,编译器就能识别类布局,后续把它当普通控件来构建、定位、加进对话框并转发事件即可,私有成员完全不用碰。 Create() 里的顺序是:先画「Forex Values Calculator:」标题标签(高度 DEFAULT_LABEL_HEIGHT,字体 Arial Bold 10号),curY 下移标签高加 GAP;再用 name+"_ForexCalc" 当唯一前缀,传入图表、子窗口和 (curX, curY) 以及 CALCULATOR_WIDTH / CALCULATOR_HEIGHT 调 m_calculator.Create()。返回 true 后立刻 AddToDialog(this),这一步把下拉菜单、动态标签-编辑框对、「计算」按钮、结果区全部注册进父对话框,z-order 才正确。 布局间距靠常量卡死:构建完把 curY 加 CALCULATOR_HEIGHT + GAP*2,后面分隔线和挂单区就从计算器底下精确起排,不会叠控件。因为块高固定,计算器内部 m_inputs[] 动态增减(比如以后加隔夜利息字段)只在保留块内下移结果区,面板布局一行都不用改。 事件侧,OnEvent 开头就把消息丢给 m_calculator.OnEvent()。若 sparam 匹配到 "MyPanel_ForexCalcDropdown" 或 "MyPanel_ForexCalcCalcBtn" 这类子控件名,计算器返回 true,面板直接 return,优先级最高;只有返回 false 才继续处理快速订单或挂单的点击。相当于计算器在面板里圈了一块自管自的子对话框。外汇与贵金属插件开发涉及实盘逻辑,复盘与模拟测试不充分可能引发界面阻塞或误触发,属高风险操作。

MQL5 / C++
CForexCalculator m_calculator;
class="macro">#include <ForexValuesCalculator.mqh>
if(!CreateLabelEx(m_secCalcLabel, curX, curY, DEFAULT_LABEL_HEIGHT, 
                  "SecCalc", "Forex Values Calculator:", clrNavy))
   class="kw">return(class="kw">false)
m_secCalcLabel.Font("Arial Bold");
m_secCalcLabel.FontSize(class="num">10);
curY += DEFAULT_LABEL_HEIGHT + GAP;
class="type">class="kw">string calcName = name + "_ForexCalc";
if(!m_calculator.Create(chart, calcName, subwin, 
                        curX, curY, CALCULATOR_WIDTH, CALCULATOR_HEIGHT))
   class="kw">return(class="kw">false);
if(!m_calculator.AddToDialog(this))
   class="kw">return(class="kw">false);
curY += CALCULATOR_HEIGHT + GAP * class="num">2;
if(m_calculator.OnEvent(id, lparam, dparam, sparam))
{
   Print("Calculator handled event: ", sparam);
   class="kw">return(true);
}

Run()与ChartRedraw()是面板活起来的两条命门

在EA初始化或面板显示流程里调用 g_tradePanel.Run() ,是把静态控件变成可交互界面的关键一步。Run() 会把控制权交给 CAppDialog 基类的事件循环,持续监听鼠标、键盘和子控件的图表事件;不调用它,CTradeManagementPanel 实例只停在内存里,不会被 MQL5 运行时注册为活动对话框。 具体现象很直接:从“挂单类型”下拉框选项、或用 CDatePicker 改到期日时,面板收不到 CHARTEVENT_OBJECT_CHANGE 或 CHARTEVENT_OBJECT_ENDEDIT,操作全部失效。一旦 Run() 生效,每次点击都会触发 OnEvent(...) 并分发给 OnChangePendingOrderType() 或 OnChangePendingDatePicker(),下拉框卡在初始值、日期选择器永不触发更新这类问题才消失。 ChartRedraw() 决定界面会不会“卡顿零点几秒”。每次对对话框或单个控件调用 Show()/Hide() 后,底层画布必须重绘,否则旧控件可能赖在屏幕上,直到下一笔价格跳动才刷新。在 HandleTradeManagement()、ToggleInterface() 及 OnEvent(...) 处理完事件后频繁调用它,能保证下拉列表即时展开、日历准确弹出、计算器结果无延迟显示。 可见性变更后重绘:HandleTradeManagement() 里 Show()/Hide() 紧跟 ChartRedraw(),面板瞬间显隐,不依赖外部活动刷新。事件委托后重绘:OnChartEvent(...) 把事件转给 g_tradePanel.OnEvent(...) 后再调一次,用户改计算器下拉项时新输入框和标签立即渲染,避免闪烁或半截 UI。 外汇与贵金属交易界面涉及真实资金,GUI 失灵可能让你错失挂单时机,属高风险环节。开 MT5 把下面代码段塞进 EA 试一下,ToggleInterface 里漏了 ChartRedraw() 你就能肉眼看到按钮位移延迟。

MQL5 / C++
class="type">void HandleTradeManagement()
{
   if(g_tradePanel)
   {
      if(g_tradePanel.IsVisible())
         g_tradePanel.Hide();
      else
         g_tradePanel.Show();
      ChartRedraw();
      class="kw">return;
   }
   g_tradePanel = new CTradeManagementPanel();
   if(!g_tradePanel.Create(g_chart_id, "TradeManagementPanel", g_subwin, class="num">310, class="num">20, class="num">875, class="num">700))
   {
      class="kw">delete g_tradePanel;
      g_tradePanel = NULL;
      class="kw">return;
   }
   class=class="str">"cmt">// ← This line activates the dialog’s own message loop
   g_tradePanel.Run();
   g_tradePanel.Show();
   ChartRedraw();
}
class=class="str">"cmt">// Toggling the main interface buttons
class="type">void ToggleInterface()
{
   class="type">bool state = ObjectGetInteger(class="num">0, toggleButtonName, OBJPROP_STATE);
   ObjectSetInteger(class="num">0, toggleButtonName, OBJPROP_STATE, !state);
   UpdateButtonVisibility(!state);
   class=class="str">"cmt">// Redraw immediately so button positions update on screen
   ChartRedraw();
}
class=class="str">"cmt">// In the OnEvent handler, after forwarding to sub‐panels:
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)
{
   if(id == CHARTEVENT_OBJECT_CLICK)
   {
      class=class="str">"cmt">// ... handle panel toggles ...
      ChartRedraw();  class=class="str">"cmt">// Ensure any Show()/Hide() calls are rendered
      class=class="str">"cmt">// Forward to communication panel
      if(g_commPanel && g_commPanel.IsVisible())
         g_commPanel.OnEvent(id, lparam, dparam, sparam);
      ChartRedraw();  class=class="str">"cmt">// Redraw after commPanel’s changes
      class=class="str">"cmt">// Forward to trade panel
      if(g_tradePanel && g_tradePanel.IsVisible())
         g_tradePanel.OnEvent(id, lparam, dparam, sparam);

「事件分发后的两次重绘时机」

在自定义面板的事件处理函数末尾,连续两次调用 ChartRedraw() 并不是冗余写法。第一次重绘发生在交易面板自身状态更新之后(如下拉框切换、日期变更),用于让控件改动立即上屏;紧接着若分析面板可见,则把事件前向转发给它,再补一次最终重绘把分析层的改动同步出来。

MQL5 / C++
ChartRedraw();   class=class="str">"cmt">// Redraw after tradePanel’s updates(e.g., combobox or date change)
class=class="str">"cmt">// Forward to analytics panel
if(g_analyticsPanel && g_analyticsPanel.IsVisible())
   g_analyticsPanel.OnEvent(id, lparam, dparam, sparam);
ChartRedraw();   class=class="str">"cmt">// Final redraw to reflect any analytics updates
逐行拆解:第1行在交易面板改完界面后强制重绘,避免用户看到旧控件状态;第3–4行判断分析面板指针非空且可见时才转发事件,防止空指针或隐藏态误触发;第5行是收尾重绘,保证分析面板写回的图形、文本不滞留后台缓冲。 在 MT5 里若漏掉第二次 ChartRedraw(),常见现象是切换品种后分析线延迟一两帧才出现。外汇与贵金属图表高频刷新,这类重绘时序错乱会放大视觉误差,实盘前应在策略测试器里反复切周期验证。

MQL5 / C++
ChartRedraw();   class=class="str">"cmt">// Redraw after tradePanel’s updates(e.g., combobox or date change)
class=class="str">"cmt">// Forward to analytics panel
if(g_analyticsPanel && g_analyticsPanel.IsVisible())
   g_analyticsPanel.OnEvent(id, lparam, dparam, sparam);
ChartRedraw();   class=class="str">"cmt">// Final redraw to reflect any analytics updates

◍ 面板里的汇率计算器怎么测

把改写后的 TradeManagementPanel 在 MetaTrader 5 里编译跑通后,重点验两件事:挂单流程有没有比旧版少点两次鼠标,以及内置汇率计算器算出来的交叉汇率和 MT5 报价窗口差多少。 测试时建议拿 EURUSD 与 USDJPY 交叉出 EURJPY,对比面板数值与手动乘除结果,偏差超过 0.5 点就说明换算逻辑有问题。外汇与贵金属杠杆高,计算器只降认知负荷,不替你管风险。 直接开 MT5 加载面板,输一组非美直盘让计算器跑一遍,比看文档快。

一点提醒

把头文件丢进 MQL5\include、Images.zip 解压到 MQL5\Images,再编译 New_Admin_Panel.mq5(17.98 KB)跑起来,这套多面板工具就能在 MT5 里实测。ForexValuesCalculator.mqh 算点值、保证金和仓位规模,TradeManagementPanel.mqh 管下单与风险计算,两者配合可复用作自己的 EA 模块。 外汇与贵金属波动剧烈、杠杆风险高,面板里的数值只是辅助参考,实盘前务必在策略测试器里验证计算逻辑与事件响应。开发者的计算引擎仍有优化空间,跑通后改参数比从头写更快。

让小布替你跑这套
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到点值、保证金与风险回报比的实时估算,把重复劳动交给小布,你专注决策。

常见问题

原生算法不依赖外部服务可用性,也不把账户相关参数传出终端,在断网或API限流时仍能算,适合作为基础风控模块。
采用模块化容器分离输入区与计算区,仅压缩原输入区宽度并挂载子面板,事件路由独立,原有挂单回调不受影响。
小布盯盘已内置品种页的实时点值、保证金与风险回报估算,可作为轻量替代;但面板内的原生计算器适合需要自定义杠杆与多品种联算的场景。
掉期与过点成本在不同经纪商符号属性差异大,测试时需用真实符号属性,且历史点差采样要覆盖新闻时段,否则预估盈亏会偏乐观。
只能作概率参考,经纪商强平线随净值波动实时变动,外汇贵金属杠杆高,极端滑点下实际平仓可能早于预估。