开发多币种 EA 交易系统(第 14 部分):风险管理器的适应性交易量变化·进阶篇
风险管理器里的平滑减仓属性
在 CVirtualRiskManager 的 protected 段新增了几个成员,公有多余接口不动。核心是两个减仓阈值属性 m_closeDailyPart 与 m_closeOverallPart:它们分别盯每日亏损限额和总亏损限额,逻辑一致,只是作用范围不同。 若把 m_closeDailyPart 设成 0.5,当当日亏损触到限额一半时,实仓规模先砍半;若亏损继续扩大、吃掉剩下那半的一半,已减半的仓位会再减半。也就是说,损失沿限额比例走,仓位按对应比例阶梯式收缩。 m_maxRestoreTime 与 m_lastVirtualProfitFactor 管的是仓位恢复。前者是分钟数:哪怕虚拟利润已经回到非负,过了这个时间 EA 也会把真实仓位补回虚拟仓位对应的规模;在此之前,只有虚拟仓位估算利润低于某个随时间变动的估值,才允许恢复交易量。 后者是倍数:新交易日开始时,虚拟仓位亏损必须比触限那一刻深出这个倍数,才立刻复原仓位大小。比如设为 1,就意味着开盘时虚拟仓位跌幅≥触限瞬间跌幅才恢复。另外注意,触限判定不再只看回撤超额,像每日限额过半(配合 0.5 阈值)也算触发事件。外汇与贵金属杠杆高,这类自动减仓逻辑实盘前务必在 MT5 策略测试器跑通。
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| Risk management class (risk manager) |</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> <span class="keyword">class="type">bool</span> m_isActive; <span class="comment">class=class="str">"cmt">// Is the risk manager active?</span> <span class="keyword">class="type">class="kw">double</span> m_baseBalance; <span class="comment">class=class="str">"cmt">// Base balance</span> ENUM_RM_CALC_DAILY_LOSS m_calcDailyLossLimit; <span class="comment">class=class="str">"cmt">// Method of calculating the maximum daily loss</span> <span class="keyword">class="type">class="kw">double</span> m_maxDailyLossLimit; <span class="comment">class=class="str">"cmt">// Parameter of calculating the maximum daily loss</span> <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> m_closeDailyPart</span>; <span class="comment">class=class="str">"cmt">// Threshold part of the daily loss</span> ENUM_RM_CALC_OVERALL_LOSS m_calcOverallLossLimit; <span class="comment">class=class="str">"cmt">// Method of calculating the maximum overall loss</span> <span class="keyword">class="type">class="kw">double</span> m_maxOverallLossLimit; <span class="comment">class=class="str">"cmt">// Parameter of calculating the maximum overall loss</span> <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> m_closeOverallPart</span>; <span class="comment">class=class="str">"cmt">// Threshold part of the overall loss</span> <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> <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> m_maxOverallProfitLimit</span>; <span class="comment">class=class="str">"cmt">// Parameter for calculating the maximum overall profit</span> <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> m_maxRestoreTime</span>; <span class="comment">class=class="str">"cmt">// Waiting time for the best entry on a drawdown</span> <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> m_lastVirtualProfitFactor</span>; <span class="comment">class=class="str">"cmt">// Initial best drawdown multiplier</span> <span class="comment">class=class="str">"cmt">// Current state</span> ENUM_RM_STATE m_state; <span class="comment">class=class="str">"cmt">// State</span> <span class="keyword">class="type">class="kw">double</span> m_lastVirtualProfit; <span class="comment">class=class="str">"cmt">// Profit of open class="kw">virtual positions at the moment of loss limit </span> <span class="keyword">class="type">class="kw">datetime</span> m_startRestoreTime; <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> <span class="keyword">class="type">class="kw">double</span> m_balance; <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,就能在每次报价时卡日损。
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 打印出来,就能看清实际占用保证金比例是怎么被三层压缩的。外汇与贵金属杠杆高,三层乘子叠加可能让单笔风险偏离预期,需自行回测验证。
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 里可直接抄的还原逻辑。
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。 外汇与贵金属杠杆品种波动剧烈,高水位法只降低「盈利后过度冒险」的概率,不消除爆仓可能,参数切换前建议用策略测试器跑一轮多段权益曲线验证。
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(); ... }