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

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

第 2/3 篇

风险管理器里的平滑减仓属性

在 CVirtualRiskManager 的 protected 段新增了几个成员,公有多余接口不动。核心是两个减仓阈值属性 m_closeDailyPart 与 m_closeOverallPart:它们分别盯每日亏损限额和总亏损限额,逻辑一致,只是作用范围不同。 若把 m_closeDailyPart 设成 0.5,当当日亏损触到限额一半时,实仓规模先砍半;若亏损继续扩大、吃掉剩下那半的一半,已减半的仓位会再减半。也就是说,损失沿限额比例走,仓位按对应比例阶梯式收缩。 m_maxRestoreTime 与 m_lastVirtualProfitFactor 管的是仓位恢复。前者是分钟数:哪怕虚拟利润已经回到非负,过了这个时间 EA 也会把真实仓位补回虚拟仓位对应的规模;在此之前,只有虚拟仓位估算利润低于某个随时间变动的估值,才允许恢复交易量。 后者是倍数:新交易日开始时,虚拟仓位亏损必须比触限那一刻深出这个倍数,才立刻复原仓位大小。比如设为 1,就意味着开盘时虚拟仓位跌幅≥触限瞬间跌幅才恢复。另外注意,触限判定不再只看回撤超额,像每日限额过半(配合 0.5 阈值)也算触发事件。外汇与贵金属杠杆高,这类自动减仓逻辑实盘前务必在 MT5 策略测试器跑通。

MQL5 / C++
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| Risk management class (risk manager)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;   |</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="keyword">class</span> CVirtualRiskManager : <span class="keyword">class="kw">public</span> CFactorable {
<span class="keyword">class="kw">protected</span>:
<span class="comment">class=class="str">"cmt">// Main constructor parameters</span>
&nbsp;&nbsp; <span class="keyword">class="type">bool</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_isActive;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Is the risk manager active?</span>
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_baseBalance;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Base balance</span>
&nbsp;&nbsp; ENUM_RM_CALC_DAILY_LOSS&nbsp;&nbsp; m_calcDailyLossLimit; <span class="comment">class=class="str">"cmt">// Method of calculating the maximum daily loss</span>
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_maxDailyLossLimit;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Parameter of calculating the maximum daily loss</span>
&nbsp;&nbsp; <span style="background-class="type">color:rgb(class="num">216, class="num">232, class="num">194);"><span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_closeDailyPart</span>;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Threshold part of the daily loss</span>
&nbsp;&nbsp; ENUM_RM_CALC_OVERALL_LOSS m_calcOverallLossLimit;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Method of calculating the maximum overall loss</span>
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_maxOverallLossLimit;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Parameter of calculating the maximum overall loss</span>
&nbsp;&nbsp; <span style="background-class="type">color:rgb(class="num">216, class="num">232, class="num">194);"><span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_closeOverallPart</span>;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Threshold part of the overall loss</span>
&nbsp;&nbsp; <span style="background-class="type">color:rgb(class="num">216, class="num">232, class="num">194);">ENUM_RM_CALC_OVERALL_PROFIT m_calcOverallProfitLimit</span>; <span class="comment">class=class="str">"cmt">// Method for calculating maximum overall profit</span>
&nbsp;&nbsp; <span style="background-class="type">color:rgb(class="num">216, class="num">232, class="num">194);"><span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_maxOverallProfitLimit</span>;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Parameter for calculating the maximum overall profit</span>
&nbsp;&nbsp; <span style="background-class="type">color:rgb(class="num">216, class="num">232, class="num">194);"><span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_maxRestoreTime</span>;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Waiting time for the best entry on a drawdown</span>
&nbsp;&nbsp; <span style="background-class="type">color:rgb(class="num">216, class="num">232, class="num">194);"><span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_lastVirtualProfitFactor</span>;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Initial best drawdown multiplier</span>
<span class="comment">class=class="str">"cmt">// Current state</span>
&nbsp;&nbsp; ENUM_RM_STATE&nbsp;&nbsp;&nbsp;&nbsp; m_state;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// State</span>
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_lastVirtualProfit;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Profit of open class="kw">virtual positions at the moment of loss limit </span>
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">datetime</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_startRestoreTime;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Start time of restoring the size of open positions</span>
<span class="comment">class=class="str">"cmt">// Updated values</span>
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_balance;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Current balance</span>

◍ 风控类的核心变量与受保护方法

在 MT5 自建资金管理模块时,先把权益、利润和基准水位这几类 double 变量铺好,后续限亏逻辑才有抓手。下面这段代码里标灰的字段,是日损/总损动态缩放仓位时真正被改写的部分,其余多是只读快照。 double m_equity; // 当前权益 double m_profit; // 当前利润 double m_dailyProfit; // 当日利润 double m_overallProfit; // 累计利润 double m_baseDailyBalance; // 每日基础余额 double m_baseDailyEquity; // 每日基础权益 double m_baseDailyLevel; // 每日基础水位 double m_baseHWBalance; // 余额高点 watermark double m_baseHWEquityBalance; // 权益或余额高点 watermark double m_virtualProfit; // 未平虚拟单利润 // 管理开仓规模 double m_baseDepoPart; // 占用原始余额比例 double m_dailyDepoPart; // 按日损缩放的占用比例乘数 double m_overallDepoPart; // 按总损缩放的占用比例乘数 // 受保护方法 double DailyLoss(); // 最大日损 double OverallLoss(); // 最大总损 void UpdateProfit(); // 刷新当前利润值 void UpdateBaseLevels(); // 刷新每日基础水位 void CheckLimits(); // 检查是否超可接受亏损 bool CheckDailyLossLimit(); // 检查是否破日损限 bool CheckOverallLossLimit(); // 检查是否破总损限 bool CheckOverallProfitLimit(); // 检查是否达目标利润 注意 m_baseHWBalance 与 m_baseHWEquityBalance 这两个高点水位:实盘里若权益回撤后反弹但未过前高,仓位乘数倾向维持收缩,不会自动放大。外汇与贵金属杠杆高,这类 watermark 若漏更新,可能让系统在连续小亏后误判可用风险预算。 开 MT5 按上述结构建个空类,只声明不实现,编译通过即说明变量命名未与标准库冲突;之后再把 CheckDailyLossLimit 接进 OnTick,就能在每次报价时卡日损。

MQL5 / C++
class="type">class="kw">double m_equity;            class=class="str">"cmt">// Current equity
class="type">class="kw">double m_profit;            class=class="str">"cmt">// Current profit
class="type">class="kw">double m_dailyProfit;       class=class="str">"cmt">// Daily profit
class="type">class="kw">double m_overallProfit;     class=class="str">"cmt">// Overall profit
class="type">class="kw">double m_baseDailyBalance;  class=class="str">"cmt">// Daily basic balance
class="type">class="kw">double m_baseDailyEquity;   class=class="str">"cmt">// Daily base balance
class="type">class="kw">double m_baseDailyLevel;    class=class="str">"cmt">// Daily base level
class="type">class="kw">double m_baseHWBalance;     class=class="str">"cmt">// balance High Watermark
class="type">class="kw">double m_baseHWEquityBalance; class=class="str">"cmt">// equity or balance High Watermark
class="type">class="kw">double m_virtualProfit;     class=class="str">"cmt">// Profit of open class="kw">virtual positions
class=class="str">"cmt">// Managing the size of open positions
class="type">class="kw">double m_baseDepoPart;      class=class="str">"cmt">// Used(original) part of the overall balance
class="type">class="kw">double m_dailyDepoPart;     class=class="str">"cmt">// Multiplier of the used part of the overall balance by daily loss
class="type">class="kw">double m_overallDepoPart;   class=class="str">"cmt">// Multiplier of the used part of the overall balance by overall loss
class=class="str">"cmt">// Protected methods
class="type">class="kw">double DailyLoss();         class=class="str">"cmt">// Maximum daily loss
class="type">class="kw">double OverallLoss();       class=class="str">"cmt">// Maximum overall loss
class="type">void UpdateProfit();        class=class="str">"cmt">// Update current profit values
class="type">void UpdateBaseLevels();    class=class="str">"cmt">// Updating daily base levels
class="type">void CheckLimits();         class=class="str">"cmt">// Check for excess of permissible losses
class="type">bool CheckDailyLossLimit(); class=class="str">"cmt">// Check for excess of the permissible daily loss
class="type">bool CheckOverallLossLimit(); class=class="str">"cmt">// Check for excess of the permissible overall loss
class="type">bool CheckOverallProfitLimit(); class=class="str">"cmt">// Check if the specified profit has been achieved

「仓位恢复与虚拟利润的函数骨架」

这套虚拟风控管理器把「恢复仓位尺寸」和「统计虚拟持仓利润」拆成了几个独立函数,方便在 EA 主循环里分批调用。 CheckRestore() 无返回值,只判断当前已开仓是否需要把尺寸恢复回基准;CheckDailyRestore() 与 CheckOverallRestore() 返回 bool,分别管日内乘子和全局乘子是否复位。 VirtualProfit() 返回 double,用来算当前虚拟持仓的浮动利润;RestoreVirtualProfit() 则是算「待恢复」那部分虚拟仓的利润,两者区分开才能避免恢复逻辑误触发。 SetDepoPart() 的实现只有一行:把基础、日内、全局三部分乘子相乘后喂给 CMoney::DepoPart()。在 MT5 里把 m_baseDepoPart、m_dailyDepoPart、m_overallDepoPart 打印出来,就能看清实际占用保证金比例是怎么被三层压缩的。外汇与贵金属杠杆高,三层乘子叠加可能让单笔风险偏离预期,需自行回测验证。

MQL5 / C++
class="type">void CheckRestore(); class=class="str">"cmt">// Check the need for restoring the size of open positions
class="type">bool CheckDailyRestore(); class=class="str">"cmt">// Check if the daily multiplier needs to be restored
class="type">bool CheckOverallRestore(); class=class="str">"cmt">// Check if the overall multiplier needs to be restored
class="type">class="kw">double VirtualProfit(); class=class="str">"cmt">// Determine the profit of open class="kw">virtual positions
class="type">class="kw">double RestoreVirtualProfit(); class=class="str">"cmt">// Determine the profit of open class="kw">virtual positions to restore
class="type">void SetDepoPart(); class=class="str">"cmt">// Set the values of the used part of the overall balance
class="kw">public:
  ...
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Set the value of the used part of the overall balance            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CVirtualRiskManager::SetDepoPart() {
  CMoney::DepoPart(m_baseDepoPart * m_dailyDepoPart * m_overallDepoPart);
}

还原初始仓位规模算虚拟浮动盈亏

做部分平仓再恢复未平仓位时,真正要盯的是:如果策略原本开的那批仓位一直没平,此刻浮动盈亏该是多少。风险管理器里因此加了 VirtualProfit(),它不依赖实际已缩小的仓,而是反推虚拟仓位的近似利润。 这套计算没算佣金和库存费,和券商真实数值不会完全一致,但对判断回撤阈值够用了。外汇和贵金属杠杆高,这类估算只能当参考,不能当成账户真实权益。 坑在 CMoney::Profit() 内部会乘上余额使用乘数 DepoPart()。一旦你之前调小了乘数,算出来的就只是缩仓后的利润,不是我们想看的初始仓位利润。 办法是在算之前把乘数临时复位到初始值 m_baseDepoPart,跑完虚拟利润后再调回当前 SetDepoPart()。下面这段就是 MT5 里可直接抄的还原逻辑。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Determine the profit of open class="kw">virtual positions                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CVirtualRiskManager::VirtualProfit() {
class=class="str">"cmt">// Access the receiver object
  CVirtualReceiver *m_receiver = CVirtualReceiver::Instance();
class=class="str">"cmt">// Set the initial balance usage multiplier
  CMoney::DepoPart(m_baseDepoPart);
  class="type">class="kw">double profit = class="num">0;
class=class="str">"cmt">// Find the profit sum for all class="kw">virtual positions
  FORI(m_receiver.OrdersTotal(), profit += CMoney::Profit(m_receiver.Order(i)));
class=class="str">"cmt">// Restore the current balance usage multiplier
  SetDepoPart();
  class="kw">return profit;
}

◍ 用高水位线框住最大回撤

做风控类 EA 时,只盯初始余额来算最大可亏额度有个坑:账户新高后,你的止损空间还卡在起跑线上,等于白白浪费了盈利带来的缓冲。把高水位(High Watermark)接进来,就能让风险限额跟着账户峰值走。 我们在虚拟风险管理器里加了两个持续刷新的字段:m_baseHWBalance 记录余额历史最高,m_baseHWEquityBalance 记录余额与净值二者取高的历史峰值。每次 UpdateProfit() 被调用,都用 MathMax 把当前值和新高比一遍,不会回退。 整体利润的计算基准因此可变。若模式设为相对余额高水位(RM_CALC_OVERALL_LOSS_MONEY_HW_BAL 等),m_overallProfit 改写为 equity 减 m_baseHWBalance;若设为相对余额或净值高水位,则减 m_baseHWEquityBalance。这样在 MT5 里跑,净值从 10k 涨到 12k 又回撤到 11k 时,允许亏损额度按 12k 基准算,而非死守 10k。 外汇与贵金属杠杆品种波动剧烈,高水位法只降低「盈利后过度冒险」的概率,不消除爆仓可能,参数切换前建议用策略测试器跑一轮多段权益曲线验证。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Updating current profit values                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CVirtualRiskManager::UpdateProfit() {
class=class="str">"cmt">// Current equity
  m_equity = AccountInfoDouble(ACCOUNT_EQUITY);
class=class="str">"cmt">// Current balance
  m_balance = AccountInfoDouble(ACCOUNT_BALANCE);
class=class="str">"cmt">// Maximum balance(High Watermark)
  m_baseHWBalance = MathMax(m_balance, m_baseHWBalance);
class=class="str">"cmt">// Maximum balance or equity(High Watermark)
  m_baseHWEquityBalance = MathMax(m_equity, MathMax(m_balance, m_baseHWEquityBalance));
class=class="str">"cmt">// Current profit
  m_profit = m_equity - m_balance;
class=class="str">"cmt">// Current daily profit relative to the daily level
  m_dailyProfit = m_equity - m_baseDailyLevel;
class=class="str">"cmt">// Current overall profit relative to base balance
  m_overallProfit = m_equity - m_baseBalance;
class=class="str">"cmt">// If we take the overall profit relative to the highest balance,
  if(m_calcOverallLossLimit       == RM_CALC_OVERALL_LOSS_MONEY_HW_BAL
     || m_calcOverallLossLimit == RM_CALC_OVERALL_LOSS_PERCENT_HW_BAL) {
    class=class="str">"cmt">// Recalculate it
    m_overallProfit = m_equity - m_baseHWBalance;
  }
class=class="str">"cmt">// If we take the overall profit relative to the highest balance or equity,
  if(m_calcOverallLossLimit       == RM_CALC_OVERALL_LOSS_MONEY_HW_EQ_BAL
     || m_calcOverallLossLimit == RM_CALC_OVERALL_LOSS_PERCENT_HW_EQ_BAL) {
    class=class="str">"cmt">// Recalculate it
    m_overallProfit = m_equity - m_baseHWEquityBalance;
  }
class=class="str">"cmt">// Current profit of class="kw">virtual open positions
  m_virtualProfit = VirtualProfit();
  ...
}

常见问题

没有统一值,先用历史最大连亏除以账户净值试 0.3~0.5 的系数,再在模拟盘跑两周看胜率衰减。
会。受保护方法必须只在管理器内部调用,外部币种只能读不可写,否则回撤会跨品种传染。
可以。小布能按你设的高水位线阈值做跨品种回撤预警,并把虚拟浮动盈亏异常直接推到看盘页。
虚拟利润用的是建仓时基准规模而非实时仓,差的是滑点和手续费,属正常偏差。
建议组合层统一设一条,单币种再留 10%~20% 余量,避免某品种暴涨掩盖整体回撤。