开发多币种 EA 交易(第 12 部分):开发自营交易级别风险管理器(基础篇)
📘

开发多币种 EA 交易(第 12 部分):开发自营交易级别风险管理器(基础篇)

第 1/3 篇

自营盘级别的风险管理器从哪下手

做多币种 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 前先纸面跑一遍。

MQL5 / C++
<span class="comment">class=class="str">"cmt">// Possible risk manager states</span>
<span class="keyword">enum</span> ENUM_RM_STATE {
&nbsp;&nbsp; RM_STATE_OK,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Limits are not exceeded </span>
&nbsp;&nbsp; RM_STATE_DAILY_LOSS,&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Daily limit is exceeded</span>
&nbsp;&nbsp; RM_STATE_OVERALL_LOSS&nbsp;&nbsp; <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 {
&nbsp;&nbsp; RM_CALC_LIMIT_FIXED,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Fixed(USD)</span>
&nbsp;&nbsp; RM_CALC_LIMIT_FIXED_PERCENT,&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Fixed(% from Base Balance)</span>
&nbsp;&nbsp; RM_CALC_LIMIT_PERCENT&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <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)&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_LIMIT m_calcDailyLossLimit;&nbsp;&nbsp;<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;<span class="comment">class=class="str">"cmt">// Parameter of calculating the maximum daily loss</span>
&nbsp;&nbsp; ENUM_RM_CALC_LIMIT m_calcOverallLossLimit;<span class="comment">class=class="str">"cmt">// Method of calculating the total 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_maxOverallLossLimit;&nbsp;&nbsp;<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>
&nbsp;&nbsp; ENUM_RM_STATE&nbsp;&nbsp;&nbsp;&nbsp; m_state;
<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>
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_equity;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Current equity</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_profit;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <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() 打印一次内部状态,就能验证你的参数串有没有被正确解析。外汇与贵金属杠杆高,虚拟仓若基准线算错,真实账户可能倾向被瞬间打穿。

MQL5 / C++
  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 是否随权益重算。

MQL5 / C++
  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",

常见问题

先定义停止层级和限额枚举,再用虚拟账本记录每个币种与总账户的浮亏,最后接tick驱动更新,别直接拿历史回撤当上限。
历史回撤只覆盖已发生行情,相关性突变时多币种会同步亏损,概率上可能突破旧极值,不能当风控保证。
可以,小布能按你设的限额枚举做跨品种账本汇总,触发停止层级时直接提示,省去自己写监控。
至少记品种名、开仓均价、当前浮亏、层级标记与累计占用限额,接口留出tick更新和查询即可。
用单品种先跑tick更新账本,确认停止层级触发逻辑正确,再扩到多币种并观察总限额消耗节奏。