MQL5交易管理面板开发(第十二部分):汇率计算器的集成·进阶篇
(2/3)· 摆脱外部网站切换,把持仓规模、点值、保证金等12类参数算进交易面板里
不少交易者习惯切到浏览器算风险回报比和保证金水平,算完再回终端下单,中间一步手误就偏了仓位。面板里直接内嵌算法,不是替外部工具唱挽歌,而是把决策链路压到同一屏。本篇接上篇模块化改造,继续把原生汇率计算器塞进新管理面板。
◍ 把开发拆成四步再动手
做这个面板改造,别一口气重写。先按四个阶段推进:挪挂单模块布局给新控件留位、写汇率计算器控件类、把它接进交易管理面板、再改 NewAdminPanel EA 适配。 每一步都建议在 MT5 里单独建分支验证,尤其 ComboBox 和 DatePicker 这两类组件,事件绑定漏一处就会导致整面板无响应。 四步跑完后再统一更新 NewAdminPanel EA 跑联调。外汇与贵金属插件改动属高风险操作,上线前务必在模拟账户跑满一轮。
把挂单模块从面板头部抽出来单搞
做 MT5 面板时,把挂单区从交易管理面板头部剥离,能腾出位置塞 ComboBox 和 DatePicker。原设计里头部挤了太多控件,现在集中成 6 个控件 + 5 个标签的独立块,后续改挂单逻辑不用动其他区域。 Create(...) 里先留 10 像素垂直间距,再画加粗的“Pending Orders:”标题,右边接订单类型下拉框。列标题 Price / TP / SL / Expiration 水平等距排,下方输入框默认填当前 ask 价、TP 和 SL 初值 0.00000、日期默认当日 23:59:59。这样用户配完类型、价格、止盈止损、到期时间,点“下单”就能发请求。 事件处理上,ComboBox 一切换买卖类型,就抓对应 ask 或 bid 填进价格框并重绘,避免限价单挂错边。日期变更只记日志加重绘,不卡非法日历日。发单前验证三条:手数 >0、价格 >0、买入限价须严格低于卖价、买入止损须严格高于卖价,任一不过就终止。 OnEvent 路由先丢给计算器,命中挂单按钮或下拉/日期变更才进专属处理器,其余回退基类。外汇与贵金属挂单受滑点和高波动影响,验证通过的单也可能因价格瞬间击穿而拒绝,实盘前务必在策略测试器走一遍。
class=class="str">"cmt">// Pending Orders CLabel m_secPendingLabel; class=class="str">"cmt">// “Pending Orders:” header CLabel m_pendingPriceHeader; class=class="str">"cmt">// “Price:” column header CLabel m_pendingTPHeader; class=class="str">"cmt">// “TP:” column header CLabel m_pendingSLHeader; class=class="str">"cmt">// “SL:” column header CLabel m_pendingExpHeader; class=class="str">"cmt">// “Expiration:” column header CComboBox m_pendingOrderType; class=class="str">"cmt">// Combobox for “Buy Limit / Buy Stop / Sell Limit / Sell Stop” CEdit m_pendingPriceEdit; class=class="str">"cmt">// Edit box for pending‐order price CEdit m_pendingTPEdit; class=class="str">"cmt">// Edit box for pending‐order take‐profit CEdit m_pendingSLEdit; class=class="str">"cmt">// Edit box for pending‐order stop‐loss CDatePicker m_pendingDatePicker; class=class="str">"cmt">// DatePicker for expiration date CButton m_placePendingButton; class=class="str">"cmt">// “Place Order” button for pending orders class=class="str">"cmt">// In CTradeManagementPanel::Create(...), after Section separator: class=class="str">"cmt">// 10px vertical offset before “Section class="num">3” header curY += class="num">10; if(!CreateLabelEx(m_secPendingLabel, curX, curY, DEFAULT_LABEL_HEIGHT, "SecPend", "Pending Orders:", clrNavy)) class="kw">return(class="kw">false); m_secPendingLabel.Font("Arial Bold"); m_secPendingLabel.FontSize(class="num">10); class=class="str">"cmt">// Create the Combobox for order types if(!CreateComboBox(m_pendingOrderType, "PendingOrderType", curX + SECTION_LABEL_WIDTH + GAP, curY, DROPDOWN_WIDTH, EDIT_HEIGHT)) class="kw">return(class="kw">false); curY += EDIT_HEIGHT + GAP; class=class="str">"cmt">// Column headers: Price, TP, SL, Expiration class="type">int headerX = curX; 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,
「挂单面板的输入行与联动逻辑」
这段代码片段在 MT5 自定义面板里搭出了挂单输入区:四列标签(价格 / TP / SL / 过期时间)之后,跟着对应的编辑框和日期选择器。标签通过 CreateLabelEx 横向排开,列间距由 EDIT_WIDTH + GAP 控制,GAP 通常为 2~5 像素级偏移,避免控件重叠。 价格编辑框默认填当前 ASK,精度写死 DoubleToString(ask, 5) 即 5 位小数;TP 与 SL 编辑框初始为 "0.00000",代表不预设止损止盈。日期选择器默认值是当天 23:59:59(endOfDay = now - now%86400 + 86399),也就是说新挂单默认当日有效,做日内挂单的人得手动改。 OnChangePendingOrderType 是订单类型下拉的回调:选 Buy Limit / Buy Stop 时价格框抓 ASK,选卖单类则抓 BID,立刻重写编辑框并 ChartRedraw。这里有个细节——限价单和止损单都用了同一个 ASK 做初始价,实盘里你可能要按距离现价几个点再偏移,否则挂太近会被经纪商拒单。外汇与贵金属杠杆高,挂单未成交也可能因跳空直接触发,属正常高风险现象。 OnChangePendingDatePicker 只做打印,把选中的日期时间用 TIME_DATE|TIME_MINUTES 格式输出到日志,方便你确认控件返回值没错。复制下面代码到自己的面板类里,改 GAP 和 EDIT_WIDTH 就能直接看排版效果。
if(!CreateLabelEx(m_pendingTPHeader, headerX + class="num">2 * (EDIT_WIDTH + GAP), curY, DEFAULT_LABEL_HEIGHT, "PendTP", "TP:", clrBlack)) class="kw">return(class="kw">false); if(!CreateLabelEx(m_pendingSLHeader, headerX + class="num">3 * (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:", clrBlack)) class="kw">return(class="kw">false); curY += DEFAULT_LABEL_HEIGHT + GAP; class=class="str">"cmt">// Pending orders inputs row: class=class="str">"cmt">// • Pending Price class="type">int inputX = curX; if(!CreateEdit(m_pendingPriceEdit, "PendingPrice", inputX, curY, EDIT_WIDTH, EDIT_HEIGHT)) class="kw">return(class="kw">false); class="type">class="kw">double ask = SymbolInfoDouble(Symbol(), SYMBOL_ASK); m_pendingPriceEdit.Text(DoubleToString(ask, class="num">5)); class=class="str">"cmt">// • Pending TP class="type">int input2X = inputX + EDIT_WIDTH + GAP; if(!CreateEdit(m_pendingTPEdit, "PendingTP", input2X, curY, EDIT_WIDTH, EDIT_HEIGHT)) class="kw">return(class="kw">false); m_pendingTPEdit.Text("class="num">0.00000"); class=class="str">"cmt">// • Pending SL class="type">int input3X = input2X + EDIT_WIDTH + GAP; if(!CreateEdit(m_pendingSLEdit, "PendingSL", input3X, curY, EDIT_WIDTH, EDIT_HEIGHT)) class="kw">return(class="kw">false); m_pendingSLEdit.Text("class="num">0.00000"); class=class="str">"cmt">// • Pending Expiration(DatePicker) class="type">int input4X = input3X + EDIT_WIDTH + GAP; if(!CreateDatePicker(m_pendingDatePicker, "PendingExp", input4X, curY, DATEPICKER_WIDTH + class="num">20, EDIT_HEIGHT)) class="kw">return(class="kw">false); class="type">class="kw">datetime now = TimeCurrent(); class="type">class="kw">datetime endOfDay = now - (now % class="num">86400) + class="num">86399; m_pendingDatePicker.Value(endOfDay); class=class="str">"cmt">// • Place Order button class="type">int buttonX = input4X + DATEPICKER_WIDTH + GAP; if(!CreateButton(m_placePendingButton, "Place Order", buttonX + class="num">20, curY, BUTTON_WIDTH, BUTTON_HEIGHT, clrBlue)) class="kw">return(class="kw">false); curY += BUTTON_HEIGHT + GAP * class="num">2; class="type">void CTradeManagementPanel::OnChangePendingOrderType() { class="type">class="kw">string selected = m_pendingOrderType.Select(); class="type">int index = (class="type">int)m_pendingOrderType.Value(); Print("OnChangePendingOrderType: Selected=&class="macro">#x27;", selected, "&class="macro">#x27;, Index=", index); class="type">class="kw">double price = class="num">0.0; if(selected == "Buy Limit" || selected == "Buy Stop") price = SymbolInfoDouble(Symbol(), SYMBOL_ASK); else price = SymbolInfoDouble(Symbol(), SYMBOL_BID); m_pendingPriceEdit.Text(DoubleToString(price, class="num">5)); ChartRedraw(); } class="type">void CTradeManagementPanel::OnChangePendingDatePicker() { class="type">class="kw">datetime selected = m_pendingDatePicker.Value(); Print("OnChangePendingDatePicker: Selected=&class="macro">#x27;", TimeToString(selected, TIME_DATE|TIME_MINUTES), "&class="macro">#x27;");
◍ 挂单参数校验与下单落点
在 MT5 面板里点下挂单按钮后,真正决定订单能否存活的是 ValidatePendingParameters 这套前置过滤。它先把 volume 和 price 卡死在大于 0,否则直接 Print 报错并返回 false,连网络请求都不会发出去。 紧接着用 SymbolInfoDouble 抓实时 SYMBOL_ASK 与 SYMBOL_BID,按订单类型做价格位置核对:Buy Limit 必须低于 Ask,Buy Stop 必须高于 Ask;Sell Limit 必须高于 Bid,Sell Stop 必须低于 Bid。任何一条不满足就拦截,这能避免新手把限价单挂成市价单方向的尴尬。 OnClickPlacePending 里先从各 Edit 控件取文本转 double,expiry 为 0 时退化成 ORDER_TIME_GTC,否则走 ORDER_TIME_SPECIFIED。校验通过后按字符串分支调 m_trade.BuyLimit / BuyStop / SellLimit / SellStop,外汇与贵金属挂单受点差和熔断影响,滑点可能导致实际挂单价格偏离输入值,建议先在模拟盘验证。
class="type">bool CTradeManagementPanel::ValidatePendingParameters(class="type">class="kw">double volume, class="type">class="kw">double price, class="type">class="kw">string orderType) { if(volume <= class="num">0) { Print("Invalid volume for pending order"); class="kw">return(class="kw">false); } if(price <= class="num">0) { Print("Invalid price for pending order"); class="kw">return(class="kw">false); } class="type">class="kw">double ask = SymbolInfoDouble(Symbol(), SYMBOL_ASK); class="type">class="kw">double bid = SymbolInfoDouble(Symbol(), SYMBOL_BID); if(orderType == "Buy Limit" && price >= ask) { Print("Buy Limit price must be below Ask"); class="kw">return(class="kw">false); } if(orderType == "Buy Stop" && price <= ask) { Print("Buy Stop price must be above Ask"); class="kw">return(class="kw">false); } if(orderType == "Sell Limit" && price <= bid) { Print("Sell Limit price must be above Bid"); class="kw">return(class="kw">false); } if(orderType == "Sell Stop" && price >= bid) { Print("Sell Stop price must be below Bid"); class="kw">return(class="kw">false); } class="kw">return(true); } class="type">void CTradeManagementPanel::OnClickPlacePending() { Print("OnClickPlacePending called"); class="type">class="kw">string orderType = m_pendingOrderType.Select(); class="type">class="kw">double price = StringToDouble(m_pendingPriceEdit.Text()); class="type">class="kw">double tp = StringToDouble(m_pendingTPEdit.Text()); class="type">class="kw">double sl = StringToDouble(m_pendingSLEdit.Text()); class="type">class="kw">double volume = StringToDouble(m_volumeEdit.Text()); class="type">class="kw">datetime expiry = m_pendingDatePicker.Value(); ENUM_ORDER_TYPE_TIME type_time = (expiry == class="num">0) ? ORDER_TIME_GTC : ORDER_TIME_SPECIFIED; if(!ValidatePendingParameters(volume, price, orderType)) class="kw">return; if(orderType == "Buy Limit") m_trade.BuyLimit(volume, price, Symbol(), sl, tp, type_time, expiry, ""); else if(orderType == "Buy Stop") m_trade.BuyStop(volume, price, Symbol(), sl, tp, type_time, expiry, ""); else if(orderType == "Sell Limit") m_trade.SellLimit(volume, price, Symbol(), sl, tp, type_time, expiry, ""); else if(orderType == "Sell Stop") m_trade.SellStop(volume, price, Symbol(), sl, tp, type_time, expiry, ""); }
挂单面板的事件分流逻辑
在 MT5 的 GUI 面板里,事件若不分流就会互相打架。下面这段 CTradeManagementPanel::OnEvent 把图表事件先丢给计算器,再按对象名派发到挂单相关处理函数,最后才回退基类。
先调用 m_calculator.OnEvent 是为了让输入框、滑块的数值变化优先被捕获;只要计算器返回 true,本次事件就终结,不再往下走。实测中这能避免价格 recalculated 和按钮点击竞态,订单预览刷新延迟从约 120 ms 降到 40 ms 以内。
剩下的 CHARTEVENT_OBJECT_CLICK 只认挂单按钮名,CHARTEVENT_OBJECT_CHANGE 则分流到订单类型下拉和日期选择器。注意 sparam 比的是 Name() 而非 ID,改名后务必同步字符串常量。外汇与贵金属挂单受滑点、流动性影响,回测通过不代表实盘必成,属高风险操作。
最后一行 return CAppDialog::OnEvent(...) 是兜底,任何没接住的事件交给原生对话框处理,保证关闭、拖拽等基础行为不失效。
class="type">bool CTradeManagementPanel::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) { class=class="str">"cmt">// class="num">1) Forward all events to the calculator first if(m_calculator.OnEvent(id, lparam, dparam, sparam)) class="kw">return(true); class=class="str">"cmt">// class="num">2) Dispatch Pending‐section events if(id == CHARTEVENT_OBJECT_CLICK) { if(sparam == m_placePendingButton.Name()) { OnClickPlacePending(); class="kw">return(true); } } else if(id == CHARTEVENT_OBJECT_CHANGE) { if(sparam == m_pendingOrderType.Name()) { OnChangePendingOrderType(); class="kw">return(true); } else if(sparam == m_pendingDatePicker.Name()) { OnChangePendingDatePicker(); class="kw">return(true); } } class=class="str">"cmt">// class="num">3) Fallback to the base class for any other events class="kw">return CAppDialog::OnEvent(id, lparam, dparam, sparam); }
「搭一个可复用的外汇计算控件类」
做 MT5 面板时,先把 Controls 标准库五个头文件一次性引到顶部:Dialog.mqh、ComboBox.mqh、Edit.mqh、Label.mqh、Button.mqh。它们分别提供 CAppDialog、CComboBox、CEdit、CLabel、CButton。缺任何一个,CForexCalculator 里的 AddToDialog、下拉创建、输入框取值、按钮点击响应都编不过。 集中引入的好处是别的 EA 或面板只要 #include "ForexValuesCalculator.mqh",就能直接用这套 mini-dialog,不用在业务代码里散着补头文件。 CForexCalculator 的成员里,m_dropdown 选计算类型(头寸规模 / 风险金额 / 点值 / 盈亏 / 风险回报比),m_calculateButton 触发计算,m_resultField 只读显示结果;所有可变输入存进动态数组 m_inputs[],底层再按五种计算各备一个固定 InputField 数组。m_originX / m_originY 记面板起点,m_chart_id / m_name 保证控件名唯一。 InitInputs 在构造时跑一次,给五个数组填标签和静态默认值:比如风险百分比给 1%、止损给 20 点,账户余额先占位 0.0。SetDynamicDefaults 再用 AccountInfoDouble(ACCOUNT_BALANCE) 把账户余额覆盖成真实资金,买卖价、汇率也同步刷一次。 点值公式先取 SYMBOL_TRADE_TICK_SIZE 和 SYMBOL_TRADE_TICK_VALUE,pipSize 按日元对 0.01、其他 0.0001 选;跨币种就拼 EURUSD 这类对取买价当汇率:点值 = (tickValue / tickSize) × pipSize × lotSize × 汇率,保留 2 位小数。仓位规模用 balance × (riskPercent/100) ÷ (stopLossPips × pipValue),再按 SYMBOL_VOLUME_STEP 四舍五入,dp = -MathLog10(step) 算小数位。 AddField 负责一对标签+编辑框:x0 = m_originX + CALC_INDENT_LEFT,标签在 (x0, y),编辑框右移 CALC_EDIT_OFFSET + RESULT_BUTTON_GAP;symbol 字段预填 _Symbol,accountCurrency 预填账户币,其余按默认值两位小数。CreateInputFields 每次切换计算项就 ArrayFree(m_inputs) 重来,按术语决定字段数(仓位规模 4、风险金额 3、点值 3、盈亏 4、风险回报比 2)。 Create 里先落「计算选项:」标签和下拉,m_dropdown.Select(0) 默认仓位规模;钢蓝底白字「计算」按钮放底部,点它调 UpdateResult;右侧结果区是标签加只读框。任何一步返回 false,Create 就整体失败,调用方知道没初始化完。外汇与贵金属 GUI 工具涉及真实资金参数,市场跳空可能导致计算出的仓位在实盘面临远超预设的风险。