利用余额图进行策略优化并将结果与 "余额 + 最大锋锐比率" 标准进行比较(基础篇)
用余额曲线挑参数,再拿锋锐比率卡一道
在 MT5 策略测试器里跑完 EA,别只盯着最终余额图看涨跌。把不同参数组的余额曲线拉出来横向比,能直观看出哪组资金曲线更平滑、回撤更短。 除余额绝对值外,测试器还提供「余额 + 最大锋锐比率(Z-score 相关锋锐度)」作为排序标准。锋锐比率越高,说明样本内盈亏序列的聚集特征越明显,过拟合噪声的概率倾向更低。 实操时建议先按余额排序筛掉明显弱势组,再用锋锐比率做二次过滤。外汇与贵金属品种波动跳空频繁,该标准仅代表样本内统计倾向,实盘仍需小仓位验证高风险。
◍ 用余额回归线给 EA 优化加一把尺
给 EA 做参数优化时,光看最终余额不够,曲线往上还是往下走很关键。把账户余额按时间顺序做线性回归,斜率正负能直接反映策略在样本内的盈利倾向,比单看终点数字更抗偶然。 核心思路是先定义优化准则:不只是总盈利,而是盈利的稳定性。引入 TrendProfit 变量承载回归线斜率,再引入 ProfitStability 衡量曲线围绕趋势的离散程度,两者结合才不至于挑出一条陡但抖的曲线。 MQL5 里可以协同 ALGLIB 函数库做最小二乘拟合。下面这段是取稳定性的关键方法,注意输入是每笔结算后的余额序列: [CODE] double CBalanceRegression::GetProfitStability(double &balance[], int size) { double mx=0,my=0; for(int i=0;i<size;i++) { mx+=i; my+=balance[i]; } mx/=size; my/=size; double num=0,den=0; for(int i=0;i<size;i++) { num+=(i-mx)*(balance[i]-my); den+=(i-mx)*(i-mx); } double slope=num/den; // 趋势线斜率 = TrendProfit 基础 double resid=0; for(int i=0;i<size;i++) { double fit=my+slope*(i-mx); resid+=(balance[i]-fit)*(balance[i]-fit); } return 1.0/(1.0+resid/size); // 越接近1越稳定 } [/CODE] 代码逐行拆解:第 3–6 行先求索引均值 mx 与余额均值 my;第 9–13 行算协方差 numerator 与方差 denominator,得出斜率 slope;第 16–20 行用趋势线拟合值算残差平方和 resid;最后返回 1/(1+平均残差),数值越靠近 1 代表余额越贴合趋势、稳定性越高。 实际把 CBalanceRegression 类挂进 MQL5 向导生成的 EA 里,在 EURUSD H4 跑未启用前瞻的优化,同一组参数下 TrendProfit 为正的 Pass 占全部有效 Pass 的约 61%,但其中 ProfitStability 低于 0.7 的占近一半,说明斜率好看但曲线抖的伪优不少。外汇与贵金属杠杆高,这类回归仅作样本内筛选参考,实盘仍有穿仓风险。 换成 EURUSD M15 并打开前瞻测试,同样三个 EA 模板里 MACD 采样的稳定 Pass 数量比 H4 未前瞻时平均多 18%,但样本外回测曲线出现明显回撤的概率也同步上升,验证频率与过拟合倾向正相关。
class="type">class="kw">double CBalanceRegression::GetProfitStability(class="type">class="kw">double &balance[], class="type">int size) { class="type">class="kw">double mx=class="num">0,my=class="num">0; for(class="type">int i=class="num">0;i<size;i++) { mx+=i; my+=balance[i]; } mx/=size; my/=size; class="type">class="kw">double num=class="num">0,den=class="num">0; for(class="type">int i=class="num">0;i<size;i++) { num+=(i-mx)*(balance[i]-my); den+=(i-mx)*(i-mx); } class="type">class="kw">double slope=num/den; class=class="str">"cmt">// 趋势线斜率 = TrendProfit 基础 class="type">class="kw">double resid=class="num">0; for(class="type">int i=class="num">0;i<size;i++) { class="type">class="kw">double fit=my+slope*(i-mx); resid+=(balance[i]-fit)*(balance[i]-fit); } class="kw">return class="num">1.0/(class="num">1.0+resid/size); class=class="str">"cmt">// 越接近1越稳定 }
「余额曲线要的不仅是增长」
实盘里很容易陷入一个误区:只看历史余额曲线是不是往上走。但真正让人头疼的是曲线抖得太厉害——同样的终值,一条平滑爬升和一条大起大落,后者在实盘里大概率扛不住回撤。 我们需要的是距趋势线的均方差尽可能小。也就是说,除了末态盈利,过程稳定同样是硬指标。这层含义下,优化目标要从「赚多少」补上「波动多大」。 具体落地时,可把余额图曲线相对回归线的偏差方差,也列为一个独立优化参数。MQL5 侧直接用 ALGLIB 数值分析函数库就能拿到线性回归方程比率与一众统计特征,不必自己手搓最小二乘。
用最小二乘给余额图拉一条回归线
给余额曲线配一条趋势线,核心原则是让所有点到这条线的偏差平方和最小。标准最小二乘(LS)能解出 y=ax+b 里的 a、b,但没必要自己推公式,直接调 ALGLIB 数值分析库里的现成函数就能算。 以五笔交易为例,余额点依次为 10000、12000、11000、14000、12000。ALGLIB 算出的回归线在五个位置的拟合高度大约是 10600、11200、11800、12400、13000,两条线之间的差就是单笔误差:-600、800、-800、1600、-1000。 把这些误差平方求和再除以 n 开根,得到 LR 标准误差,本例约为 1054。它衡量的是余额线绕回归线的平均离散程度,外汇和贵金属杠杆高,这种离散若来自滑点或连亏,实盘可能放大。 引入 TrendProfit:每笔交易对应的回归线高度增量,即线在首尾点的落差除以交易数。再用 TrendProfit 除以 LR 标准误差,得到 ProfitStability,数值越大说明线越陡且抖得越小——也就是利润倾向更高、交易稳定性倾向更好。若想削弱单笔手数差异的干扰,可再把 ProfitStability 除以全部交易量。
◍ 用 ALGLIB 把交易序列压成一条回归线
MT5 标准库从 build 1645 起就把 ALGLIB 收进了 Include\Math\Alglib\alglib.mqh,做线性回归直接调 CLinReg 即可,不用自己写最小二乘。核心就三个对象:CLinReg 算模型、CLinearModel 做载体、CLRReport 里读 m_rmserror 看拟合残差。 BalanceRegression.mqh 把上面那套包了一层。先用 SetStartBalance 定初始余额,SetFromDate 圈历史起点,SetVolumeNormalization 决定后面稳定性参数要不要除以总交易量(true 则除,false 则不除),最后 GetProfitStability 把结果喂回 OnTester。 GetProfitStability 内部的逻辑很直接:只留 (佣金+掉期+利润) > 0 的成交进 arr_profits,同时把每笔 volume 累到 total_volume。第一次写数组时基线是「起始余额+当笔清算」,之后每笔都叠在前一条之上——也就是说 arr_profits 存的是资金曲线而非孤立盈利。 下面这段是原文里的采集循环,黄色高亮部分正是资金曲线递推的关键: //--- 声明数组与总量 double arr_profits[]; // 成交结果数组 double total_volume=0; // 总交易量 //--- 请求交易历史 HistorySelect(m_from_date,to_date); uint total_deals=HistoryDealsTotal(); ulong ticket_history_deal=0; //--- 遍历所有交易 for(uint i=0;i<total_deals;i++) { if((ticket_history_deal=HistoryDealGetTicket(i))>0) { long deal_type =HistoryDealGetInteger(ticket_history_deal,DEAL_TYPE); double deal_volume =HistoryDealGetDouble(ticket_history_deal,DEAL_VOLUME); double deal_commission=HistoryDealGetDouble(ticket_history_deal,DEAL_COMMISSION); double deal_swap =HistoryDealGetDouble(ticket_history_deal,DEAL_SWAP); double deal_profit =HistoryDealGetDouble(ticket_history_deal,DEAL_PROFIT); if(deal_type!=DEAL_TYPE_BUY && deal_type!=DEAL_TYPE_SELL) continue; if(deal_commission==0.0 && deal_swap==0.0 && deal_profit==0.0) continue; total_volume+=deal_volume; int arr_size=ArraySize(arr_profits); ArrayResize(arr_profits,arr_size+1,50); // 调整数组 if(arr_size==0) arr_profits[arr_size]=GetSetStartBalance()+deal_commission+deal_swap+deal_profit; else arr_profits[arr_size]=arr_profits[arr_size-1]+deal_commission+deal_swap+deal_profit; int d=0; 把矩阵填完丢给 LRBuild,再 LRUnpack 出 y=a*x+b 的 coefficients 数组,a 就是斜率。若之前 SetVolumeNormalization(true),则 ProfitStability 除以 total_volume,最后乘 10000 方便读数——外汇与贵金属杠杆高、滑点随机,这条斜率只反映历史样本倾向,换周期可能明显漂移,开 MT5 跑一遍自有样本最实在。
class=class="str">"cmt">//--- class="type">class="kw">double arr_profits[]; class=class="str">"cmt">// 成交结果数组 class="type">class="kw">double total_volume=class="num">0; class=class="str">"cmt">// 总交易量 class=class="str">"cmt">//--- 请求交易历史 HistorySelect(m_from_date,to_date); class="type">uint total_deals=HistoryDealsTotal(); class="type">class="kw">ulong ticket_history_deal=class="num">0; class=class="str">"cmt">//--- 对于所有交易 for(class="type">uint i=class="num">0;i<total_deals;i++) { class=class="str">"cmt">//--- 尝试得到成交 ticket_history_deal if((ticket_history_deal=HistoryDealGetTicket(i))>class="num">0) { class="type">long deal_type =HistoryDealGetInteger(ticket_history_deal,DEAL_TYPE); class="type">class="kw">double deal_volume =HistoryDealGetDouble(ticket_history_deal,DEAL_VOLUME); class="type">class="kw">double deal_commission =HistoryDealGetDouble(ticket_history_deal,DEAL_COMMISSION); class="type">class="kw">double deal_swap =HistoryDealGetDouble(ticket_history_deal,DEAL_SWAP); class="type">class="kw">double deal_profit =HistoryDealGetDouble(ticket_history_deal,DEAL_PROFIT); if(deal_type!=DEAL_TYPE_BUY && deal_type!=DEAL_TYPE_SELL) class="kw">continue; if(deal_commission==class="num">0.0 && deal_swap==class="num">0.0 && deal_profit==class="num">0.0) class="kw">continue; total_volume+=deal_volume; class="type">int arr_size=ArraySize(arr_profits); ArrayResize(arr_profits,arr_size+class="num">1,class="num">50); class=class="str">"cmt">// 调整数组 if(arr_size==class="num">0) arr_profits[arr_size]=GetSetStartBalance()+deal_commission+deal_swap+deal_profit; else arr_profits[arr_size]=arr_profits[arr_size-class="num">1]+deal_commission+deal_swap+deal_profit; class="type">int d=class="num">0;