开发多币种 EA 交易系统(第 14 部分):风险管理器的适应性交易量变化·综合运用
◍ 限额检查的拆分式重写
虚拟风控模块把限额检查从一团揉在一起的逻辑,拆成了 DailyLoss / OverallLoss / OverallProfit 三个独立布尔方法,主入口 CheckLimits() 只负责「任一触发即记录虚拟利润快照并广播仓位变更」。这样做的好处是:单日亏损、总亏损、总利润三条线可以分别调参,不会互相耦合。 每日亏损检查里有个关键阈值——当 m_dailyDepoPart 乘子被削到小于 0.05 时直接归零,意味着该日可用保证金占比已低到不再值得开仓。外汇与贵金属杠杆高,这种硬截断能压住连续回撤下的情绪加仓冲动,但也可能在震荡市过早锁死当日交易权限。 总亏损检查逻辑相近,区别在于只有 m_overallDepoPart 归零才切到 RM_STATE_OVERALL_LOSS;而总利润达标时只是重置乘子并标记 RM_STATE_OVERALL_PROFIT,不碰保证金占用。三者返回 bool 供 CheckLimits 判断,下面是拆出来的核心片段。
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 和日损阈值按自己承受力改掉再跑。
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 策略测试器里跑一轮看回撤曲线。
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 主线的紧迫问题,这套管理器留给你在策略测试器里跑一遍验证边界。