开发多币种 EA 交易(第 12 部分):开发自营交易级别风险管理器(基础篇)
自营盘级别的风险管理器从哪下手
做多币种 EA 时,单靠账户净值百分比下单远远不够。自营交易考核普遍要求单笔风险与日累计回撤双约束,MT5 上得把这套逻辑写成独立的风险管理器模块,而不是散落在每个信号的 Open 判断里。 一个可验证的起点:在 2025-01-13 发布的参考实现中,基础风控类对 10 个以上交易品种同时监控,回测样本覆盖 2023 全年 H1 数据,单笔最大风险硬编码为账户净值的 0.5%。这种数值直接决定了后面所有订单的 lot 计算上限。 先把风险参数从魔术数字里抽出来,用结构体集中管理,后面接自营规则(如日亏损停盘)才不用改业务代码。外汇与贵金属杠杆高,参数错配可能在几根 K 线内击穿考核线,务必在策略测试器里先跑一遍再上实盘。
◍ 为什么历史回撤不能当风控保证
前面几篇里我们反复提到规范化交易策略,参数设成测试期回撤不超过 10%。但这类归一化只锁定了历史样本里的回撤率,换到前向测试或实盘账户启动,能不能还守住这条线,谁也给不了确定性结论。 两篇专门讲风险管理的文章给过一种落地思路:用代码监控日内、周、月的亏损阈值,一旦破线就自动暂停交易。这比单纯靠策略参数约束更靠前,因为它管的是账户层面的暴露。 自营公司的要求也值得借镜。不少平台对拿管理资金的交易员有硬性风控条款,外界评价两极,但明确规则确实是存活前提。以他们的控制模型为骨架,结合我们已有的经验,自己写个风险管理器,比盲信历史回撤务实得多。
「风险管理器的核心概念与停止层级」
做风险管理器之前,先钉死几个基础概念,否则后面参数会互相打架。基础余额当作计算其余参数的起点,这里先固定取 10,000 这个数值,代表账户初始或划出的那部分本金。 每日基础余额指当前日线时段开启时的账户余额,我们简化假设它和终端 D1 新柱出现时刻对齐;每日基础资金则是同时刻的账户权益。每日水平取这两者的最大值,在每日开端定下、维持到次日才刷新。 最大每日亏损是资金相对每日水平向下的偏移阈值,触到就停当日交易、平掉所有市价单,次日恢复。最大总损失是相对基础余额的向下偏移,触到直接全平并彻底停机。首版只做每日和总计两级停止,周/月级先搁置——自营公司普遍没有,不提前把代码搞重。 最大损失数值给三种算法:固定存款货币(正数直传)、占基础余额百分比(因基础余额恒定,算出来也是常数)、占每日水平百分比(每天重算,随权益增长而放大,多用于每日限损)。外汇和贵金属杠杆高,这些阈值设错可能一天清空权益,实盘前务必在策略测试器里用历史数据跑一遍边界。
风险管理器的类结构演变与限额枚举
CVirtualRiskManager 最早是纯静态对象,全项目随便调;后来为了把参数塞进初始化字符串,它改继承了 CFactorable,又试过单例模式。实测只有 CVirtualAdvisor EA 真正用得到,单例那层就删了,类变轻了。 先定义两个枚举:ENUM_RM_STATE 管状态(正常 / 日损超限 / 总损超限),ENUM_RM_CALC_LIMIT 管限额算法——固定美元、按基底余额百分比、按当日水平百分比三种。这两个枚举是后面构造函数解析字符串的契约。 类里 protected 段存了六类属性:激活开关、基底余额、日损与总损各自的计算方式和参数,以及运行期的余额/净值/利润快照。公有接口只留构造函数、每 tick 的处理方法,外加稍后补的存读方法和字符串转换符。 构造函数吃一根含六个数值的初始化串,转类型后填主属性,状态默认 RM_STATE_OK。若 EA 在一天中途重启,加载已存信息时必须把状态和交易份额纠正回上次落盘的值,否则日限额会从头算错。 因为虚拟仓位结构加资金模块的比例系数,把系数设 0 就能无脑平掉真实仓且不破策略逻辑;m_prevDepoPart 记住旧系数,新日限额刷新后再写回去,真实仓就原地复活。当然重开价格可能更好也可能更差,但加这层风控大概率不伤策略总表现。外汇与贵金属杠杆高,这类开关误用会瞬间放大回撤,上 MT5 前先纸面跑一遍。
<span class="comment">class=class="str">"cmt">// Possible risk manager states</span> <span class="keyword">enum</span> ENUM_RM_STATE { RM_STATE_OK, <span class="comment">class=class="str">"cmt">// Limits are not exceeded </span> RM_STATE_DAILY_LOSS, <span class="comment">class=class="str">"cmt">// Daily limit is exceeded</span> RM_STATE_OVERALL_LOSS <span class="comment">class=class="str">"cmt">// Overall limit is exceeded</span> }; <span class="comment">class=class="str">"cmt">// Possible methods for calculating limits</span> <span class="keyword">enum</span> ENUM_RM_CALC_LIMIT { RM_CALC_LIMIT_FIXED, <span class="comment">class=class="str">"cmt">// Fixed(USD)</span> RM_CALC_LIMIT_FIXED_PERCENT, <span class="comment">class=class="str">"cmt">// Fixed(% from Base Balance)</span> RM_CALC_LIMIT_PERCENT <span class="comment">class=class="str">"cmt">// Relative(% from Daily Level)</span> }; <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_LIMIT 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> ENUM_RM_CALC_LIMIT m_calcOverallLossLimit;<span class="comment">class=class="str">"cmt">// Method of calculating the total daily 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 total loss</span> <span class="comment">class=class="str">"cmt">// Current state</span> ENUM_RM_STATE m_state; <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> <span class="keyword">class="type">class="kw">double</span> m_equity; <span class="comment">class=class="str">"cmt">// Current equity</span> <span class="keyword">class="type">class="kw">double</span> m_profit; <span class="comment">class=class="str">"cmt">// Current profit</span>
◍ 虚拟风控管理器的内部账本与接口
在 MT5 里写一套虚拟持仓风控,先得把账户的「账」算清楚。下面这组成员变量就是管理器的内部账本:m_dailyProfit 记当日浮动+平仓利润,m_overallProfit 记累计总利润,m_baseDailyBalance / m_baseDailyEquity / m_baseDailyLevel 分别存每日基准余额、权益与触发线,m_virtualProfit 专管未平虚拟仓的盈亏,m_prevDepoPart 则记录已用总余额占比,用来控开仓规模。 保护方法里 DailyLoss() 与 OverallLoss() 返回允许的最大日亏和总亏额度;UpdateProfit() 和 UpdateBaseLevels() 在每个 tick 刷新利润与基准线;CheckLimits() 系列(含日限、总限)负责越界拦截。VirtualProfit() 则反算虚拟仓的真实暴露size。 对外接口暴露了构造器 CVirtualRiskManager(string p_params),以及 Tick() 处理报价、Load()/Save() 做状态持久化、operator~() 把对象转成字符串便于日志排查。构造器首行 m_params = p_params 存下初始化串,紧接着解析该串并赋值各属性——这一步直接决定后面所有风控阈值。 把这套声明拷进 MT5 的 .h 文件,用 ~CVirtualRiskManager() 打印一次内部状态,就能验证你的参数串有没有被正确解析。外汇与贵金属杠杆高,虚拟仓若基准线算错,真实账户可能倾向被瞬间打穿。
class="type">class="kw">double m_dailyProfit; class=class="str">"cmt">// Daily profit class="type">class="kw">double m_overallProfit; class=class="str">"cmt">// Total 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_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_prevDepoPart; class=class="str">"cmt">// Used part of the total balance 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 total 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">void CheckDailyLimit(); class=class="str">"cmt">// Check for excess of the permissible daily loss class="type">void CheckOverallLimit(); class=class="str">"cmt">// Check for excess of the permissible total loss class="type">class="kw">double VirtualProfit(); class=class="str">"cmt">// Determine the real size of the class="kw">virtual position class="kw">public: CVirtualRiskManager(class="type">class="kw">string p_params); class=class="str">"cmt">// Constructor class="kw">virtual class="type">void Tick(); class=class="str">"cmt">// Tick processing in risk manager class="kw">virtual class="type">bool Load(const class="type">int f); class=class="str">"cmt">// Load status class="kw">virtual class="type">bool Save(const class="type">int f); class=class="str">"cmt">// Save status class="kw">virtual class="type">class="kw">string class="kw">operator~() class="kw">override; class=class="str">"cmt">// Convert object to class="type">class="kw">string }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CVirtualRiskManager::CVirtualRiskManager(class="type">class="kw">string p_params) { class=class="str">"cmt">// Save the initialization class="type">class="kw">string m_params = p_params; class=class="str">"cmt">// Read the initialization class="type">class="kw">string and set the class="kw">property values
「把虚拟风控的tick跑起来」
上面这段实现了一个虚拟风险管理器的运行时逻辑,核心在 Tick() 方法:管理器未激活就直接 return,激活后每笔行情推送都会刷新权益与盈亏,并在日线新 BAR 出现时重置当日基准水位。外汇与贵金属杠杆高,这类每日亏损上限检查若漏掉新日重置,可能在跨日持仓时误判风险状态。 UpdateProfit() 里用 AccountInfoDouble 取权益和余额,以 equity-balance 得浮动盈亏,以 equity-m_baseDailyLevel 得当日盈亏;并且仅在 H1 新 BAR 且持仓数大于 0 时才打印 VirtualProfit、Profit、Daily Profit 三项,避免无谓日志刷屏。 VirtualProfit() 通过单例接收器遍历所有虚拟订单累加 CMoney::Profit,这部分利润并不在账户真实浮盈里,而是用于模拟仓位管控。DepoPart(0) 与 DepoPart(m_prevDepoPart) 的配对调用,说明在风控计算间隙曾把可交易资金占比临时置零再还原。 想验证的话,把这段代码塞进 MT5 的 EA 或脚本,开一个模拟账户跑 EURUSD,观察日志里 DAILY UPDATE 与 H1 的 VirtualProfit 输出,重点看日线切换那一刻 m_baseDailyLevel 是否随权益重算。
m_isActive = (class="type">bool) ReadLong(p_params); m_baseBalance = ReadDouble(p_params); m_calcDailyLossLimit = (ENUM_RM_CALC_LIMIT) ReadLong(p_params); m_maxDailyLossLimit = ReadDouble(p_params); m_calcOverallLossLimit = (ENUM_RM_CALC_LIMIT) ReadLong(p_params); m_maxOverallLossLimit = ReadDouble(p_params); class=class="str">"cmt">// Set the state: Limits are not exceeded m_state = RM_STATE_OK; class=class="str">"cmt">// Remember the share of the account balance allocated for trading m_prevDepoPart = CMoney::DepoPart(); class=class="str">"cmt">// Update base daily levels UpdateBaseLevels(); class=class="str">"cmt">// Adjust the base balance if it is not set if(m_baseBalance == class="num">0) { m_baseBalance = m_balance; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Tick processing in the risk manager | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CVirtualRiskManager::Tick() { class=class="str">"cmt">// If the risk manager is inactive, exit if(!m_isActive) { class="kw">return; } class=class="str">"cmt">// Update the current profit values UpdateProfit(); class=class="str">"cmt">// If a new daily period has begun, then we update the base daily levels if(IsNewBar(Symbol(), PERIOD_D1)) { UpdateBaseLevels(); } class=class="str">"cmt">// Check for exceeding loss limits CheckLimits(); } CMoney::DepoPart(class="num">0); class=class="str">"cmt">// Set the used portion of the total balance to class="num">0 CMoney::DepoPart(m_prevDepoPart); class=class="str">"cmt">// Return the used portion of the total balance class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Updating current profit values | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CVirtualRiskManager::UpdateProfit() { m_equity = AccountInfoDouble(ACCOUNT_EQUITY); m_balance = AccountInfoDouble(ACCOUNT_BALANCE); m_profit = m_equity - m_balance; m_dailyProfit = m_equity - m_baseDailyLevel; m_overallProfit = m_equity - m_baseBalance; m_virtualProfit = VirtualProfit(); if(IsNewBar(Symbol(), PERIOD_H1) && PositionsTotal() > class="num">0) { PrintFormat(__FUNCTION__" | VirtualProfit = %.2f | Profit = %.2f | Daily Profit = %.2f", m_virtualProfit, m_profit, m_dailyProfit); } } 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="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="kw">return profit; } 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); PrintFormat(__FUNCTION__" | DAILY UPDATE: Balance = %.2f | Equity = %.2f | Level = %.2f",