开发多币种 EA 交易系统(第 14 部分):风险管理器的适应性交易量变化·综合运用
📘

开发多币种 EA 交易系统(第 14 部分):风险管理器的适应性交易量变化·综合运用

第 3/3 篇

◍ 限额检查的拆分式重写

虚拟风控模块把限额检查从一团揉在一起的逻辑,拆成了 DailyLoss / OverallLoss / OverallProfit 三个独立布尔方法,主入口 CheckLimits() 只负责「任一触发即记录虚拟利润快照并广播仓位变更」。这样做的好处是:单日亏损、总亏损、总利润三条线可以分别调参,不会互相耦合。 每日亏损检查里有个关键阈值——当 m_dailyDepoPart 乘子被削到小于 0.05 时直接归零,意味着该日可用保证金占比已低到不再值得开仓。外汇与贵金属杠杆高,这种硬截断能压住连续回撤下的情绪加仓冲动,但也可能在震荡市过早锁死当日交易权限。 总亏损检查逻辑相近,区别在于只有 m_overallDepoPart 归零才切到 RM_STATE_OVERALL_LOSS;而总利润达标时只是重置乘子并标记 RM_STATE_OVERALL_PROFIT,不碰保证金占用。三者返回 bool 供 CheckLimits 判断,下面是拆出来的核心片段。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Check loss limits                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CVirtualRiskManager::CheckLimits() {
   if(false
      || CheckDailyLossLimit()     class=class="str">"cmt">// Check daily limit
      || CheckOverallLossLimit()   class=class="str">"cmt">// Check overall limit
      || CheckOverallProfitLimit() class=class="str">"cmt">// Check overall profit
   ) {
      class=class="str">"cmt">// Remember the current level of class="kw">virtual profit
      m_lastVirtualProfit = m_virtualProfit;
      class=class="str">"cmt">// Notify the recipient about changes
      CVirtualReceiver::Instance().Changed();
   }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Check daily loss limit                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CVirtualRiskManager::CheckDailyLossLimit() {
class=class="str">"cmt">// If daily loss is reached and positions are still open
   if(m_dailyProfit < -DailyLoss() * (class="num">1 - m_dailyDepoPart * (class="num">1 - m_closeDailyPart))
&& CMoney::DepoPart() > class="num">0) {
      class=class="str">"cmt">// Reduce the multiplier of the used part of the overall balance by the daily loss
      m_dailyDepoPart *= (class="num">1 - m_closeDailyPart);
      class=class="str">"cmt">// If the multiplier is already too small,
      if(m_dailyDepoPart < class="num">0.05) {
         class=class="str">"cmt">// Set it to class="num">0
         m_dailyDepoPart = class="num">0;
      }
      class=class="str">"cmt">// Set the value of the used part of the overall balance
      SetDepoPart();
      class=class="str">"cmt">// Set the risk manager to the achieved daily loss state
      m_state = RM_STATE_DAILY_LOSS;
      class="kw">return true;
   }
   class="kw">return false;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Check the overall loss limit                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CVirtualRiskManager::CheckOverallLossLimit() {
class=class="str">"cmt">// If overall loss is reached and positions are still open
   if(m_overallProfit < -OverallLoss() * (class="num">1 - m_overallDepoPart * (class="num">1 - m_closeOverallPart))
      && CMoney::DepoPart() > class="num">0) {
      class=class="str">"cmt">// Reduce the multiplier of the used part of the overall balance by the overall loss
      m_overallDepoPart *= (class="num">1 - m_closeOverallPart);
      class=class="str">"cmt">// If the multiplier is already too small,
      if(m_overallDepoPart < class="num">0.05) {
         class=class="str">"cmt">// Set it to class="num">0
         m_overallDepoPart = class="num">0;
         class=class="str">"cmt">// Set the risk manager to the achieved overall loss state
         m_state = RM_STATE_OVERALL_LOSS;
      }
      class=class="str">"cmt">// Set the value of the used part of the overall balance
      SetDepoPart();

用虚拟风控类锁死整体利润与日损边界

CVirtualRiskManager 里的 CheckOverallProfitLimit 只做一件事:当 m_overallProfit 越过 m_maxOverallProfitLimit 且账户还有仓位占用(CMoney::DepoPart()>0),就把 m_overallDepoPart 归零并切到 RM_STATE_OVERALL_PROFIT。这意味着整体利润达标后,后续新开仓的可用保证金比例直接被掐断,属于硬止损式利润保护。 UpdateBaseLevels 每天重锚基准:把当前 balance 与 equity 的较大值赋给 m_baseDailyLevel,再用 m_dailyProfit = m_equity - m_baseDailyLevel 算出当日浮动盈亏。若之前已触发 RM_STATE_DAILY_LOSS,这里会翻成 RM_STATE_RESTORE 并记录 m_startRestoreTime,等于给仓位恢复流程打了一个时间戳。 外汇与贵金属杠杆高,这类虚拟风控逻辑只管资金比例边界,不预测行情;实盘前务必在 MT5 策略测试器里把 m_maxOverallProfitLimit 和日损阈值按自己承受力改掉再跑。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Check if the specified profit has been achieved                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CVirtualRiskManager::CheckOverallProfitLimit() {
class=class="str">"cmt">// If overall loss is reached and positions are still open
  if(m_overallProfit > m_maxOverallProfitLimit && CMoney::DepoPart() > class="num">0) {
    class=class="str">"cmt">// Reduce the multiplier of the used part of the overall balance by the overall loss
    m_overallDepoPart = class="num">0;
    class=class="str">"cmt">// Set the risk manager to the achieved overall profit state
    m_state = RM_STATE_OVERALL_PROFIT;
    class=class="str">"cmt">// Set the value of the used part of the overall balance
    SetDepoPart();
    ...
    class="kw">return true;
  }
  class="kw">return false;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Update daily base levels                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CVirtualRiskManager::UpdateBaseLevels() {
class=class="str">"cmt">// Update balance, funds and base daily level
  m_baseDailyBalance = m_balance;
  m_baseDailyEquity = m_equity;
  m_baseDailyLevel = MathMax(m_baseDailyBalance, m_baseDailyEquity);
  m_dailyProfit = m_equity - m_baseDailyLevel;
  ...
class=class="str">"cmt">// If the daily loss level was reached earlier, then
  if(m_state == RM_STATE_DAILY_LOSS) {
    class=class="str">"cmt">// Switch to the state of restoring the sizes of open positions
    m_state = RM_STATE_RESTORE;
    class=class="str">"cmt">// Remember restoration start time
    m_startRestoreTime = TimeCurrent();
  }
}

「亏损乘数怎么自动回血」

虚拟风控里,日亏损乘数和总亏损乘数被压低后,并不是永远半仓运行。系统会持续判断:当前虚拟利润是否掉到了「该恢复」的阈值以下,一旦触发,就以当前价格重开真实仓位,把乘数重新拉回 1.0。 CheckRestore() 是入口,只在 m_state == RM_STATE_RESTORE 时干活。它并行调用 CheckDailyRestore() 与 CheckOverallRestore(),两者独立判定;只要有一个恢复成功,就重算占用保证金比例并广播变更事件,两个都恢复了才把状态置回 RM_STATE_OK。 恢复阈值不是写死的。RestoreVirtualProfit() 用线性插值算:从开始恢复时刻起,时间拖得越久(分钟计),允许重开仓位的虚拟利润标准就越低,公式是 m_lastVirtualProfit * m_lastVirtualProfitFactor * (1 - t / m_maxRestoreTime)。若 m_maxRestoreTime 为 0,则直接返回当前虚拟利润,等于不等待。 外汇与贵金属杠杆高,这类自动恢复逻辑若参数过松,可能在不利价位放大敞口,实盘前务必在 MT5 策略测试器里跑一轮看回撤曲线。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Check the need for restoring the size of open positions          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CVirtualRiskManager::CheckRestore() {
class=class="str">"cmt">// If we need to restore the state to normal, then
  if(m_state == RM_STATE_RESTORE) {
    class=class="str">"cmt">// Check the possibility of restoring the daily loss multiplier to normal 
    class="type">bool dailyRes = CheckDailyRestore();
    class=class="str">"cmt">// Check the possibility of restoring the overall loss multiplier to normal
    class="type">bool overallRes = CheckOverallRestore();
    class=class="str">"cmt">// If at least one of them has recovered,
    if(dailyRes || overallRes) {
      
      class=class="str">"cmt">// Set the value of the used part of the overall balance
      SetDepoPart();
      class=class="str">"cmt">// Notify the recipient about changes
      CVirtualReceiver::Instance().Changed();
      class=class="str">"cmt">// If both multipliers are restored to normal,
      if(dailyRes && overallRes) {
        class=class="str">"cmt">// Set normal state
        m_state = RM_STATE_OK;
      }
    }
  }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Check if the daily multiplier needs to be restored               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CVirtualRiskManager::CheckDailyRestore() {
class=class="str">"cmt">// If the current class="kw">virtual profit is less than the one desired for recovery,
  if(m_virtualProfit <= RestoreVirtualProfit()) {
    class=class="str">"cmt">// Restore the daily loss multiplier
    m_dailyDepoPart = class="num">1.0;
    class="kw">return true;
  }
  class="kw">return false;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Check if the overall multiplier needs to be restored             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CVirtualRiskManager::CheckOverallRestore() {
class=class="str">"cmt">// If the current class="kw">virtual profit is less than the one desired for recovery,
  if(m_virtualProfit <= RestoreVirtualProfit()) {
    class=class="str">"cmt">// Restore the overall loss multiplier
    m_overallDepoPart = class="num">1.0;
    class="kw">return true;
  }
  class="kw">return false;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Determine the profit of class="kw">virtual positions for recovery           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CVirtualRiskManager::RestoreVirtualProfit() {
class=class="str">"cmt">// If the maximum recovery time is not specified,
  if(m_maxRestoreTime == class="num">0) {
    class=class="str">"cmt">// Return the current value of the class="kw">virtual profit
    class="kw">return m_virtualProfit;
  }
class=class="str">"cmt">// Find the elapsed time since the start of recovery in minutes
  class="type">class="kw">double t = (TimeCurrent() - m_startRestoreTime) / class="num">60.0;
class=class="str">"cmt">// Return the calculated value of the desired class="kw">virtual profit
class=class="str">"cmt">// depending on the time elapsed since the start of recovery
  class="kw">return m_lastVirtualProfit * m_lastVirtualProfitFactor * (class="num">1 - t / m_maxRestoreTime);
}

◍ 把风控器推到极限看边界

先按原风险管理器参数跑 EA,daily 与 overall 限额触发时全平(scale_=1.0),图 6 显示回撤与基准完全一致,归一化年均利润(OnTester 输出)反而略高于预期,说明改动没破坏原有逻辑。 把部分平仓比例 rmCloseDailyPart_ 和 rmCloseOverallPart_ 都设成 0.5,回撤略微收窄,归一化年均利润随之小涨;再把回撤期最优入场等待 rmMaxRestoreTime_ 设 1440 分钟、虚拟利润乘数 rmLastVirtualProfitFactor_ 设 1.5(纯直觉值),图 8 里归一化年均利润又提了几个百分点,这套机制值得做。 把仓位规模 scale_ 拉到 3 倍,风控触发更频繁:利润约翻 2 倍,但回撤涨了 3 倍,归一化值被拖低,不过单日回撤始终没破限额。再扩到 10 倍(图 10 实际参数仍记 scale_=3.0,原文此处疑似笔误),风控已兜不住总损失,直接停手——它绝非万能盾,只是策略里资金管理的补丁。 用遗传优化挑风控参数,两年样本每遍约 3 分钟;以归一化年均利润为优化准则,表头显示好参数能比差参数多约 20% 额外利润,不过最优集仍略低于图 8 的直觉值。外汇与贵金属杠杆高,这类回测结论只代表历史样本表现,实盘可能失效。

别让风控器替你扛枪

这套虚拟风险管理器把可接受回撤的硬限制做成了可插拔机制,比写死在 EA 里的老办法灵活,MT5 里加载 VirtualRiskManager.mqh(45.58 KB)就能接自己的仓位逻辑。 但回测里有个现象值得警醒:当实际仓位比推荐值大出一大截时,净值曲线斜率会陡到风控器来不及砍仓,回撤就已经击穿阈值。外汇和贵金属杠杆高,这种瞬杀不是小概率。 正确姿态是把它当最后一道保险,而不是依赖它救场。调参时尽量让管理器整轮跑下来一次都不触发,才算把仓位算对了。 暂时先不堆新功能了,下一篇回到 EA 主线的紧迫问题,这套管理器留给你在策略测试器里跑一遍验证边界。

常见问题

把整段限额判断拆成单币种预检+汇总复核两步,先各自算占用再统一比上限,能避开循环里重复锁资源导致的卡死。
在类里维护总浮盈与当日已亏两个变量,每笔平仓后刷新,触任一边界即禁止新开仓,不依赖具体币种回调。
小布可接入你的品种页做AIGC诊断,自动标出当日亏损与总利润临界,越线直接推送提醒,你只管决策。
把乘数上限绑在日损百分比上,回血只在本日亏损回收过半后逐步降档,别让乘数连续翻倍数。
多是异步平仓刷新延迟所致,极限测试需加50毫秒级状态同步等待,否则边界判断读到旧值。