风险管理(第二部分):在图形界面中实现手数计算(基础篇)
📘

风险管理(第二部分):在图形界面中实现手数计算(基础篇)

第 1/3 篇

◍ 在 MT5 面板里实时算手数

把风险管理从 EA 后台搬到图形面板,核心是先改好手数与止损的检索函数,再让控件能随行情刷新。原文给出的实现路径是:用 MQL5 标准面板库画标签、按钮、组合框,在 OnInit 里做完常规初始化,之后靠 OnCharEvent 接键盘输入。 作者 Niquel Mendoza 在 2025 年 12 月 15 日发布的这篇示例,页面统计显示有 1 257 次查看、0 条评论,说明该面板原型偏底层验证、尚未引发社区讨论。你可以在 MT5 里新建一个 EA,把面板库 include 进来,照着这条链路把止损价和账户净值喂进手数公式。 验证方式很直接:编译后拖进图表,用键盘改一下止损距离,看组合框里的手数是否动态变化。外汇与贵金属杠杆高,面板算出的手数仅作仓位参考,实际成交风险仍由市场波动主导。

「用控件库把风控函数做成界面」

这一节把前面讲过的风控逻辑真正落到 MT5 界面里,不再靠手动填参数算手数。核心做法是直接调用 MQL5 自带的控件库(Standard Library 的 CAppDialog 等),把仓位计算和止损距离封装成可交互面板。 对交易者来说,价值在于:开仓前输入账户风险百分比、止损点数,界面直接给出手数,避免心算误差。外汇和贵金属杠杆高,手数算错可能瞬间放大回撤,这类工具更偏风控而非预测。 后续会重构之前文章里的几个函数,让它们能被界面按钮直接调用,而不是只在 EA 里后台跑。你打开 MT5 的 MQL5/Include/Controls 目录就能看到现成控件头文件,本节就基于它们改。

手数与止损检索函数的健壮性改造

把原先的手数和止损计算函数重做了一遍,核心目标是少踩坑、好排错。改动集中在四处:最大手数函数加了可用预付款校验和失败打印;理想手数函数先卡死止损距离有效性;新增一个只吃美元风险、不依赖止损距离的手数函数;止损距离计算改为复用新手数结果。 调试消息是这次的重点。GetMaxLote 里如果 OrderCalcMargin 算不出保证金,直接 Print 出错误码;可用预付款 ≤0 时用 PrintFormat 报出具体负值,避免静默返回 0 让人误以为是正常手数。外汇和贵金属杠杆高,预付款瞬间变负并不罕见,这类提示能省掉大量盲调时间。 GetIdealLot 现在强制要求 StopLoss>0,否则打印 ERROR SL 并把 nlot 置 0。它顺手把点差(SYMBOL_SPREAD)加 1 点再乘 tick_value 算进风险,比只算纯止损距离更接近实盘磨损。 新增的 GetLotByRiskPerOperation 只传每次操作的美元风险和下单类型,不强制给止损,适合那些用固定金额试仓的交易者。CalculateSL 则反过来,用手数反推止损点数,并保证结果为正。开 MT5 把下面代码丢进 EA 里跑一遍,改改 DEVIATION 和 STOP_LIMIT 参数就能看到不同品种的手数边界。

MQL5 / C++
<span class="comment">class=class="str">"cmt">//+----------------------------------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| Calculates the maximum lot size that can be traded based on free margin and symbol specifics |</span>
<span class="comment">class=class="str">"cmt">//+----------------------------------------------------------------------------------------------+</span>
<span class="keyword">class="type">class="kw">double</span> GetMaxLote(<span class="macro">ENUM_ORDER_TYPE</span> type, <span class="keyword">class="type">class="kw">double</span> DEVIATION = <span class="number">class="num">100</span>, <span class="keyword">class="type">class="kw">double</span> STOP_LIMIT = <span class="number">class="num">50</span>)
  {
   <span class="keyword">class="type">class="kw">double</span> VOLUME = <span class="number">class="num">1.0</span>; <span class="comment">class=class="str">"cmt">// 初始手数设为1标准手,用于测算单手保证金</span>
   <span class="macro">ENUM_ORDER_TYPE</span> new_type = MarketOrderByOrderType(type); <span class="comment">class=class="str">"cmt">// 把传入订单类型映射成实际下单类型</span>
   <span class="keyword">class="type">class="kw">double</span> price = PriceByOrderType(<span class="predefines">_Symbol</span>, type, DEVIATION, STOP_LIMIT); <span class="comment">class=class="str">"cmt">// 按类型和偏差拿到对应报价</span>
   <span class="keyword">class="type">class="kw">double</span> volume_step = <span class="functions">SymbolInfoDouble</span>(<span class="predefines">_Symbol</span>, <span class="macro">SYMBOL_VOLUME_STEP</span>); <span class="comment">class=class="str">"cmt">// 读取品种最小手数步长</span>
   <span class="keyword">class="type">class="kw">double</span> margin = <span class="macro">EMPTY_VALUE</span>; <span class="comment">class=class="str">"cmt">// 所需保证金先置空</span>
   <span class="functions">ResetLastError</span>(); <span class="comment">class=class="str">"cmt">// 清掉之前的错误码</span>
   <span class="keyword">if</span>(!<span class="functions">OrderCalcMargin</span>(new_type, <span class="predefines">_Symbol</span>, VOLUME, price, margin)) <span class="comment">class=class="str">"cmt">// 算1手需要的保证金</span>
     {
      <span class="functions">Print</span>(<span class="class="type">class="kw">string">"OrderCalcMargin() failed. Error "</span>, <span class="functions">GetLastError</span>()); <span class="comment">class=class="str">"cmt">// 失败就打印错误码</span>
      <span class="keyword">class="kw">return</span> <span class="number">class="num">0</span>; <span class="comment">class=class="str">"cmt">// 算不出保证金直接退出返回0</span>
     }
   <span class="keyword">if</span>(<span class="functions">AccountInfoDouble</span>(<span class="macro">ACCOUNT_MARGIN_FREE</span>)  <= <span class="number">class="num">0</span>) <span class="comment">class=class="str">"cmt">// 检查可用预付款是否非负</span>
     {
      <span class="functions">PrintFormat</span>(<span class="class="type">class="kw">string">"Free margin of %+.2f is invalid, you cannot open trades right now"</span>,<span class="functions">AccountInfoDouble</span>(<span class="macro">ACCOUNT_MARGIN_FREE</span>)); <span class="comment">class=class="str">"cmt">// 打印负预付款数值</span>
      <span class="keyword">class="kw">return</span> <span class="number">class="num">0</span>; <span class="comment">class=class="str">"cmt">// 不可用则退出</span>
     }
   <span class="keyword">class="type">class="kw">double</span> result = <span class="functions">MathFloor</span>((<span class="functions">AccountInfoDouble</span>(<span class="macro">ACCOUNT_MARGIN_FREE</span>) / margin) / volume_step) * volume_step; <span class="comment">class=class="str">"cmt">// 可用预付款除以单手保证金再按步长向下取整</span>
   <span class="keyword">class="kw">return</span> result; <span class="comment">class=class="str">"cmt">// 返回最大可开手数</span>
  }
<span class="comment">class=class="str">"cmt">//+---------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| Determine the optimal lot size based on risk and current conditions |</span>
<span class="comment">class=class="str">"cmt">//+---------------------------------------------------------------------+</span>
<span class="keyword">class="type">void</span> GetIdealLot(<span class="keyword">class="type">class="kw">double</span>&amp; nlot, <span class="keyword">class="type">class="kw">double</span> glot, <span class="keyword">class="type">class="kw">double</span> max_risk_per_operation, <span class="keyword">class="type">class="kw">double</span>&amp; new_risk_per_operation, <span class="keyword">class="type">long</span> StopLoss)
  {
   <span class="keyword">if</span>(StopLoss <= <span class="number">class="num">0</span>) <span class="comment">class=class="str">"cmt">// 止损距离必须为正</span>
     {
      <span class="functions">Print</span>(<span class="class="type">class="kw">string">"[ERROR SL] Stop Loss distance is less than or equal to zero, now correct the stoploss distance: "</span>, StopLoss); <span class="comment">class=class="str">"cmt">// 报错并打印无效距离</span>
      nlot = <span class="number">class="num">0.0</span>; <span class="comment">class=class="str">"cmt">// 手数置0</span>
      <span class="keyword">class="kw">return</span>;
     }
   <span class="functions">Print</span>(<span class="class="type">class="kw">string">"Max Lot: "</span>, glot, <span class="class="type">class="kw">string">"  |  RiskPerOperation: "</span>, max_risk_per_operation); <span class="comment">class=class="str">"cmt">// 打印最大手数和单笔风险</span>
   new_risk_per_operation = <span class="number">class="num">0</span>; <span class="comment">class=class="str">"cmt">// 初始化实际风险</span>
   <span class="keyword">class="type">long</span> spread = (<span class="keyword">class="type">long</span>)<span class="functions">SymbolInfoInteger</span>(<span class="predefines">_Symbol</span>, <span class="macro">SYMBOL_SPREAD</span>); <span class="comment">class=class="str">"cmt">// 取点差</span>
   <span class="keyword">class="type">class="kw">double</span> tick_value = <span class="functions">SymbolInfoDouble</span>(<span class="predefines">_Symbol</span>, <span class="macro">SYMBOL_TRADE_TICK_VALUE</span>); <span class="comment">class=class="str">"cmt">// 每跳价值</span>
   <span class="keyword">class="type">class="kw">double</span> step = <span class="functions">SymbolInfoDouble</span>(<span class="predefines">_Symbol</span>, <span class="macro">SYMBOL_VOLUME_STEP</span>); <span class="comment">class=class="str">"cmt">// 手数步长</span>
   <span class="keyword">class="type">class="kw">double</span> rpo = (glot * (spread + <span class="number">class="num">1</span> + (StopLoss * tick_value))); <span class="comment">class=class="str">"cmt">// 用最大手数、点差+1点及止损跳值估算风险</span>

◍ 按固定美元风险反推手数与止损

这段逻辑解决一个实战问题:你只关心每笔最多亏多少美元,而不是百分比,那就得让程序反推能开多大仓、止损该放几点。 先看第一段手数裁剪:当实际风险 rpo 超过设定上限 max_risk_per_operation 时,若上限本身 ≤0 直接退出;否则用 new_lot = (上限/实际风险)*原手数 缩放,再按品种步长 MathFloor 对齐。新风险重新用 spread+1+SL*tick_value 估算,避免点差吞掉预算。 核心函数 GetLotByRiskPerOperation 接收的是美元金额(注释明确 risk_per_operation in USD, not %)。它先用 OrderCalcMargin 算 1 标准手所需保证金,再用 MathFloor((风险/保证金)/步长)*步长 得到合规手数;若结果 ≤0 打印无效并返 0。 CalculateSL 则串起前面:先拿理想手数,再读 SYMBOL_SPREAD 和 SYMBOL_TRADE_TICK_VALUE,为后续用风险反推止损点数做准备。外汇与贵金属杠杆高,点值随品种跳动,这类计算必须在实盘前于 MT5 策略测试器用真实品种验证。 别把保证金当静态常数 不同经纪商对同一品种的 OrderCalcMargin 返回可能差出 5%~15%,尤其贵金属交叉盘。开仓前打印一次 margin 值,比盲目信回测里的写死数字稳妥。

MQL5 / C++
if(rpo > max_risk_per_operation)
  {
    if(max_risk_per_operation <= class="num">0)
      class="kw">return;
    class="type">class="kw">double new_lot = (max_risk_per_operation / rpo) * glot;
    new_lot  = MathFloor(new_lot / step) * step;
    new_risk_per_operation =  new_lot * (spread + class="num">1 + (StopLoss * tick_value));
    nlot =  new_lot;
   }
 else
   {
    new_risk_per_operation = rpo;
    nlot = glot;
   }
 if(nlot <= class="num">0)
    PrintFormat("The lot %.2f is invalid, the risk %.2f increases or the sl %i decreases",nlot,max_risk_per_operation,StopLoss);
}
class=class="str">"cmt">//+--------------------------------------------------------------------+
class=class="str">"cmt">//| Function to obtain the ideal lot based on your risk per operation  |
class=class="str">"cmt">//+--------------------------------------------------------------------+
class=class="str">"cmt">// risk_per_operation in USD, not %
class="type">class="kw">double GetLotByRiskPerOperation(class="type">class="kw">double risk_per_operation, const ENUM_ORDER_TYPE order_type, class="type">class="kw">double DEVIATION = class="num">100, class="type">class="kw">double STOP_LIMIT = class="num">50)
  {
   class="type">class="kw">double VOLUME = class="num">1.0; class=class="str">"cmt">// Initial volume size
   ENUM_ORDER_TYPE new_type = MarketOrderByOrderType(order_type);
   class="type">class="kw">double price = PriceByOrderType(_Symbol, order_type, DEVIATION, STOP_LIMIT); class=class="str">"cmt">// Price for the given order type
   class="type">class="kw">double volume_step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); class=class="str">"cmt">// Volume step for the symbol
   class="type">class="kw">double margin = EMPTY_VALUE; class=class="str">"cmt">// Required margin, initialized as empty
   ResetLastError();
   if(!OrderCalcMargin(new_type, _Symbol, VOLUME, price, margin))
     {
      Print("OrderCalcMargin() failed. Error ", GetLastError());
      class="kw">return class="num">0; class=class="str">"cmt">// Exit the function if margin calculation fails
     }
   class="type">class="kw">double result = MathFloor((risk_per_operation / margin) / volume_step) * volume_step;
   if(result <= class="num">0)
     {
      PrintFormat("The lot %.2f is invalid, the risk %.2f increases",result,risk_per_operation);
      class="kw">return class="num">0;
     }
   PrintFormat("The ideal lot for %.2f risk per trade is %.2f lots",risk_per_operation,result);
   class="kw">return result; class=class="str">"cmt">// Return the calculated maximum lot size
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+-----------------------------------------------------------------------+
class=class="str">"cmt">//| Calculate the stop loss distance in points based on risk and lot size |
class=class="str">"cmt">//+-----------------------------------------------------------------------+
class="type">class="kw">double CalculateSL(const ENUM_ORDER_TYPE type, class="type">class="kw">double risk_per_operation, class="type">class="kw">double &chosen_lot, class="type">class="kw">double DEVIATION = class="num">100, class="type">class="kw">double STOP_LIMIT = class="num">50)
  {
   class="type">class="kw">double lot = GetLotByRiskPerOperation(risk_per_operation,type,DEVIATION,STOP_LIMIT);
   chosen_lot = lot;
   class="type">long spread = (class="type">long)SymbolInfoInteger(_Symbol, SYMBOL_SPREAD);
   class="type">class="kw">double tick_value = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);

「用风险额度反推止损点数」

在 MT5 脚本里,常用「每笔风险金额」倒推应该挂多远止损。下面这段逻辑把点值、点差和手数全摊开算,直接给出整数止损点数。 核心算式是:result = ((risk_per_operation / lot) - spread - 1) / tick_value。也就是先用总风险额除以手数得到每手可亏,再扣掉点差和 1 个缓冲点,最后除以每跳价值,得到以 tick 为单位的理论止损距离。

MQL5 / C++
class="type">class="kw">double result = ((risk_per_operation / lot) - spread - class="num">1) / tick_value;
class="type">long ideal_sl = class="type">long(MathRound(result));
if(ideal_sl <= class="num">0)
  {
  PrintFormat("Stop loss %i invalid, please increase the risk per trade %.2f",ideal_sl,risk_per_operation);
  class="kw">return class="num">0;
  }
class="kw">return ideal_sl;
}
逐行拆解:第 1 行定义双精度变量 result,按上述公式算出理论止损跳数;第 2 行用 MathRound 四舍五入并强转成 long 型整数 ideal_sl。第 3~7 行做防御——若 ideal_sl 小于等于 0,说明风险额相对点差太小,打印提示并返回 0 终止,否则第 8 行返回计算出的止损点数。 外汇与贵金属属高杠杆品种,点差跳变会让实际亏损偏离预设。开 MT5 把这段塞进 EA 的仓位模块,调一下 risk_per_operation 和 lot,就能看见不同风险偏好下止损被推到什么位置。

MQL5 / C++
class="type">class="kw">double result = ((risk_per_operation / lot) - spread - class="num">1) / tick_value;
class="type">long ideal_sl = class="type">long(MathRound(result));
if(ideal_sl <= class="num">0)
  {
  PrintFormat("Stop loss %i invalid, please increase the risk per trade %.2f",ideal_sl,risk_per_operation);
  class="kw">return class="num">0;
  }
class="kw">return ideal_sl;

从坐标与锚点摸清 MQL5 控件地基

在 MT5 里画任何界面元素,先要认清楚图形对象靠 X、Y 两个坐标定位:X 管水平位置,Y 管垂直位置。光有位置不够,对象还有高度和宽度属性,设错了控件就会溢出面板可见边界,界面直接乱掉。 锚点决定 X/Y 以对象的哪一部分作参考,MQL5 用 ENUM_BASE_CORNER 提供四种:CORNER_LEFT_UPPER(左上)、CORNER_LEFT_LOWER(左下)、CORNER_RIGHT_LOWER(右下)、CORNER_RIGHT_UPPER(右上)。实战中 CORNER_LEFT_UPPER 是默认最常用锚点,本文所有示例也只采用这四种之一。 控件库本身在 MetaEditor 按 F1 打开文档,进“内容 → MQL5 帮助文件 → 标准库”才能看到。类结构从 CObject 往下,CWnd 派生出 CWndContainer(复杂控件)和 CWndObj(简单控件);当前要落地的主面板类是直接用 CAppDialog 撑起来的。 具体控件映射关系很固定:CLabel 显示静态文本(派生于 OBJ_LABEL),CComboBox 做下拉选择且是复合对象,CButton 对应 OBJ_BUTTON 执行动作,CEdit 对应 OBJ_EDIT 收手动输入。开 MT5 建个空 EA,先拖一个 CAppDialog 把这四个类实例化一遍,比读文档来得快。

常见问题

在风控面板输入固定美元风险和当前止损点数,界面会按品种合约大小实时算出手数,改一个数立刻刷新,不用自己拿计算器估。
止损检索函数没做空值保护,锚点缺失就返回异常;加一层健壮性判断,找不到止损线时提示手动输入而非静默出0。
可以,小布盯盘已内置风险反推逻辑,打开对应品种页填好可亏金额和止损,它直接给出手数与止损点建议。
多数情况是锚点坐标算错导致控件画出屏幕外,先打印坐标调试,再确认点击回调函数已挂到对应组件上。
把「每笔可亏美元」做成输入框,联动止损点数框,用反推公式实时输出手数,比拍脑袋定仓位更抗回撤。