MQL5交易管理面板开发(第十二部分):汇率计算器的集成·综合运用
(3/3)·从挂单布局改造到EA适配更新,13节拆解原生汇率计算器的全链路集成
切来切去在外部网站算点值、保证金和掉期,是多数手动交易者的日常。窗口一多就容易填错参数,风控动作反而被工具拖慢。把计算收进终端内的面板,不是为了炫技,是少一步切换就少一次出错。
控件挂载与结果刷新的内部链路
面板在 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 退出时面板关掉也会留内存尾。
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。
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,避免误下单。
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 点,风险金额会线性跳变,验证函数没写错。
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 工具受点差跳空影响,这类面板仅作仓位参考,实际成交风险仍偏高。
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 的图表,不用动布局逻辑。
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 把这几个函数挂到面板按钮事件里,就能在盯盘时实时算仓位风险。外汇与贵金属杠杆高,计算结果仅作概率参考,实际爆仓风险随波动放大。
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 是否重复建了同名字对象。
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 才继续处理快速订单或挂单的点击。相当于计算器在面板里圈了一块自管自的子对话框。外汇与贵金属插件开发涉及实盘逻辑,复盘与模拟测试不充分可能引发界面阻塞或误触发,属高风险操作。
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() 你就能肉眼看到按钮位移延迟。
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() 并不是冗余写法。第一次重绘发生在交易面板自身状态更新之后(如下拉框切换、日期变更),用于让控件改动立即上屏;紧接着若分析面板可见,则把事件前向转发给它,再补一次最终重绘把分析层的改动同步出来。
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
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 模块。 外汇与贵金属波动剧烈、杠杆风险高,面板里的数值只是辅助参考,实盘前务必在策略测试器里验证计算逻辑与事件响应。开发者的计算引擎仍有优化空间,跑通后改参数比从头写更快。