算法交易中的风险管理器(基础篇)
算法交易里的风险闸门怎么搭
在 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 倍,实盘前按品种波动重调。外汇与贵金属杠杆高,参数误设可能放大回撤,需先在策略测试器验证。
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| RiskManagerAlgo |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class</span> RiskManagerAlgo : <span class="keyword">class="kw">public</span> RiskManagerBase { <span class="keyword">class="kw">protected</span>: CSymbolInfo r_symbol; <span class="comment">class=class="str">"cmt">// instance</span> <span class="keyword">class="type">class="kw">double</span> slippfits; <span class="comment">class=class="str">"cmt">// allowable slippage per trade</span> <span class="keyword">class="type">class="kw">double</span> spreadfits; <span class="comment">class=class="str">"cmt">// allowable spread relative to the opened stop level</span> <span class="keyword">class="type">class="kw">double</span> riskPerDeal; <span class="comment">class=class="str">"cmt">// risk per trade in the deposit currency</span> <span class="keyword">class="kw">public</span>: RiskManagerAlgo(<span class="keyword">class="type">void</span>); <span class="comment">class=class="str">"cmt">// constructor</span> ~RiskManagerAlgo(<span class="keyword">class="type">void</span>); <span class="comment">class=class="str">"cmt">// destructor</span> <span class="comment">class=class="str">"cmt">//---getters</span> <span class="keyword">class="type">bool</span> GetRiskTradePermission() {<span class="keyword">class="kw">return</span> RiskTradePermission;};</span> <span class="comment">class=class="str">"cmt">//---interface implementation</span> <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span> SlippageCheck() class="kw">override; <span class="comment">class=class="str">"cmt">// checking the slippage for an open order</span> <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span> SpreadMonitor(<span class="keyword">class="type">int</span> intSL) class="kw">override; <span class="comment">class=class="str">"cmt">// spread control</span> }; <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 = <span class="number">class="num">2.0</span>; <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 = <span class="number">class="num">2.0</span>; <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 为空实现,说明该类未申请堆内存,可安全依赖编译器默认回收。外汇与贵金属杠杆高,这类风控类参数设错会放大回撤概率,上机前建议先在策略测试器用历史数据跑一遍构造注入是否生效。
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,把代码贴进新头文件,先确认编译期只报「未实现接口方法」这类错,就能验证泛型骨架没写歪。外汇和贵金属杠杆高,这类止损前置检查只在概率上降低滑点吞噬,不保证成交安全。
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 的脏数据污染本次判断。
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