算法交易中的风险管理器·进阶篇
🛡️

算法交易中的风险管理器·进阶篇

(2/3)·从手动风控到算法封装,解决近距离止损与滑点吞噬收益的隐性漏洞

偏理论进阶 第 2/3 篇
把止损止盈写死在策略里,常在跳空时被动扛亏。算法交易里把风控抽成独立类,才能按波动重算止损、按成交滑点修正开仓量,不让意外价差吃掉计划利润。

遍历持仓把每单风险算清楚

做批量风控前,得先把当前账户里每一张持仓的关键字段抓出来。下面这段逻辑用 r_position.SelectByIndex(i) 按序号定位,再比对 Symbol() 是否等于当前图表品种,只处理同品种仓位,避免跨品种误算。 抓到目标仓位后,止损价、开仓价、浮动盈利、手数、票据号、持仓方向分别存进变量。买仓按 Bid 平仓、卖仓按 Ask 平仓,这个细节不写对,后续测算的平仓点就会偏一个 spread。 风险额不是拍脑袋。用 NormalizeDouble(MathAbs(PriceStopLoss-PriceOpen)/r_symbol.Point(),0) 把止损距离换算成整数点数,再乘 TickValueLoss() 得到的单点价值和手数,得出 potentionLossOnDeal——也就是触止损时这单可能亏多少。外汇和贵金属杠杆高,这套数算错一格点,账户回撤可能就超你容忍线。 让小布替你跑这套 把 lot_cost 和 potentionLossOnDeal 打进 Print(),挂 EA 里跑一轮,看和你手动算的止损风险是否一致,不一致就查 Point 和合约大小。

MQL5 / C++
class="type">class="kw">double lot_cost = r_symbol.TickValueLoss();              class=class="str">"cmt">// 获取当前品种的每点价值(以账户币种计)
class="type">bool ticket_sc = class="num">0;                                       class=class="str">"cmt">// 标记平仓是否成功的布尔变量
r_position.SelectByIndex(i)
      Symbl = r_position.Symbol();                        class=class="str">"cmt">// 取出该持仓的交易品种
      if(Symbl==Symbol())                                class=class="str">"cmt">// 判断是否为当前图表品种
        PriceStopLoss = r_position.StopLoss();            class=class="str">"cmt">// 记录其止损价
        PriceOpen = r_position.PriceOpen();              class=class="str">"cmt">// 记录开仓价
        ProfitCur = r_position.Profit();                 class=class="str">"cmt">// 记录当前浮动盈亏
        LotsOrder = r_position.Volume();                 class=class="str">"cmt">// 记录持仓手数
        Ticket = r_position.Ticket();
        class="type">int dir = r_position.Type();                     class=class="str">"cmt">// 判定持仓类型(买或卖)
        if(dir == POSITION_TYPE_BUY)                     class=class="str">"cmt">// 若是买入持仓
          {
           PriceClose = r_symbol.Bid();                  class=class="str">"cmt">// 以买价平仓
          }
        if(dir == POSITION_TYPE_SELL)                    class=class="str">"cmt">// 若是卖出持仓
          {
           PriceClose = r_symbol.Ask();                  class=class="str">"cmt">// 以卖价平仓
          }
class="type">int curr_sl_ord = (class="type">int) NormalizeDouble(MathAbs(PriceStopLoss-PriceOpen)/r_symbol.Point(),class="num">0); class=class="str">"cmt">// 将止损距离规范化为整数点数
class="type">class="kw">double potentionLossOnDeal = NormalizeDouble(curr_sl_ord * lot_cost * LotsOrder,class="num">2); class=class="str">"cmt">// 计算触及止损时的潜在亏损金额

「滑点超标时如何强制平仓」

风险管理的实战里,光设止损不够,还得防流动性吃掉的滑点。下面这段逻辑判断:当潜在亏损超过单笔风险阈值,且持仓已经浮亏、又挂了止损,就直接市价平掉。 核心判定行 potentionLossOnDeal > NormalizeDouble(riskPerDeal*slippfits, 0) 里,riskPerDeal 是计划单笔风险,slippfits 是可容忍滑点系数,NormalizeDouble(..., 0) 把结果四舍五入到整数金额,避免浮点误差误触。 SlippageCheck 函数开头先 r_symbol.Refresh() 拉最新报价,再声明一组局部变量:收盘价、止损价、开仓价、手数、当前利润、订单号、品种名。这些在后续循环里会被逐仓填充,任一为 0 通常意味着数据未就绪,函数会跳过该仓。 外汇与贵金属杠杆高,滑点可能在数据行情瞬间扩大数倍,这类硬平机制能截断超出预期的小概率亏损,但无法消除跳空风险。

MQL5 / C++
potentionLossOnDeal>NormalizeDouble(riskPerDeal*slippfits,class="num">0) &&   class=class="str">"cmt">// if the resulting stop exceeds risk per trade given the threshold value
ProfitCur<class="num">0 &&   class=class="str">"cmt">// and the order is at a loss
PriceStopLoss != class="num">0  class=class="str">"cmt">// if stop loss is not set, don&class="macro">#x27;t touch
)
ticket_sc = r_trade.PositionClose(Ticket);   class=class="str">"cmt">// close order
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                SlippageCheck                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool RiskManagerAlgo::SlippageCheck(class="type">void) class="kw">override
  {
  r_symbol.Refresh();   class=class="str">"cmt">// update symbol data
  class="type">class="kw">double PriceClose = class="num">0,   class=class="str">"cmt">// close price for the order
        PriceStopLoss = class="num">0,   class=class="str">"cmt">// stop loss price for the order
        PriceOpen = class="num">0,   class=class="str">"cmt">// open price for the order
        LotsOrder = class="num">0,   class=class="str">"cmt">// order lot volume
        ProfitCur = class="num">0;   class=class="str">"cmt">// current order profit
  class="type">ulong  Ticket = class="num">0;   class=class="str">"cmt">// order ticket
  class="type">class="kw">string Symbl;   class=class="str">"cmt">// symbol

◍ 遍历持仓并抓取平仓要素

在 MT5 的 EA 逻辑里,先拿到当前品种的 TickValueLoss() 才能算清每点价值,否则后续盈亏判断只是空转。用一个 bool 变量记录平仓是否成功,比直接依赖 OrderSend 返回值更方便做循环内的状态追踪。 下面这段从 PositionsTotal() 倒序扫持仓,SelectByIndex(i) 选中后只处理 Symbol() 与当前图表品种一致的单子,避免跨品种误平。对外汇与贵金属来说,点差和滑点在高危时段可能瞬间扩大,只平自己品种是基本防守。 选中后把止损、开仓价、浮动盈利、手数、票据号、持仓方向全部暂存到局部变量。买仓按 Bid 平、卖仓按 Ask 平,这个细节若写反,回测和实盘都会出现隐性点差损耗。 代码逐行拆解: double lot_cost = r_symbol.TickValueLoss(); // 取当前品种每tick亏损金额 bool ticket_sc = 0; // 平仓成功标志,初始为假 for(int i = PositionsTotal(); i>=0; i--) // 从持仓总数倒序循环到0 { if(r_position.SelectByIndex(i)) // 按序号选中第i个持仓 { Symbl = r_position.Symbol(); // 读出该持仓交易品种 if(Symbl==Symbol()) // 仅当品种与当前图表一致才继续 { PriceStopLoss = r_position.StopLoss(); // 记录止损价 PriceOpen = r_position.PriceOpen(); // 记录开仓价 ProfitCur = r_position.Profit(); // 记录当前浮动盈亏 LotsOrder = r_position.Volume(); // 记录手数 Ticket = r_position.Ticket(); // 记录持仓票据号 int dir = r_position.Type(); // 判断持仓方向 if(dir == POSITION_TYPE_BUY) // 若为多单 { PriceClose = r_symbol.Bid(); // 多单以买价平仓 } if(dir == POSITION_TYPE_SELL) // 若为空单 { PriceClose = r_symbol.Ask(); // 空单以卖价平仓 }

if(dir == POSITION_TYPE_BUYdir == POSITION_TYPE_SELL) // 确属双向持仓之一

{

MQL5 / C++
  class="type">class="kw">double lot_cost = r_symbol.TickValueLoss();                                                                     class=class="str">"cmt">// get tick value
  class="type">bool ticket_sc = class="num">0;                                                                                                  class=class="str">"cmt">// variable for successful closing
  for(class="type">int i = PositionsTotal(); i>=class="num">0; i--)                                                                             class=class="str">"cmt">// start loop through orders
    {
    if(r_position.SelectByIndex(i))
      {
      Symbl = r_position.Symbol();                                                                                    class=class="str">"cmt">// get the symbol
      if(Symbl==Symbol())                                                                                             class=class="str">"cmt">// check if it&class="macro">#x27;s the right symbol
        {
        PriceStopLoss = r_position.StopLoss();                                                                        class=class="str">"cmt">// remember its stop loss
        PriceOpen = r_position.PriceOpen();                                                                            class=class="str">"cmt">// remember its open price
        ProfitCur = r_position.Profit();                                                                               class=class="str">"cmt">// remember financial result
        LotsOrder = r_position.Volume();                                                                               class=class="str">"cmt">// remember order lot volume
        Ticket = r_position.Ticket();
        class="type">int dir = r_position.Type();                                                                                   class=class="str">"cmt">// define order type
        if(dir == POSITION_TYPE_BUY)                                                                                   class=class="str">"cmt">// if it is Buy
          {
          PriceClose = r_symbol.Bid();                                                                                 class=class="str">"cmt">// close at Bid
          }
        if(dir == POSITION_TYPE_SELL)                                                                                  class=class="str">"cmt">// if it is Sell
          {
          PriceClose = r_symbol.Ask();                                                                                 class=class="str">"cmt">// close at Ask
          }
        if(dir == POSITION_TYPE_BUY || dir == POSITION_TYPE_SELL)

用止损点数卡死单笔风险

EA 在持仓监控里先算止损距离:把(止损价-开仓价)的绝对值除以品种 Point,再 NormalizeDouble 到整数点,得到 curr_sl_ord。这一步直接把价格差翻译成「离场还要扛多少点」,是后面风控的分母。 潜在亏损额用 curr_sl_ord × 每点成本 lot_cost × 手数 LotsOrder 算出,保留两位小数。假设 EURUSD 的 lot_cost 约 10 美元/标准手每点,0.1 手扛 50 点止损,potentionLossOnDeal 就是 50×1×0.1=5 美元,数字可在 MT5 策略测试器里逐项 Print 核对。 只有当三个条件同时成立才强平:算出的潜在亏损大于「单笔风险上限 riskPerDeal × 滑点系数 slippfits」、当前浮亏 ProfitCur<0、且止损价非零(没设止损就不动它)。外汇与贵金属杠杆高,这种硬砍仓逻辑能避免亏损单在极端波动里吃掉账户缓冲,但也可能因滑点导致 Close 失败,需监听 GetLastError 做日志。

MQL5 / C++
class="type">int curr_sl_ord = (class="type">int) NormalizeDouble(MathAbs(PriceStopLoss-PriceOpen)/r_symbol.Point(),class="num">0); class=class="str">"cmt">// check the resulting stop
class="type">class="kw">double potentionLossOnDeal = NormalizeDouble(curr_sl_ord * lot_cost * LotsOrder,class="num">2); class=class="str">"cmt">// calculate risk upon reaching the stop level
if(
   potentionLossOnDeal>NormalizeDouble(riskPerDeal*slippfits,class="num">0) &&   class=class="str">"cmt">// if the resulting stop exceeds risk per trade given the threshold value
   ProfitCur<class="num">0                                                                   &&   class=class="str">"cmt">// and the order is at a loss
   PriceStopLoss != class="num">0                                                                     class=class="str">"cmt">// if stop loss is not set, don&class="macro">#x27;t touch
   )
   {
   ticket_sc = r_trade.PositionClose(Ticket);                                                                       class=class="str">"cmt">// close order
   Print(__FUNCTION__+", RISKPERDEAL: "+DoubleToString(riskPerDeal));                                               class=class="str">"cmt">//
   Print(__FUNCTION__+", slippfits: "+DoubleToString(slippfits));                                                   class=class="str">"cmt">//
   Print(__FUNCTION__+", potentionLossOnDeal: "+DoubleToString(potentionLossOnDeal));                               class=class="str">"cmt">//
   Print(__FUNCTION__+", LotsOrder: "+DoubleToString(LotsOrder));                                                   class=class="str">"cmt">//
   Print(__FUNCTION__+", curr_sl_ord: "+IntegerToString(curr_sl_ord));                                              class=class="str">"cmt">//
   if(!ticket_sc)
      {
      Print(__FUNCTION__+", Error Closing Orders №"+IntegerToString(ticket_sc)+" on slippage. Error №"+IntegerToString(GetLastError())); class=class="str">"cmt">// output to log
      }
   else

「滑点导致的订单关闭日志怎么打」

在 MQL5 的订单处理循环里,当某张订单因滑点被关闭,需要在日志里留下可追溯记录,否则复盘时根本分不清是正常平仓还是异常滑点砍单。 下面这段代码片段嵌在订单遍历逻辑中,作用是把发生滑点关闭的订单号输出到专家日志: Print(__FUNCTION__+", Orders №"+IntegerToString(ticket_sc)+" closed by slippage."); // output to log continue; 其中 __FUNCTION__ 自动返回当前函数名,IntegerToString(ticket_sc) 把订单 ticket 转成字符串拼接,最后 continue 跳过后续处理直接进入下一轮循环。 在 MT5 按 F8 打开策略测试器跑一遍含滑点环境的回测,控制台里搜 'closed by slippage' 就能看到具体哪些单子受影响了,外汇和贵金属点差跳变频繁,这类滑点风险需重点核查。

MQL5 / C++
     {
      Print(__FUNCTION__+", Orders №"+IntegerToString(ticket_sc)+" closed by slippage."); class=class="str">"cmt">// output to log
     }
     class="kw">continue;
   }
  }
 }
}
class="kw">return(ticket_sc);
}
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 开仓前的点差闸门怎么设

EA 在发单之前先跑一遍点差校验,逻辑并不复杂:取当前品种点差,和用户传进来的止损点数乘一个系数做比较。系数通常设在 2 以上,也就是点差一旦超过止损距离的一半,这单就先按住不开。 具体实现里用 CSymbolInfo 的 Spread() 拿到实时点差,塞进 int 型变量,再和 intSL*spreadfits 比大小。超出就返回 false,仓位被拦到下一个 tick 再来评估。外汇和贵金属杠杆高,点差吃掉短止损的概率本来就大,这种拦截能少挨几刀。 别把系数拧得太死 系数若卡在 1 附近,EA 会频繁因点差略超止损而空转,日志刷得凶却不成交。实盘里把 spreadfits 调到 2~3,要么等点差缩回来,要么直接放弃过窄止损的入场,比硬吃亏损更划算。 下面这段是核心判断,开 MT5 把 spreadfits 当成输入参数暴露出来,跑两周看拦截次数和成交质量的取舍。

MQL5 / C++
  class="type">bool SpreadAllowed = true;
  class="type">int SpreadCurrent = r_symbol.Spread();
if(SpreadCurrent>intSL*spreadfits)
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                SpreadMonitor                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool RiskManagerAlgo::SpreadMonitor(class="type">int intSL)
  {
class=class="str">"cmt">//--- spread control
   class="type">bool SpreadAllowed = true;                                                                     class=class="str">"cmt">// allow spread trading and check ratio further
   class="type">int SpreadCurrent = r_symbol.Spread();                                                         class=class="str">"cmt">// current spread values
   if(SpreadCurrent>intSL*spreadfits)                                                             class=class="str">"cmt">// if the current spread is greater than the stop and the coefficient
     {
      SpreadAllowed = false;                                                                      class=class="str">"cmt">// prohibit trading
      Print(__FUNCTION__+IntegerToString(__LINE__)+
            ". 点差太大了!Spread:"+
            IntegerToString(SpreadCurrent)+", SL:"+IntegerToString(intSL));class=class="str">"cmt">// notify
     }
   class="kw">return SpreadAllowed;                                                                          class=class="str">"cmt">// class="kw">return result
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
交给小布盯盘看回撤
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到实时回撤与滑点预警,你专注把风控类接进自己的 EA。

常见问题

算数止损按波动数值计算、不依赖图表形态,适合量化的入场优先逻辑;技术形态止损绑定分型或高低点,破位即离场。进阶实现里两者可并存,由风险管理器按品种波动率切换。
经纪商在快速行情中可能因点差放大拒绝过近止损,独立接口可在开仓前校验距离并平移止损,避免订单被拒或滑点失控。
Getter 封装受保护字段的读取逻辑,外部只能按开发者设定拿到风控参数,不能直接改字段,保证算数止损与滑点上限不被策略层误写。
小布内置的 AIGC 诊断可识别品种页的回撤与滑点异常,但 EA 代码需你自行集成本文的类;小布负责盯盘预警,不替你编译 EA。
点差直接决定算数止损下的开仓手数,放在风控类可在计算风险时同步剔除高点差时段,防止按窄利差算出的仓位在宽利差时爆风险。