MQL5中的替代风险回报标准(基础篇)
「MQL5 里换个算法看风险回报」
在 MQL5 里,风险回报比不一定非得用固定的 1:2、1:3 来卡。传统 R:R 把止损和止盈都当成静态距离,实际行情里波动结构不对称,硬套倍数常会过早砍掉该拿的单。 一种替代思路是把风险回报建立在 ATR 倍数或近期波动分位上,让止损和止盈随市场呼吸。这样在黄金 15 分钟这类高波动品种上,止损不会被毛刺扫掉,止盈也更贴近真实波段。外汇与贵金属杠杆高,波动放大后爆仓概率陡增,任何参数都先用模拟盘验证。 下面这段 MQL5 片段演示了如何用 iATR 算动态止损距离,读者可直接粘进 EA 的 OnTick 里改自己的倍数。
class="type">class="kw">double atr = iATR(_Symbol, PERIOD_CURRENT, class="num">14, class="num">0); class="type">class="kw">double stopLoss = atr * class="num">1.5; class="type">class="kw">double takeProfit = atr * class="num">3.0; OrderSend(_Symbol, OP_BUY, class="num">0.1, Ask, class="num">3, Ask - stopLoss, Bid + takeProfit, "ATR_RR", class="num">0, class="num">0, clrGreen);
为什么夏普比率不够用
做交易的人都想让回报最大化,但回报一高,回撤和爆仓的概率往往同步放大。业内真正拿来横向比业绩的,是风险调整后的收益,而不是裸收益率。 夏普比率是这类指标里出镜率最高的一个,但它默认收益服从正态分布、波动恒定,这套前提在外汇和贵金属这种跳空频发、厚尾明显的品种上站不住脚。 正因为夏普的假设太理想化,后来才冒出一批替代指标,目标很直接:保留夏普那种跨策略可比的通用性,同时不踩正态分布的坑。 本文这一节先交代背景,后续会直接给出这些替代指标在 MT5 里的实现代码,并用假设权益曲线跑出它们的特征差异,方便你本地复现验证。外汇与贵金属属高风险品种,任何指标都只是概率参考,不构成方向判断。
◍ 用随机数造一条可复现的权益曲线
想看清权益曲线的统计特性,最干净的办法不是拿真实策略去跑,而是用随机数直接生成。我们以标普500的交易日历为时间轴,初始资金设成可配置金额(比如 10000),每一根 K 线对应的收益用一个带固定种子的随机数决定。 种子固定是关键:同样种子下,任何人重跑代码得到的权益曲线和收益序列逐点一致,排除了样本偶然性带来的误读。 这套模拟不涉及任何入场出场规则,纯粹是权益路径的蒙特卡洛式构造,用来给后续回测框架当基准参照。外汇与贵金属实盘受杠杆和滑点影响,随机路径仅作方法演示,实盘风险显著更高。
「用随机分量画出可比权益曲线」
在 MT5 里把一个 EA 跑起来,它能同时画出三条权益曲线:红色是基准,蓝、绿两条是由基准派生出的假设曲线。基准本身可通过配置初始资本改变,所以红线的形状先由你给的本金和标的收益决定。 派生曲线不是凭空画的。每条都带一个随机分量,由两个可调常数控制——平均系数和标准偏差系数。它们会和基准收益的标准差结合,决定生成正态分布随机数的参数,从而长出蓝、绿两条看起来「像但又不同」的权益轨迹。 原文用的样本区间是 2020 年 3 月 20 日到 2022 年 1 月 5 日(含)的标普 500 收盘价,先算收益序列,再累乘成权益。想验证的话,直接拿这段历史跑 EA,调平均系数和标准偏差系数,就能看到假设曲线相对红线的离散程度怎么变。外汇与贵金属品种波动结构不同,实盘映射前需意识到这类随机模拟的高风险属性。
用最大回撤看策略的风险底色
回撤衡量的是策略在任意两个时间点之间净值下滑的最大幅度,它直接暴露了为换取收益所承担的真实风险。 如果把多段历史中的最高回撤值汇总成序列再做统计,就能得到一种变异性指标——回撤分布越分散,策略在不同行情阶段的稳定性越差。 打开 MT5 的策略测试器,跑完回测后看『净值图』里最深的峰谷落差,那个数值就是本段所说的最大回撤,可据此判断自己能否承受对应级别的浮亏。外汇与贵金属杠杆高,回撤放大可能触发强平,验证前先用小资金或模拟盘。
◍ 用回撤平方和替代波动的 Burke 比率
- 年 Burke 在《更锋利的夏普比率》里提出用 Burke 比率替代夏普比率:把夏普分母里的波动项换成若干最高绝对回撤的平方和,分子可用净利润或平均收益。两种口径分别叫净利润 Burke 与平均收益 Burke,对外汇与贵金属策略评价更看重极端回撤而非日常抖动,但这类指标仍只反映历史样本,实盘存在滑点与时序断裂的高风险。
净利润口径的函数默认回撤取数为 N/20(N 为净值点数);若传入 0 则自动按此计算。下面这段 MQL5 是 netprofit_burke 与 MaxNDrawdowns 的实现核心,可直接拷进 MT5 脚本跑净值数组验证。 代码逐行看:netprofit_burke 先取数组大小,n_highestdrawdowns<=0 时设为 insize/20;调用 MaxNDrawdowns 拿到升序回撤序列 outdd,对每项平方累加得 sumdd;返回 (末值-初值)/sqrt((1/N)*sumdd),即净利润除以回撤平方均值开根。MaxNDrawdowns 用双重循环算所有 i<j 的 in_ec[i]-in_ec[j] 存 v,排序后取正数前 num_drawdowns 个,未取到返回 false。 把一段 EA 实盘净值 csv 读进 double 数组传给 netprofit_burke,对比同段净值的 Sharpe,若 Burke 明显更高,说明回撤集中可控、日常波动大但没真亏透,这类曲线在贵金属隔夜单里可能出现。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|Net profit based Burke ratio | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double netprofit_burke(class="type">class="kw">double &in_ec[],class="type">int n_highestdrawdowns=class="num">0) { class="type">class="kw">double outdd[]; class="type">class="kw">double sumdd=class="num">0; class="type">int insize=ArraySize(in_ec); if(n_highestdrawdowns<=class="num">0) n_highestdrawdowns=class="type">int(insize/class="num">20); if(MaxNDrawdowns(n_highestdrawdowns,in_ec,outdd)) { for(class="type">int i=class="num">0; i<ArraySize(outdd); i++) { sumdd+=(outdd[i]*outdd[i]); } class="kw">return (in_ec[insize-class="num">1]-in_ec[class="num">0])/(MathSqrt((class="num">1.0/class="type">class="kw">double(insize)) * sumdd)); } else class="kw">return class="num">0; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|Maximum drawdowns function given equity curve | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool MaxNDrawdowns(const class="type">int num_drawdowns,class="type">class="kw">double &in_ec[],class="type">class="kw">double &out_dd[]) { ZeroMemory(out_dd); ResetLastError(); if(num_drawdowns<=class="num">0) { Print("Invalid function parameter for num_drawdowns "); class="kw">return class="kw">false; } class="type">class="kw">double u[],v[]; class="type">int size = ArraySize(in_ec); if((ArrayResize(v,(size*(size-class="num">1))/class="num">2)< class="type">int((size*(size-class="num">1))/class="num">2))|| (ArraySize(out_dd)!=num_drawdowns && ArrayResize(out_dd,num_drawdowns)<num_drawdowns)) { Print(__FUNCTION__, " resize error ", GetLastError()); class="kw">return class="kw">false; } class="type">int k=class="num">0; for(class="type">int i=class="num">0; i<size-class="num">1; i++) { for(class="type">int j=i+class="num">1; j<size; j++) { v[k]=in_ec[i]-in_ec[j]; k++; } } ArraySort(v); for(class="type">int i=class="num">0; i<k; i++) { if(v[i]>class="num">0) { if(i) {