风险管理(第二部分):在图形界面中实现手数计算·综合运用
面板里手搭止损与仓位标签
这段代码片段来自一个风险管理面板的初始化逻辑,负责在 MT5 图表上逐行铺出“理想仓位”和“理想止损”两组界面元素。y1 坐标以 25、50、30 为步距累加,说明控件纵向排布留出了明确间隔,避免标签和按钮重叠。 先处理仓位显示:创建名为 L-Result-Lote 的标签,固定前缀文本 “Ideal Lot: ”,右侧再开一个空白标签 L-The-Result-lot 放计算结果,x 偏移 +65 像素。任何 CreateLabel 返回 false 就直接中断并返回 false,保证面板不全就整体不挂载。 止损区块从注释 // --- Stop Loss Calculation Section --- 开始。先建 “Get SL” 标题,紧跟一个下拉框 ORDER_TYPE_SL,默认塞入 ORDER_TYPE_BUY;随后 “Get ideal sl:” 标签配一个 “Get” 按钮(x+90),点击后由别处逻辑算值;最后 Ideal SL: 与空白结果标签同样错开 65 像素。外汇与贵金属波动剧烈,这类面板算出的“理想”值仅基于你设的风险参数,实盘止损可能被滑点击穿,属高风险操作。 最末的 Create() 函数把图表句柄、名称、子窗口与四角坐标传入,先调父类 CAppDialog::Create 再建客户区,两步任一失败即返回 false。复制这段代码到你的 EA 面板类里,把 x1/y1 起始值改成自己图表左边距,编译后能在 EURUSD 的 M5 上直接看到这套控件树。
y1 += class="num">25; if (!CreateLabel(m_label_result_lote, "L-Result-Lote", "Ideal Lot: ", x1, y1)) class="kw">return false; class=class="str">"cmt">// Create the label for displaying the ideal lot if (!CreateLabel(m_label_the_result_lote, "L-The-Result-lot", " ", x1 + class="num">65, y1)) class="kw">return false; class=class="str">"cmt">// Create a label to display the calculated ideal lot y1 += class="num">50; class=class="str">"cmt">// --- Stop Loss Calculation Section --- if (!CreateLabel(m_label_get_sl, "L-Get-SL-Title", "Get SL", x1, y1)) class="kw">return false; class=class="str">"cmt">// Create the label for stop loss calculation section if (!CreateComboBox(m_combobox_order_type_get_sl, "ORDER_TYPE_SL", elements_order_type, "ORDER_TYPE_BUY", x1 + class="num">50, y1)) class="kw">return false; class=class="str">"cmt">// Create the combo box to select order type for stop loss calculation this.order_type_get_sl = ORDER_TYPE_BUY; class=class="str">"cmt">// Default order type y1 += class="num">30; if (!CreateLabel(m_label_lote, "L-LOTE", "Get ideal sl:", x1, y1)) class="kw">return false; class=class="str">"cmt">// Create the label for getting the ideal stop loss if (!CreateButton(m_button_get_sl, "GET-SL", "Get", x1 + class="num">90, y1)) class="kw">return false; class=class="str">"cmt">// Create the button to get the stop loss value y1 += class="num">25; if (!CreateLabel(m_label_result_sl, "L-Result-sl", "Ideal SL:", x1, y1)) class="kw">return false; class=class="str">"cmt">// Create the label for displaying the ideal stop loss if (!CreateLabel(m_label_the_result_sl, "L-The-result-sl", " ", x1 + class="num">65, y1)) class="kw">return false; class=class="str">"cmt">// Create a label to display the calculated ideal stop loss class="kw">return true; class=class="str">"cmt">// If all components are successfully created } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| function to create the interface | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CRiskManagementPanel::Create(const class="type">long chart,const class="type">class="kw">string name,const class="type">int subwin,const class="type">int x1,const class="type">int y1,const class="type">int x2,const class="type">int y2) { if(!CAppDialog::Create(chart,name,subwin,x1,y1,x2,y2)) class="kw">return(false); if(!CreateAreaClientPanel()) class="kw">return(false); class=class="str">"cmt">//--- succeed class="kw">return(true); }
「让面板标签和组合框跟着计算值走」
风控面板的易用性,一半靠动态刷新。用户切一次组合框,标签里的手数和止损就得立刻变,否则算得再准也没人信。下面这组成员函数就干这件事:把内部变量和界面控件绑起来。 EditLabelResultLote 和 EditLabelResultSL 是最轻量的写标签接口,各接收一个 string,直接喂给对应的标签控件。你可以把任何格式化后的数字(比如 "0.32" 或 "125.4")传进去,界面不闪、不卡。 组合框回调才是重点。OnChangeComBoxOrderTypeGetSL 在用户改「取SL用的订单类型」时触发,它遍历 elements_order_type 数组,用 m_combobox_order_type_get_sl.Select() 拿到当前文本,命中后调用 StringOrderTypeToEnum 写进 order_type_get_sl,并 Print 一条日志方便你在专家选项卡里核对。OnChangeComBoxOrderTypeGetLote 逻辑完全一致,只是目标变量换成 order_type_get_lot。 实盘里外汇和贵金属波动大、滑点随机,这类界面同步函数不牵涉下单,但能让你少看错一行数字。建议在 MT5 里接上日志后,手动切 5~6 次组合框,确认 Print 输出的枚举和标签文本一一对应再接计算核心。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Function to edit the text of the lot size label | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CRiskManagementPanel::EditLabelResultLote(class="type">class="kw">string text) { class=class="str">"cmt">// This function updates the text of the label that shows the ideal lot size. this.m_label_the_result_lote.Text(text); class=class="str">"cmt">// Set the new text to the label } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Function to edit the text of the stop loss label | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CRiskManagementPanel::EditLabelResultSL(class="type">class="kw">string text) { class=class="str">"cmt">// This function updates the text of the label that shows the ideal stop loss value. this.m_label_the_result_sl.Text(text); class=class="str">"cmt">// Set the new text to the stop loss label } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Function to update the variable that stores | class=class="str">"cmt">//| the order type to obtain the ideal sl | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CRiskManagementPanel::OnChangeComBoxOrderTypeGetSL(class="type">void) { class=class="str">"cmt">// Iterate through the order types array to find the selected type for(class="type">int i = class="num">0; i < ArraySize(elements_order_type); i++) { class=class="str">"cmt">// If the selected order type matches the one in the array if(m_combobox_order_type_get_sl.Select() == elements_order_type[i]) { class=class="str">"cmt">// Update the order type variable for stop loss this.order_type_get_sl = StringOrderTypeToEnum(m_combobox_order_type_get_sl.Select()); Print("New order type for sl: ", EnumToString(this.order_type_get_sl)); class=class="str">"cmt">// Log the selected order type break; } } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Function to update the variable that stores | class=class="str">"cmt">//| the order type to obtain the ideal lot | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CRiskManagementPanel::OnChangeComBoxOrderTypeGetLote(class="type">void) { class=class="str">"cmt">// Iterate through the order types array to find the selected type for(class="type">int i = class="num">0; i < ArraySize(elements_order_type); i++) { class=class="str">"cmt">// If the selected order type matches the one in the array if(m_combobox_order_type_get_lot.Select() == elements_order_type[i]) { class=class="str">"cmt">// Update the order type variable for lot size this.order_type_get_lot = StringOrderTypeToEnum(m_combobox_order_type_get_lot.Select());
◍ 把面板字符串映射回订单枚举
风控面板里下拉框选中的订单类型通常以字符串形式暂存,真正下发指令前必须还原成 MQL5 的 ENUM_ORDER_TYPE,否则 OrderSend 会直接拒单。 下面这段成员函数接收 const string 入参,逐条比对 8 种常见挂单与市价单字符串,命中即返回对应枚举;若全部不匹配则返回 WRONG_VALUE,调用方应据此拦截非法输入。 注意 WRONG_VALUE 在 MQL5 标准头中定义为 -1(UINT 上下文里是 0xFFFFFFFF),用它做哨兵值比返回 0 安全,因为 0 可能与其他隐式类型混淆。外汇与贵金属杠杆高,枚举错配会导致实际开仓方向偏离预期,复盘时务必先打印 OrderType 再发单。
ENUM_ORDER_TYPE CRiskManagementPanel::StringOrderTypeToEnum(const class="type">class="kw">string OrderType) { if(OrderType == "ORDER_TYPE_BUY") class="kw">return ORDER_TYPE_BUY; if(OrderType == "ORDER_TYPE_SELL") class="kw">return ORDER_TYPE_SELL; if(OrderType == "ORDER_TYPE_BUY_LIMIT") class="kw">return ORDER_TYPE_BUY_LIMIT; if(OrderType == "ORDER_TYPE_SELL_LIMIT") class="kw">return ORDER_TYPE_SELL_LIMIT; if(OrderType == "ORDER_TYPE_BUY_STOP") class="kw">return ORDER_TYPE_BUY_STOP; if(OrderType == "ORDER_TYPE_SELL_STOP") class="kw">return ORDER_TYPE_SELL_STOP; if(OrderType == "ORDER_TYPE_BUY_STOP_LIMIT") class="kw">return ORDER_TYPE_BUY_STOP_LIMIT; if(OrderType == "ORDER_TYPE_SELL_STOP_LIMIT") class="kw">return ORDER_TYPE_SELL_STOP_LIMIT; class="kw">return WRONG_VALUE; }
用自定义事件接管面板按键
在 MQL5 控件库里,面板按钮的响应不走 CHARTEVENT_OBJECT_CLICK 这类默认事件,而是走项目 Includes\Controls\Defines.mqh 里自己定义的事件枚举。这个文件除了放控件配色等默认值,末尾集中声明了所有交互事件,是我们接管键盘与按钮逻辑的总入口。 写 OnChartEvent 之前得先认清楚这点:类里声明的 OnChartEvent 方法,最终要调起上一节那几个算手数、算止损、刷编辑框的函数,但触发源不是系统默认点击,而是 Defines.mqh 映射出来的自定义事件ID。 打开 Includes\Controls\Defines.mqh 翻到文件末尾,你能直接看到事件枚举的声明结构;改面板行为时,只动这里和对应的分发逻辑即可,别去碰 CHARTEVENT_OBJECT_CLICK 那条老路,否则事件会重复触发、编辑框被写两遍。外汇与贵金属品种波动大,面板算出的手数仅作参考,实盘前务必在 MT5 策略测试器里用历史 tick 验一遍事件分发是否漏触发。
「面板控件的三类关键事件怎么接」
在 MT5 自定义面板里,Defines.mqh 用宏把控件事件编号定死了,但实战中真正要盯的只有三个:ON_CLICK(按钮被点)、ON_CHANGE(下拉框值变)、ON_END_EDIT(编辑框输完)。它们各自对应 OnChartEvent 收到的 id 参数,且都必须叠加 CHARTEVENT_CUSTOM 才构成唯一标识,否则会跟系统图表事件撞车。 OnEvent 作为统一入口,拿到 id、lparam、dparam、sparam 四个参数。lparam 一般装控件 ID,这是判断“到底哪个组合框或按钮触发”的唯一依据;例如手数类型组合框变化时用 lparam == m_combobox_order_type_get_lot.Id() 拦截,再去调 OnChangeComBoxOrderTypeGetLote 更新内部 ENUM_ORDER_TYPE。 编辑框走的是 ON_END_EDIT 逻辑。用户填完“每次操作风险”“止损”等字段,if 里比对待 lparam 与控件 ID,命中就把字符串转成 double 或 int 并做规范化,内部变量才刷新——中途编辑不触发,能少算很多无效值。 按钮点击用 ON_CLICK 捕获。m_button_get_lote 按下就跑 GetIdealLot,m_button_get_sl 按下跑 CalculateSL,结果写进 EditLabelResultLote / EditLabelResultSL,随后 m_button_get_lote.Pressed(false) 把按钮弹起。外汇与贵金属波动剧烈,这类自动算出的手数、止损仅作参考,实盘仍可能因滑点偏离。 最后务必把事件回传给基类 CAppDialog,否则未被显式处理的焦点、拖拽等事件会丢。下面这段宏定义可直接抄进 Defines.mqh 对照看,ON_CLICK 是 0、ON_CHANGE 是 4、ON_END_EDIT 是 6,其余编号留着扩展用。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Events | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#define ON_CLICK(class="num">0) class=class="str">"cmt">// clicking on control event class="macro">#define ON_DBL_CLICK(class="num">1) class=class="str">"cmt">// class="type">class="kw">double clicking on control event class="macro">#define ON_SHOW(class="num">2) class=class="str">"cmt">// showing control event class="macro">#define ON_HIDE(class="num">3) class=class="str">"cmt">// hiding control event class="macro">#define ON_CHANGE(class="num">4) class=class="str">"cmt">// changing control event class="macro">#define ON_START_EDIT(class="num">5) class=class="str">"cmt">// start of editing event class="macro">#define ON_END_EDIT(class="num">6) class=class="str">"cmt">// end of editing event class="macro">#define ON_SCROLL_INC(class="num">7) class=class="str">"cmt">// increment of scrollbar event class="macro">#define ON_SCROLL_DEC(class="num">8) class=class="str">"cmt">// decrement of scrollbar event class="macro">#define ON_MOUSE_FOCUS_SET(class="num">9) class=class="str">"cmt">// the "mouse cursor entered the control" event class="macro">#define ON_MOUSE_FOCUS_KILL(class="num">10) class=class="str">"cmt">// the "mouse cursor exited the control" event class="macro">#define ON_DRAG_START(class="num">11) class=class="str">"cmt">// the "control dragging start" event class="macro">#define ON_DRAG_PROCESS(class="num">12) class=class="str">"cmt">// the "control is being dragged" event class="macro">#define ON_DRAG_END(class="num">13) class=class="str">"cmt">// the "control dragging end" event class="macro">#define ON_BRING_TO_TOP(class="num">14) class=class="str">"cmt">// the "mouse events priority increase" event class="macro">#define ON_APP_CLOSE(class="num">100) class=class="str">"cmt">// "closing the application" event class=class="str">"cmt">//+------------------------------------------------------------------+ if(id == ON_CLICK + CHARTEVENT_CUSTOM && lparam == m_button_get_lote.Id()) {
◍ 风控面板的事件分发与仓位反推
在 MT5 自建 GUI 风控面板里,所有控件交互都收敛到 CRiskManagementPanel::OnEvent 这一个入口。它靠 id 与 lparam 区分事件类型:ON_CHANGE 管下拉框切换,ON_END_EDIT 管输入框落定,ON_CLICK 管按钮触发,三者都叠加 CHARTEVENT_CUSTOM 偏移以避开系统事件冲突。 风险额输入框的处理最值得盯:文本先转 double,再除以 100 乘账户余额,最后 NormalizeDouble 到 2 位小数。比如余额 10000、填 2 代表 2%,算出来 risk_per_operation = 200.00,直接 Print 到日志便于核对。 点“获取手数”按钮时,面板会调 GetIdealLot,把算好的风险额、SL 点数、订单类型丢进去,反推出 new_lot,并用 PrintFormat 打出“若失败亏损金额”的实测数值。外汇与贵金属杠杆高,SL 点数和手数错配可能在几秒内吞掉本金,参数务必先在模拟盘验证。 “获取 SL”按钮走另一条路:CalculateSL 根据风险额和当前偏离、stop_limit 反推理想止损点数,返回 long 型 new_sl 并写回标签。这段逻辑让你不必手算点位,但底层假设滑点可控,极端行情可能偏离。
class="type">bool CRiskManagementPanel::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 == ON_CHANGE + CHARTEVENT_CUSTOM && lparam == m_combobox_order_type_get_lot.Id()) { OnChangeComBoxOrderTypeGetLote(); } if(id == ON_CHANGE + CHARTEVENT_CUSTOM && lparam == m_combobox_order_type_get_sl.Id()) { OnChangeComBoxOrderTypeGetSL(); } if(id == ON_END_EDIT + CHARTEVENT_CUSTOM && lparam == m_edit_risk_per_operation.Id()) { this.risk_per_operation = StringToDouble(m_edit_risk_per_operation.Text()); this.risk_per_operation = NormalizeDouble((this.risk_per_operation / class="num">100.0) * AccountInfoDouble(ACCOUNT_BALANCE), class="num">2); Print("Edit Risk Per Operation: ", this.risk_per_operation); } if(id == ON_END_EDIT + CHARTEVENT_CUSTOM && lparam == m_edit_sl.Id()) { this.sl = StringToInteger(m_edit_sl.Text()); Print("Edit SL: ", this.sl); } if(id == ON_END_EDIT + CHARTEVENT_CUSTOM && lparam == m_edit_deviation.Id()) { this.deviation = StringToDouble(m_edit_deviation.Text()); Print("Edit Deviation: ", this.deviation); } if(id == ON_END_EDIT + CHARTEVENT_CUSTOM && lparam == m_edit_stop_limit.Id()) { this.stop_limit = StringToDouble(m_edit_stop_limit.Text()); Print("Edit Stop Limit: ", this.stop_limit); } if(id == ON_CLICK + CHARTEVENT_CUSTOM && lparam == m_button_get_lote.Id()) { Print("Risk Per operation: ", this.risk_per_operation); Print("SL in points: ", this.sl); Print("Order type get lot: ", EnumToString(this.order_type_get_lot)); class="type">class="kw">double new_lot; class="type">class="kw">double new_risk_per_operation; GetIdealLot(new_lot, GetMaxLote(this.order_type_get_lot), this.risk_per_operation, new_risk_per_operation, this.sl); PrintFormat("Loss in case the following operation fails, with the parameters: lot %.2f and stop loss of %i points will be %.2f ", new_lot, this.sl, new_risk_per_operation); EditLabelResultLote(DoubleToString(new_lot, class="num">2)); m_button_get_lote.Pressed(false); } if(id == ON_CLICK + CHARTEVENT_CUSTOM && lparam == m_button_get_sl.Id()) { Print("Risk Per operation: ", this.risk_per_operation); Print("Order type get sl: ", EnumToString(this.order_type_get_lot)); class="type">class="kw">double new_lot; class="type">long new_sl = CalculateSL(this.order_type_get_sl, this.risk_per_operation, new_lot, this.deviation, this.stop_limit); PrintFormat("For the risk per operation %.2f the chosen lot is %.2f and the ideal stop loss in points is %i", this.risk_per_operation, new_lot, new_sl); EditLabelResultSL(IntegerToString(new_sl));
按钮状态复位与事件回传
在自定义对话框里处理完按钮逻辑后,必须把 m_button_get_sl 的按压态清掉,否则界面会卡在“已按下”的视觉状态,下次点击可能不触发新事件。 代码中用 m_button_get_sl.Pressed(false) 显式复位,这一行紧接在业务分支结束的花括号之后,确保无论走哪个分支都能执行到。 最后 return CAppDialog::OnEvent(id, lparam, dparam, sparam) 把未消费的事件交回基类,保证对话框其他控件照常工作。外汇与贵金属面板开发属高风险定制,建议在 MT5 策略测试器以外先用实盘模拟账户验证按钮交互。
m_button_get_sl.Pressed(false); } class="kw">return(CAppDialog::OnEvent(id, lparam, dparam, sparam));
「把风控面板挂进EA生命周期」
要在MT5机器人里跑一个带界面的风险管理模块,第一步是在全局区把自定义类对象放出来。这样OnInit、OnDeinit和OnChartEvent三个系统函数都能直接摸到同一块面板实例,不用反复new。 OnInit里调用Create做实体化:图表ID填0代表主窗口,标题写"Test Risk Management",子窗口参数0(EA环境没有独立副图),左上角坐标(40,40),宽高380×380像素。若Create返回false,立刻return(INIT_FAILED),MT5日志会报初始化失败,机器人不会瞎跑。 面板活起来靠Run(),它把按钮、输入框这些控件的交互不挂起。退场时OnDeinit必须调Destroy,否则残留图形对象可能赖在图表上,下次加载EA容易叠层错位。 用户点按钮或改文本框,事件不会自动进面板——得在OnChartEvent里把id/lparam/dparam/sparam原样转交给risk_panel.ChartEvent。外汇与贵金属杠杆高,这类界面只是参数录入前端,实际下单前仍需人工核对止损位。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Global Variables | class=class="str">"cmt">//+------------------------------------------------------------------+ CRiskManagementPanel risk_panel; class=class="str">"cmt">// Declare the panel object globally class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- Create the risk management panel if(!risk_panel.Create(class="num">0, "Test Risk Management", class="num">0, class="num">40, class="num">40, class="num">380, class="num">380)) class="kw">return(INIT_FAILED); class=class="str">"cmt">// If panel creation fails, class="kw">return an error code class=class="str">"cmt">//--- Run the panel risk_panel.Run(); class=class="str">"cmt">// Start the graphical interface class=class="str">"cmt">//--- Return success class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- Destroy the risk management panel risk_panel.Destroy(reason); class=class="str">"cmt">// Free resources and remove the panel } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert chart event function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(const class="type">int id, class=class="str">"cmt">// Event ID const class="type">long& lparam, class=class="str">"cmt">// Event parameter of type class="type">long const class="type">class="kw">double& dparam, class=class="str">"cmt">// Event parameter of type class="type">class="kw">double const class="type">class="kw">string& sparam) class=class="str">"cmt">// Event parameter of type class="type">class="kw">string { class=class="str">"cmt">//--- Pass the chart event to the risk panel for processing risk_panel.ChartEvent(id, lparam, dparam, sparam); }
◍ 用 XAUUSD 实盘面板跑一遍手数计算
把 EA 加载到 XAUUSD(黄金)图表上,先确认账户没有未平仓位,这样面板输出不会被旧持仓干扰。调试阶段建议把账户余额、风险字段都摊开看,避免隐形参数吃掉了你的真实敞口。 在“每次操作风险”填 3%(按当前余额计),顺手把偏差和止损限额的编辑框也过一遍。选“根据止损点数算手数”,订单类型挑 ORDER_TYPE_BUY,止损给 500 点,点“保存”。面板回 0.03 手,专家选项卡补了一句:最大手数 0.45、风险 3.0%、若按此手数和止损亏掉是 15.45 美元。 再测另一条路——按每次操作风险反推止损。点“获取理想止损”那行的“获取”,出来止损位 1856 点,理想交易量 0.01 手,风险仍是 3.0%。黄金波动大、杠杆高,这类数字仅代表当下参数下的数学结果,实际滑点可能让亏损偏离。 开 MT5 把这两个按钮各点一次,比对你的账户余额算出的手数是否和面板一致;不一致就查偏差字段是不是被改过。
画得少,看得清
这套风险管理面板把最常被手工算错的两件事——手数和止损点位——收进了一个 EA 内嵌界面。核心逻辑落在 Risk_Management.mqh 里,EA 文件 Risk_Management_Panel.mq5(22.34 KB)只负责把编辑框、组合框和按钮的事件接到计算函数上,没有多余装饰。 从工程角度看,MQL5 的 CWndContainer 继承体系让自定义面板可以随窗口重绘自动布局,比早期用 OBJ_RECTANGLE 硬拼坐标稳得多。你打开 MT5 把两个文件丢进 Include 与 Experts 目录编译,就能在图表右侧看到可输入余额、风险百分比和止损距离的实时手数反馈。 外汇与贵金属杠杆高、滑点随机,任何面板算出的数值都只是概率参考,真下单前仍要核对点值和合约规模。下一步若想扩成多品种监控,直接在主类里复用这里的 CComboBox 品种枚举即可,不必重写布局代码。