算法交易中的风险管理器(基础篇)
📘

算法交易中的风险管理器(基础篇)

第 1/3 篇

算法交易里的风险闸门怎么搭

在 MT5 上做算法交易,风险管理器不是加在策略尾部的补丁,而是贯穿下单链路的闸门。2024 年 11 月 15 日一篇针对 MetaTrader 5 的实战拆解被阅读 1289 次、收藏 2 次,说明这类底层模块的关注度并不低,但多数人卡在「如何把止损、滑点、点差控制统一进接口」而不是写个指标。 外汇与贵金属品种本身高杠杆、高波动,算法如果在开仓瞬间不对滑点和点差做拦截,回测漂亮的策略实盘可能直接被摩擦成本吃掉。把风险控制器做成独立模块,策略层只发意图,执行层再裁决是否放行,是降低耦合的常见做法。 这一节先铺开整体边界:从算法交易的子类划分,到近距离止损的接口处理,再到开仓滑点与点差的双控,后面才会落到接口实现与交易模块代码。读者现在可以打开 MT5 的 EA 模板,确认自己当前下单函数里有没有独立的风控判断分支。

「先把风险类的词汇表钉死」

做算法交易的风险控制,第一步不是写代码,而是把术语边界划清,否则后面类里的字段含义会互相打架。上一篇文章讲手动交易风险管理器时已经验证过:风险控制即使套在盈利策略上,也能在短期内把大幅回撤的概率压下来。 这里先固定几个容易混的概念。高低价是一根K线在时间段的极值;止损是反向走到限定值前平仓锁亏,开仓时算好;止盈是吃到位就走、通常卡在品种日常波动尽头。技术形态止损挂在图表结构上(分型、突破等),入场可动、止损不动;算数止损只看波动计算值,跟图形无关,此时找入场点比摆止损位置更重要。 滑点这个坑必须单独盯:按市价单开仓时,经纪商成交价可能偏离你算止损用的价格。比如算好止损100点,不稳定行情里实际开在150点,按风险管理器参数这单风险远超预期,就得考虑提前平掉避免扩大亏损。外汇和贵金属杠杆高,滑点带来的额外风险敞口可能瞬间吃掉数日利润。 还有两种风格和动量定义。日内不开隔夜仓,省去跳空和移仓费用;持仓交易是收到信号就一次性算全风险、平掉前不再开新号。动量指同框架内无回撤单向运动,价格回到起点算重启,其点数受波动、新闻、关键价位影响。

◍ 用继承把风控基类扩成算法交易子类

在 MT5 里做日内活跃交易,重复写风控逻辑既费时又容易引入新 bug。把之前写好的 RiskManagerBase 基类通过继承复用,是省开发时间最直接的办法:基类的限定与事件处理原样保留,子类只补 EA 需要的额外功能。 架构上盯四条:不写重复代码、守 SOLID、方便多团队并行、能扩展到任意策略。开闭原则保证类只扩不改基风险逻辑;单一职责让每个方法好读好改;类分文件放,别人接手不抓瞎。RiskManagerAlgo 不用 final 修饰,后续还能再继承,适配空间基本留给任何系统。 子类比基类多出几个关键件:一个 getter 从 protected 区取 RiskTradePermission 的布尔值——true 才允许策略下单,false 即便出信号也封单;一个 CSymbolInfo 实例管品种属性;slippfits 存用户设的滑点允许值,spreadfits 存相对于止损位的点差允许值,riskPerDeal 存每笔存款币种风险。日内信号多,单笔风险常等同日风险,但所有仓单风险之和不应超当日风险。 下面这段是类声明与全局输入参数的骨架,复制进 MT5 就能编译看结构。注意 inp_slippfits 和 inp_spreadfits 默认都设 2.0,代表允许滑点/点差相对止损为 2 倍,实盘前按品种波动重调。外汇与贵金属杠杆高,参数误设可能放大回撤,需先在策略测试器验证。

MQL5 / C++
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; RiskManagerAlgo&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;&nbsp;&nbsp;|</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="keyword">class</span> RiskManagerAlgo : <span class="keyword">class="kw">public</span> RiskManagerBase
&nbsp;&nbsp;{
<span class="keyword">class="kw">protected</span>:
&nbsp;&nbsp; CSymbolInfo&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; r_symbol;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// instance</span>
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;slippfits;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// allowable slippage per trade</span>
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;spreadfits;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// allowable spread relative to the opened stop level</span>
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;riskPerDeal;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// risk per trade in the deposit currency</span>
<span class="keyword">class="kw">public</span>:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; RiskManagerAlgo(<span class="keyword">class="type">void</span>);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// constructor</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;~RiskManagerAlgo(<span class="keyword">class="type">void</span>);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// destructor</span>
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//---getters</span>
&nbsp;&nbsp; <span class="keyword">class="type">bool</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;GetRiskTradePermission() {<span class="keyword">class="kw">return</span> RiskTradePermission;};</span>
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//---interface implementation</span>
&nbsp;&nbsp; <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;SlippageCheck() class="kw">override;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// checking the slippage for an open order</span>
&nbsp;&nbsp; <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;SpreadMonitor(<span class="keyword">class="type">int</span> intSL) class="kw">override;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// spread control</span>
&nbsp;&nbsp; };
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="keyword">input</span> group <span class="class="type">class="kw">string">"RiskManagerAlgoClass"</span>
<span class="keyword">input</span> <span class="keyword">class="type">class="kw">double</span> inp_slippfits&nbsp;&nbsp;&nbsp;&nbsp;= <span class="number">class="num">2.0</span>;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// inp_slippfits - allowable slippage per open deal</span>
<span class="keyword">input</span> <span class="keyword">class="type">class="kw">double</span> inp_spreadfits&nbsp;&nbsp; = <span class="number">class="num">2.0</span>;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// inp_spreadfits - allowable spread relative to the stop level to open</span>

风险控制器的构造与参数注入

在 MT5 的 EA 架构里,把单笔风险、滑点和点差约束从输入变量搬进风险管理类,是隔离交易逻辑和资金控制的第一步。下面这段 C++ 风格代码展示了 RiskManagerAlgo 类如何接管三个外部输入。 input double inp_risk_per_deal = 100; 这一行声明了每笔交易的存款币种风险额度,默认 100 单位(如美元账户即 100 美元),属于外部可调参数,改它就能直接改仓位计算基准。 第一个构造函数用赋值语句把 inp_slippfits、inp_spreadfits、inp_risk_per_deal 分别拷给成员 slippfits、spreadfits、riskPerDeal;第二个构造函数走初始化列表,但注意列表里写的是 rispPerDeal(inp_risk_per_deal),拼写与成员名 riskPerDeal 不一致,编译可能报未初始化或产生脏值,复制时务必核对。 析构函数 RiskManagerAlgo::~RiskManagerAlgo 为空实现,说明该类未申请堆内存,可安全依赖编译器默认回收。外汇与贵金属杠杆高,这类风控类参数设错会放大回撤概率,上机前建议先在策略测试器用历史数据跑一遍构造注入是否生效。

MQL5 / C++
input class="type">class="kw">double inp_risk_per_deal   = class="num">100;   class=class="str">"cmt">// inp_risk_per_deal - risk per trade in the deposit currency
class=class="str">"cmt">//--- implementation of the interface
   class="kw">virtual class="type">bool         SlippageCheck() class="kw">override;   class=class="str">"cmt">// checking the slippage for an open order
   class="kw">virtual class="type">bool         SpreadMonitor(class="type">int intSL) class="kw">override;                class=class="str">"cmt">// spread control
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 RiskManagerAlgo                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
RiskManagerAlgo::RiskManagerAlgo(class="type">void)
  {
   slippfits   = inp_slippfits;           class=class="str">"cmt">// copy slippage condition
   spreadfits  = inp_spreadfits;          class=class="str">"cmt">// copy spread condition
   riskPerDeal = inp_risk_per_deal;       class=class="str">"cmt">// copy risk per trade condition
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 RiskManagerAlgo                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
RiskManagerAlgo::RiskManagerAlgo(class="type">void):slippfits(inp_slippfits),
                                         spreadfits(inp_spreadfits),
                                         rispPerDeal(inp_risk_per_deal)
  {
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 ~RiskManagerAlgo                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
RiskManagerAlgo::~RiskManagerAlgo(class="type">void)
  {
  }

「用接口接管近距离止损的泛型设计」

在 MT5 里做风险管理模块,最怕把止损逻辑写死在某一个策略类里。MQL5 借用了 C++ 的抽象类思路,也给了更轻量的接口(interface)类型,专门用来定义「能管近距离止损」这种泛化能力,不绑定具体实现。 接口本质也是一种类,但限制很死:不能放成员变量,也没有构造和析构函数,里面声明的方法全是纯虚的。好处是表达紧凑,子类只要把方法补上就能直接用,适合当父类被风控类和交易架构同时继承。 下面这段定义了一个抽象类 CShortStopLoss 和一个接口 IShortStopLoss,二者都规定了两个必须实现的方法:SlippageCheck 查开单滑点,SpreadMonitor 按传入的 intSL 控点差。注意抽象类哪怕只带一个 virtual 函数就无法实例化,接口则连字段都不让写。 打开 MT5 的 MetaEditor,把代码贴进新头文件,先确认编译期只报「未实现接口方法」这类错,就能验证泛型骨架没写歪。外汇和贵金属杠杆高,这类止损前置检查只在概率上降低滑点吞噬,不保证成交安全。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  CShortStopLoss                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CShortStopLoss
  {
class="kw">public:
                      CShortStopLoss(class="type">void) {};       class=class="str">"cmt">// 构造函数,类内只要有一个virtual函数就会变成抽象类
 class="kw">virtual               ~CShortStopLoss(class="type">void) {};       class=class="str">"cmt">// 析构函数,同样可以是virtual
 class="kw">virtual class="type">bool          SlippageCheck()             = NULL;  class=class="str">"cmt">// 检查已开订单的滑点,纯虚函数
 class="kw">virtual class="type">bool          SpreadMonitor(class="type">int intSL)= NULL;  class=class="str">"cmt">// 点差控制,传入intSL参数,纯虚函数
  };
interface IShortStopLoss
  {
 class="kw">virtual class="type">bool   SlippageCheck();          class=class="str">"cmt">// 检查已开订单的滑点,接口中自动为纯虚
 class="kw">virtual class="type">bool   SpreadMonitor(class="type">int intSL); class=class="str">"cmt">// 点差控制,传入intSL参数,接口中自动为纯虚
  };

◍ 用 Refresh 和逐仓遍历掐掉异常滑点

在 MT5 的 EA 里做滑点控制,第一步不是算数,而是保证你读到的品种字段是最新的。CSymbolInfo 的 Refresh() 和 RefreshRates() 不一样:前者刷新该品种全部字段,后者只刷当前报价。把 Refresh() 放进每个 tick,EA 每次迭代拿到的都是终端最新状态,否则你算出来的滑点可能是上一帧的幽灵。 遍历已开仓位是靠 PositionsTotal() 拿到总数,再用 CPositionInfo 的 SelectByIndex() 按索引逐个选中。选中后先比对 symbol 是不是当前图表品种——我们只处理自己 EA 挂的品种,不跨品种乱平。 平仓价逻辑很直接:买仓用 Bid 卖平,卖仓用 Ask 买平。拿到开仓价和止损价后,用 MathAbs(开仓价-止损价) 除以 Point() 得到以点为单位的止损距离,再乘上手数和 TickValueLoss() 给出的每点价值,就是这笔仓的实际潜在亏损。外汇和贵金属杠杆高,滑点可能在瞬间把潜在亏损推过你的承受阈值,必须每个 tick 复核。 复核时加了两个条件。ProfitCur 确保只在亏损区处理:剧烈波动常让订单向盈利方向滑点,扩大止损缩小止盈,若价格回亏损区才平,避免误杀大概率延续的行情。PriceStopLoss != 0 则放过没设止损的仓——这类仓风险未限定,是否纳入由你策略定,我们单品种运行故不自动平。 若算出的亏损超出用户设定的 slippfits 偏差,就调基类 RiskManagerBase 里 CTrade 的 PositionClose(),把 ticket 传进去,返回结果存进 bool 变量 ticket_sc 做执行控制。下面这段是方法里的变量声明原貌,注意所有字段都初始化为 0,防止上一 tick 的脏数据污染本次判断。

MQL5 / C++
  r_symbol.Refresh();                                                                           class=class="str">"cmt">// update symbol data
  class="type">class="kw">double PriceClose = class="num">0,                                                                         class=class="str">"cmt">// close price for the order
         PriceStopLoss = class="num">0,                                                                      class=class="str">"cmt">// stop loss price for the order
         PriceOpen = class="num">0,                                                                           class=class="str">"cmt">// open price for the order
         LotsOrder = class="num">0,                                                                           class=class="str">"cmt">// order lot volume
         ProfitCur = class="num">0;                                                                           class=class="str">"cmt">// current order profit
  class="type">ulong  Ticket = class="num">0;                                                                              class=class="str">"cmt">// order ticket
  class="type">class="kw">string Symbl;                                                                                   class=class="str">"cmt">// symbol

常见问题

主要拦异常滑点、近距离止损自成交、单仓超限这三类的失控。先把风险词汇表钉死,再在基类里统一接管。
在控制器构造时把最大滑点、止损间距等参数直接传进去。子类只重写具体判定逻辑,不碰全局阈值。
小布可以替你跑这套风控检查,逐仓扫异常滑点并标出近距离止损冲突。打开对应品种页就能看到诊断结果。
接口接管后同一套逻辑能套不同订单类型,不必每类写一遍。Refresh 时统一遍历,减少漏判。
在 Refresh 里逐仓比对手价与挂单价差,超阈值的直接撤单重挂。外汇贵金属波动大,这类滑点高风险需常开。