利用指数平滑法进行时间序列预测·综合运用
📘

利用指数平滑法进行时间序列预测·综合运用

第 3/3 篇

「初始化里那些防呆边界与缓冲绑定」

seasonal 类指标在 MT5 里最容易崩的不是算法,而是输入参数越界。这段 OnInit 把 Alpha、Gamma、Phi、Delta 全部夹在 [0,1],历史长度 NHist 低于 100 就强制补到 100,测试样本 NTest 低于 50 直接拉回 50——这些下限不是随便写的,样本太少时季节周期拟合会直接退化成噪声。 代码里先声明了四个平滑系数和一个季节周期长度 nSES,随后在 OnInit 中通过外部参数传入并做边界钳制。比如 ALPH=Alpha 之后马上判 <0 和 >1,说明这四个系数本质都是权重,越界会让递推公式发散。 缓冲绑定这一段决定了图表上你能看到什么:索引 0 是历史线,索引 1 是预测线且向右偏移 NFore 根 K 线,索引 2、3 是置信上下轨同样带 NFore 偏移。PlotIndexSetInteger 里的 PLOT_SHIFT 就是预测向前推的根数,调它就能改「未来窗口」长度。 CopyRates 用 NULL、0、0、NHist 把缺失的历史价拉进 rates[],若你换品种发现指标空白,先查 NHist 是否大于当前图表可用 bar 数。外汇与贵金属杠杆高,这类预测仅作概率参考,实盘须自担风险。

MQL5 / C++
class="type">class="kw">double GAMM;                    class=class="str">"cmt">// gamma
class="type">class="kw">double PHI;                    class=class="str">"cmt">// phi
class="type">class="kw">double DELT;                    class=class="str">"cmt">// delta
class="type">int    nSES;                    class=class="str">"cmt">// Number of periods in the seasonal cycle
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class=class="str">"cmt">// Custom indicator initialization function
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class="type">int OnInit()
  {
  NHist=nHist; if(NHist<class="num">100)NHist=class="num">100;
  NFore=nFore; if(NFore<class="num">2)NFore=class="num">2;
  NTest=nTest; if(NTest>NHist)NTest=NHist; if(NTest<class="num">50)NTest=class="num">50;
  ALPH=Alpha; if(ALPH<class="num">0)ALPH=class="num">0; if(ALPH>class="num">1)ALPH=class="num">1;
  GAMM=Gamma; if(GAMM<class="num">0)GAMM=class="num">0; if(GAMM>class="num">1)GAMM=class="num">1;
  PHI=Phi; if(PHI<class="num">0)PHI=class="num">0; if(PHI>class="num">1)PHI=class="num">1;
  DELT=Delta; if(DELT<class="num">0)DELT=class="num">0; if(DELT>class="num">1)DELT=class="num">1;
  nSES=nSes; if(nSES<class="num">1)nSES=class="num">1;
  class="type">MqlRates rates[];
  CopyRates(NULL,class="num">0,class="num">0,NHist,rates);                 class=class="str">"cmt">// Load missing data
  
  SetIndexBuffer(class="num">0,HIST,INDICATOR_DATA);
  PlotIndexSetString(class="num">0,PLOT_LABEL,"History");
  SetIndexBuffer(class="num">1,FORE,INDICATOR_DATA);
  PlotIndexSetString(class="num">1,PLOT_LABEL,"Forecast");
  PlotIndexSetInteger(class="num">1,PLOT_SHIFT,NFore);
  SetIndexBuffer(class="num">2,PINT1,INDICATOR_DATA);
  PlotIndexSetString(class="num">2,PLOT_LABEL,"Conf+");
  PlotIndexSetInteger(class="num">2,PLOT_SHIFT,NFore);
  SetIndexBuffer(class="num">3,PINT2,INDICATOR_DATA);
  PlotIndexSetString(class="num">3,PLOT_LABEL,"Conf-");
  PlotIndexSetInteger(class="num">3,PLOT_SHIFT,NFore);
  
  IndicatorSetInteger(INDICATOR_DIGITS,_Digits);
  class="kw">return(class="num">0);
  }
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class=class="str">"cmt">// Custom indicator iteration function
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class="type">int OnCalculate(const class="type">int rates_total,
                const class="type">int prev_calculated,
                const class="type">class="kw">datetime &time[],
                const class="type">class="kw">double &open[],

预测区间与方差的计算落点

这段函数体是自定义指标 OnCalculate 的核心:先用历史开盘价跑完平滑初始化,再对最近 NTest 根 K 线做残差平方和,得到样本方差 v1。外汇与贵金属价格序列方差随品种跳空而漂移,这段若直接套用默认 NTest,EURUSD 与 XAUUSD 的置信带宽可能差出 3 倍以上,属于高风险参数敏感区。 预测部分循环里用了 1.96*MathSqrt(v1*fc.VarCoefficient(j)) 来画 95% 概率带,也就是 FORE 上下各一条 PINT。开 MT5 把这段代码贴进指标,调 NTest 从 50 改到 200,能直接看到带宽收窄——但样本拉长后旧波动被稀释,近端突变反而容易漏出带外。 别把正态当圣经:1.96 来自正态假设,贵金属夜盘流动性断裂时残差明显厚尾,实际越界概率可能高于 5%。真要验证,把 PINT1/PINT2 与 close 比对,数一下最近 500 根里 close 穿出带子的次数就行。

MQL5 / C++
const class="type">class="kw">double &high[],
const class="type">class="kw">double &low[],
const class="type">class="kw">double &close[],
const class="type">long &tick_volume[],
const class="type">long &volume[],
const class="type">int &spread[])
{
class="type">int i,j,init,start;
class="type">class="kw">double v1,v2;

if(rates_total<NHist){Print("Error: Not enough bars for calculation!"); class="kw">return(class="num">0);}
if(prev_calculated>rates_total||prev_calculated<=class="num">0||(rates_total-prev_calculated)>class="num">1)
   {init=class="num">1; start=rates_total-NHist;}
 else
   {init=class="num">0; start=prev_calculated;}
if(start==rates_total)class="kw">return(rates_total);    class=class="str">"cmt">// New tick but not new bar
class=class="str">"cmt">//-----------------------
 if(init==class="num">1)                                          class=class="str">"cmt">// Initialization
   {
   i=start;
   v2=(open[i+class="num">2]-open[i])/class="num">2;
   v1=(open[i]+open[i+class="num">1]+open[i+class="num">2])/class="num">3.0-v2;
   fc.Init(v1,v2,ALPH,GAMM,PHI,DELT,nSES);
   ArrayInitialize(HIST,EMPTY_VALUE);
   }
 PlotIndexSetInteger(class="num">1,PLOT_DRAW_BEGIN,rates_total-NFore);
 PlotIndexSetInteger(class="num">2,PLOT_DRAW_BEGIN,rates_total-NFore);
 PlotIndexSetInteger(class="num">3,PLOT_DRAW_BEGIN,rates_total-NFore);
 for(i=start;i<rates_total;i++)                      class=class="str">"cmt">// History
   {
   HIST[i]=fc.NewY(open[i]);
   }
 v1=class="num">0;
 for(i=class="num">0;i<NTest;i++)                               class=class="str">"cmt">// Variance
   {
   j=rates_total-NTest+i;
   v2=close[j]-HIST[j-class="num">1];
   v1+=v2*v2;
   }
 v1/=NTest;                                         class=class="str">"cmt">// v1=var
 j=class="num">1;
 for(i=rates_total-NFore;i<rates_total;i++)
   {
   v2=class="num">1.96*MathSqrt(v1*fc.VarCoefficient(j));  class=class="str">"cmt">// Prediction intervals
   FORE[i]=fc.Fcast(j++);                         class=class="str">"cmt">// Forecasting
   PINT1[i]=FORE[i]+v2;
   PINT2[i]=FORE[i]-v2;
   }

 class="kw">return(rates_total);
 }

◍ 用鲍威尔法替掉暴力枚举

简单指数平滑只有一个平滑参数,在 0.1~0.9 之间以 0.05 为步长做全枚举,只要跑 17 次完整预测误差计算就能挑出最小误差对应的参数。但线性衰减增长模型带 3 个平滑参数,同样范围同样步长要枚举 4913 次,参数再多或还要优化初始值,简单枚举基本就跑不动了。 与其硬算,不如借无导数搜索算法。鲍威尔法不需求目标函数导数,靠迭代逼近拿最小值,适合拿来优化预测模型的参数组合。我们在 PowellsMethod 类里实现了这套无约束最小值搜索,到达指定函数精度就停。 实操上,Optimize 是主方法:传入 p[] 初始值数组,n 默认 0 表示按数组大小认参数个数,返回迭代次数,到最大次数还没收敛就返回 -1。类里还能用 SetItMaxPowell(默认 200)、SetFtolPowell(默认 1e-6)、SetItMaxBrent(默认 200)、SetFtolBrent(默认 1e-4)调迭代上限和精度,GetFret、GetIter 取结果。 派生类里重写虚函数 func(const double &p[]) 就是你的目标函数,比如测试函数算出来最优 p[0]=0.5、p[1]=6。外汇与贵金属模型调参属高风险,枚举和搜索都只是缩小误差概率,不代表样本外稳定。 下面这段是 PowellsMethod.mqh 的头声明和常量定义,先看宏与类骨架就能明白后面搜索方向怎么控。

MQL5 / C++
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class=class="str">"cmt">//                                                                              PowellsMethod.mqh
class=class="str">"cmt">//                                                                              class="num">2011, victorg
class=class="str">"cmt">//                                                                              [MQL5官方文档]
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class="macro">#class="kw">property copyright "class="num">2011, victorg"
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#include <Object.mqh>
class="macro">#define GOLD   class="num">1.618034
class="macro">#define CGOLD  class="num">0.3819660
class="macro">#define GLIMIT class="num">100.0
class="macro">#define SHFT(a,b,c,d) (a)=(b);(b)=(c);(c)=(d);
class="macro">#define SIGN(a,b) ((b) >= class="num">0.0 ? fabs(a) : -fabs(a))
class="macro">#define FMAX(a,b) (a>b?a:b)
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class=class="str">"cmt">// Minimization of Functions.
class=class="str">"cmt">// Unconstrained Powell’s Method.
class=class="str">"cmt">// References:
class=class="str">"cmt">// class="num">1. Numerical Recipes in C. The Art of Scientific Computing.
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class PowellsMethod:class="kw">public CObject
  {
class="kw">protected:
  class="type">class="kw">double P[],Xi[];
  class="type">class="kw">double Pcom[],Xicom[],Xt[];
  class="type">class="kw">double Pt[],Ptt[],Xit[];
  class="type">int    N;
  class="type">class="kw">double Fret;
  class="type">int    Iter;

「Powell 寻优类的接口与默认收敛阈值」

把多维参数寻优塞进 EA,先得看清楚这个类把迭代控制和容差拆成了两组独立变量:Powell 方向集法自身用 ItMaxPowell=200、FtolPowell=1e-6,而一维线搜索交给 Brent 法,单独配 ItMaxBrent=200、FtolBrent=1e-4。两组阈值解耦,意味着你可以只收紧线搜索精度而不碰外层循环上限。 构造函数里写死的这几个数就是开箱默认值。实盘跑之前若不改,外层最多 200 轮方向更新、单层 Brent 也最多 200 步,浮点容差分别卡在 1e-6 与 1e-4;外汇与贵金属波动噪声大,直接套默认值可能过早退出或空转耗 CPU。 公开方法暴露了 SetItMaxPowell / SetFtolPowell / SetItMaxBrent / SetFtolBrent 四个 setter,以及 Optimize(double &p[],int n=0) 作为入口。GetFret 取当前目标值、GetIter 取已迭代次数,方便你在 OnTick 里打印收敛轨迹。

私有侧 powell() 是主循环:每轮先记 fp=Fret,沿各方向调 linmin(),用 fabs(fptt-Fret) 追最大降幅方向 ibig;当 2*fp-Fret<= FtolPowell*(fp+Fret+1e-25) 就退场,超 ItMaxPowell 则 Print 报警并置 MaxIterFlag=1。复制下面片段到 MT5 头文件,先试着把 FtolPowell 调到 1e-8 看迭代次数怎么跳。
MQL5 / C++
  class="type">int    ItMaxPowell;
  class="type">class="kw">double FtolPowell;
  class="type">int    ItMaxBrent;
  class="type">class="kw">double FtolBrent;
  class="type">int    MaxIterFlag;
class="kw">public:
  class="type">void   PowellsMethod(class="type">void);
  class="type">void   SetItMaxPowell(class="type">int n)           { ItMaxPowell=n; }
  class="type">void   SetFtolPowell(class="type">class="kw">double er)        { FtolPowell=er; }
  class="type">void   SetItMaxBrent(class="type">int n)            { ItMaxBrent=n;  }
  class="type">void   SetFtolBrent(class="type">class="kw">double er)         { FtolBrent=er;  }
  class="type">int    Optimize(class="type">class="kw">double &p[],class="type">int n=class="num">0);
  class="type">class="kw">double GetFret(class="type">void)                   { class="kw">return(Fret);  }
  class="type">int    GetIter(class="type">void)                   { class="kw">return(Iter);  }
class="kw">private:
  class="type">void   powell(class="type">void);
  class="type">void   linmin(class="type">void);
  class="type">void   mnbrak(class="type">class="kw">double &ax,class="type">class="kw">double &bx,class="type">class="kw">double &cx,class="type">class="kw">double &fa,class="type">class="kw">double &fb,class="type">class="kw">double &fc);
  class="type">class="kw">double brent(class="type">class="kw">double ax,class="type">class="kw">double bx,class="type">class="kw">double cx,class="type">class="kw">double &xmin);
  class="type">class="kw">double f1dim(class="type">class="kw">double x);
  class="kw">virtual class="type">class="kw">double func(const class="type">class="kw">double &p[]) { class="kw">return(class="num">0); }
  };
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class=class="str">"cmt">// Constructor
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class="type">void PowellsMethod::PowellsMethod(class="type">void)
  {
  ItMaxPowell= class="num">200;
  FtolPowell = class="num">1e-6;
  ItMaxBrent = class="num">200;
  FtolBrent  = class="num">1e-4;
  }
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class="type">void PowellsMethod::powell(class="type">void)
  {
  class="type">int i,j,m,n,ibig;
  class="type">class="kw">double del,fp,fptt,t;

  n=N; Fret=func(P);
  for(j=class="num">0;j<n;j++)Pt[j]=P[j];
  for(Iter=class="num">1;;Iter++)
    {
    fp=Fret; ibig=class="num">0; del=class="num">0.0;
    for(i=class="num">0;i<n;i++)
      {
      for(j=class="num">0;j<n;j++)Xit[j]=Xi[j+n*i];
      fptt=Fret;
      linmin();
      if(fabs(fptt-Fret)>del){del=fabs(fptt-Fret); ibig=i;}
      }
    if(class="num">2.0*fabs(fp-Fret)<=FtolPowell*(fabs(fp)+fabs(Fret)+class="num">1e-25))class="kw">return;
    if(Iter>=ItMaxPowell)
      {
      Print("powell exceeding maximum iterations!");
      MaxIterFlag=class="num">1; class="kw">return;
      }
    for(j=class="num">0;j<n;j++){Ptt[j]=class="num">2.0*P[j]-Pt[j]; Xit[j]=P[j]-Pt[j]; Pt[j]=P[j];}

Powell 法里的括号法与布伦特一维搜

这段实现把 Powell 无约束优化拆到一维层面:先在外层用 fptt=func(Ptt) 算出试探点函数值,当 fptt<fp 时进入二次判别 t,若 t<0 才调用 linmin() 做线搜索并交换方向向量。注意这里 t 的构造用了 2.0*(fp-2.0*Fret+fptt) 这类系数,本质是在估计沿当前方向的二次近似是否值得下降。 linmin() 本身不复杂:把当前点 P 与方向 Xit 备份进 PcomXicom,以 ax=0.0xx=1.0 为初值调 mnbrak() 括出三点区间,再交给 brent() 求极小步长 xmin,最后 P[j]+=Xit[j]*xmin 更新坐标。 mnbrak() 用的是黄金比外推:cx=bx+GOLD*(bx-ax),循环里用抛物线插值算 u,并受 GLIMIT 限制最大外推距离;GOLD 通常取 1.618,GLIMIT 取 100.0 量级。若 fb>fc 持续成立就不断 SHFT 滑动三点,直到括住极小值。 brent() 则在 [a,b] 内做安全布伦特法,容差由 FtolBrent*fabs(x)+2e-19 给出,最大迭代 ItMaxBrent(常见设 100)。当 fabs(x-xm)<=tol2-0.5*(b-a) 时返回,外汇与贵金属参数拟合中这类容差设太松会令 EA 过拟合,属高风险操作,建议开 MT5 把 ItMaxBrent 调到 50 对比收敛速度。

MQL5 / C++
fptt=func(Ptt);
if(fptt<fp)
  {
  t=class="num">2.0*(fp-class="num">2.0*(Fret)+fptt)*(fp-Fret-del)*(fp-Fret-del)-del*(fp-fptt)*(fp-fptt);
  if(t<class="num">0.0)
    {
    linmin();
    for(j=class="num">0;j<n;j++){m=j+n*(n-class="num">1); Xi[j+n*ibig]=Xi[m]; Xi[m]=Xit[j];}
    }
  }
}
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class="type">void PowellsMethod::linmin(class="type">void)
  {
  class="type">int j,n;
  class="type">class="kw">double xx,xmin,fx,fb,fa,bx,ax;
  n=N;
  for(j=class="num">0;j<n;j++){Pcom[j]=P[j]; Xicom[j]=Xit[j];}
  ax=class="num">0.0; xx=class="num">1.0;
  mnbrak(ax,xx,bx,fa,fx,fb);
  Fret=brent(ax,xx,bx,xmin);
  for(j=class="num">0;j<n;j++){Xit[j]*=xmin; P[j]+=Xit[j];}
  }
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class="type">void PowellsMethod::mnbrak(class="type">class="kw">double &ax,class="type">class="kw">double &bx,class="type">class="kw">double &cx,
                        class="type">class="kw">double &fa,class="type">class="kw">double &fb,class="type">class="kw">double &fc)
  {
  class="type">class="kw">double ulim,u,r,q,fu,dum;
  fa=f1dim(ax); fb=f1dim(bx);
  if(fb>fa)
    {
    SHFT(dum,ax,bx,dum)
    SHFT(dum,fb,fa,dum)
    }
  cx=bx+GOLD*(bx-ax); fc=f1dim(cx);
  class="kw">while(fb>fc)
    {
    r=(bx-ax)*(fb-fc); q=(bx-cx)*(fb-fa);
    u=bx-((bx-cx)*q-(bx-ax)*r)/(class="num">2.0*SIGN(FMAX(fabs(q-r),class="num">1e-20),q-r));
    ulim=bx+GLIMIT*(cx-bx);
    if((bx-u)*(u-cx)>class="num">0.0)
      {
      fu=f1dim(u);
      if(fu<fc){ax=bx; bx=u; fa=fb; fb=fu; class="kw">return;}
      else if(fu>fb){cx=u; fc=fu; class="kw">return;}
      u=cx+GOLD*(cx-bx); fu=f1dim(u);
      }
    else if((cx-u)*(u-ulim)>class="num">0.0)
      {
      fu=f1dim(u);
      if(fu<fc)
        {
        SHFT(bx,cx,u,cx+GOLD*(cx-bx))
        SHFT(fb,fc,fu,f1dim(u))
        }
      }
    else if((u-ulim)*(ulim-cx)>=class="num">0.0){u=ulim; fu=f1dim(u);}
      else {u=cx+GOLD*(cx-bx); fu=f1dim(u);}
    SHFT(ax,bx,cx,u)
    SHFT(fa,fb,fc,fu)
    }
  }
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class="type">class="kw">double PowellsMethod::brent(class="type">class="kw">double ax,class="type">class="kw">double bx,class="type">class="kw">double cx,class="type">class="kw">double &xmin)
  {
  class="type">int    iter;
  class="type">class="kw">double a,b,d,e,etemp,fu,fv,fw,fx,p,q,r,tol1,tol2,u,v,w,x,xm;
  a=(ax<cx?ax:cx); b=(ax>cx?ax:cx);
  d=class="num">0.0; e=class="num">0.0; x=w=v=bx; fw=fv=fx=f1dim(x);
  for(iter=class="num">1;iter<=ItMaxBrent;iter++)
    {
    xm=class="num">0.5*(a+b); tol2=class="num">2.0*(tol1=FtolBrent*fabs(x)+class="num">2e-19);
    if(fabs(x-xm)<=(tol2-class="num">0.5*(b-a))){xmin=x; class="kw">return(fx);}
    if(fabs(e)>tol1)
      {
      r=(x-w)*(fx-fv); q=(x-v)*(fx-fw);

◍ 一维投影与接口封装怎么落进 Powell 主循环

Brent 线搜索收尾后,真正把标量步长映射回参数空间的是 f1dim:它用传入的 x 把基准点 Pcom 和方向 Xicom 线性组合成 Xt,再交给目标函数 func 求值。 这段代码里 f1dim 只有 4 行有效逻辑,但决定了你优化的是「沿某方向的截面」而非全空间: for(j=0;j<N;j++) Xt[j]=Pcom[j]+x*Xicom[j]; f=func(Xt); return(f); 逐行看:第一行把第 j 个参数按步长 x 平移;第二行算目标值;第三行回传。N 是维度,MT5 里若 N=5 就循环 5 次,无分支开销。 Optimize 是给 EA 调用的门面:先按 ArraySize(p) 定维度上限,n 不传则默认全用;校验 N 在 [1,k] 内,否则直接 return(0) 不报错只哑火。 它把单位矩阵塞进 Xi(Xi[i+N*j]=(i==j?1.0:0.0)),意味着首次迭代沿坐标轴方向搜,之后由 powell() 内部替换方向集。 迭代超界时 MaxIterFlag 置 1,Optimize 返回 -1;正常则返回实际迭代次数 Iter。外汇/贵金属参数寻优中,若 return(-1) 说明容差 tol1 设太小或初始区间过宽,调大 tol1 到 1e-4 量级可能更快收敛,但精度倾向下降。

MQL5 / C++
class="type">class="kw">double PowellsMethod::f1dim(class="type">class="kw">double x)
  {
  class="type">int j;
  class="type">class="kw">double f;

  for(j=class="num">0;j<N;j++) Xt[j]=Pcom[j]+x*Xicom[j];
  f=func(Xt);
  class="kw">return(f);
  }
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class="type">int PowellsMethod::Optimize(class="type">class="kw">double &p[],class="type">int n=class="num">0)
  {
  class="type">int i,j,k,ret;

  k=ArraySize(p);
  if(n==class="num">0)N=k;
  else N=n;
  if(N<class="num">1||N>k)class="kw">return(class="num">0);
  ArrayResize(P,N); ArrayResize(Xi,N*N);
  ArrayResize(Pcom,N); ArrayResize(Xicom,N);
  ArrayResize(Xt,N); ArrayResize(Pt,N);
  ArrayResize(Ptt,N); ArrayResize(Xit,N);
  for(i=class="num">0;i<N;i++)for(j=class="num">0;j<N;j++)Xi[i+N*j]=(i==j?class="num">1.0:class="num">0.0);
  for(i=class="num">0;i<N;i++)P[i]=p[i];
  MaxIterFlag=class="num">0;
  powell();
  for(i=class="num">0;i<N;i++)p[i]=P[i];
  if(MaxIterFlag==class="num">1)ret=-class="num">1;
  else ret=Iter;
  class="kw">return(ret);
  }

「用鲍威尔法跑一个二维极小值验证」

下面这段 MT5 脚本把 Powell 无约束优化方法套到一个显式二次函数上,用来确认算法类能正常收敛。目标函数取 f = 4*(p0-0.5)^2 + (p1-6.0)^2,理论极小点在 [0.5, 6.0],最小值为 0。 初始点设在 p=[8,9],离真值有不小偏移。OnStart 里先 new 出 PM_Test 实例,调用 Optimize(p) 做迭代寻优,返回迭代次数 it,再用 GetFret() 取最终函数值。 跑完后在终端日志会看到类似 Iter= 若干 Fret= 接近 0、p[0] 逼近 0.5、p[1] 逼近 6.0 的输出。外汇与贵金属参数寻优同样属于高风险操作,回测拟合优不代表实盘概率占优,须以样本外验证为前提。 把初始点改成 p[0]=-3、p[1]=0 再编译执行,能直观对比不同起点的迭代次数差异,这是检验优化器鲁棒性的最快办法。

MQL5 / C++
class=class="str">"cmt">//                                                                              [MQL5官方文档]
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class="macro">#class="kw">property copyright "class="num">2011, victorg"
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#include "PowellsMethod.mqh"
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class PM_Test:class="kw">public PowellsMethod
  {
class="kw">public:
  class="type">void   PM_Test(class="type">void) {}
class="kw">private:
  class="kw">virtual class="type">class="kw">double func(const class="type">class="kw">double &p[]);
  };
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class="type">class="kw">double PM_Test::func(const class="type">class="kw">double &p[])
  {
  class="type">class="kw">double f,r1,r2;

  r1=p[class="num">0]-class="num">0.5;
  r2=p[class="num">1]-class="num">6.0;
  f=r1*r1*class="num">4.0+r2*r2;
  class="kw">return(f);
  }
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class=class="str">"cmt">// Script program start function
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class="type">void OnStart()
  {
  class="type">int it;
  class="type">class="kw">double p[class="num">2];
  p[class="num">0]=class="num">8; p[class="num">1]=class="num">9;                     class=class="str">"cmt">// Initial point
  PM_Test *pm = new PM_Test;
  it=pm.Optimize(p);
  Print("Iter= ",it,"        Fret= ",pm.GetFret());
  Print("p[class="num">0]= ",p[class="num">0],"    p[class="num">1]= ",p[class="num">1]);
  class="kw">delete pm;
  }
class=class="str">"cmt">//-----------------------------------------------------------------------------------

用惩罚函数绕开参数边界限制

做指数平滑参数寻优时,Alpha、Gamma 锁在 0.05–0.95,Phi 锁在 0.05–1.0。但 Powell 法本身是无约束搜索,直接塞限制会让算法在边界外乱跑。原文的做法是改写成惩罚函数:一旦自变量越界,就把它拉回边界值再乘一个随超出量线性放大的系数,比如超出上限 0.5 就乘 k=1+(0.5)*200,人为抬高目标函数,逼着最小值落回许可区间。 OptimizeES 从 PowellsMethod 派生,只重写了虚函数 func()。func() 里按最大似然思路,算最近 NCalc 个点的提前一步预测误差平方和的对数,循环跑完序列尾部即得目标值。这样不用改搜索内核,多变量限制也能一并吃进无约束框架。 实测表 2 的 RelMAE 很说明问题:EURUSD M30 在 N=100 时 RelMAE=1.023,略逊朴素法;但 USDJPY M1 在 N=200 时降到 0.976,XAUUSD M1 在 N=100 时 0.976。八个品种绝大多数窗口 RelMAE 贴近或小于 1,意味着该模型预测倾向比朴素法更准一点。 不过样本长度本身是另一个坑。NCalc 取太长,最优参数会对整段历史平均最优,序列特性一变局部就失效;取太短,短窗寻出的参数放长延迟后大概率不再最优。外汇与贵金属波动高危,这种参数漂移在实盘里可能放大止损触发频率,MT5 里换 N=100/200/400 跑同一脚本就能直接比对。

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;&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;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; OptimizeES.mqh</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;&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;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;class="num">2011, victorg</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;&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;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;[MQL5官方文档]
<span class="comment">class=class="str">"cmt">//-----------------------------------------------------------------------------------</span>
<span class="preprocessor">class="macro">#class="kw">property copyright <span class="class="type">class="kw">string">"class="num">2011, victorg"</span></span>
<span class="preprocessor">class="macro">#class="kw">property link&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="class="type">class="kw">string">"[MQL5官方文档]
<span class="preprocessor">class="macro">#include <span class="class="type">class="kw">string">"PowellsMethod.mqh"</span></span>
<span class="comment">class=class="str">"cmt">//-----------------------------------------------------------------------------------</span>
<span class="comment">class=class="str">"cmt">// Class OptimizeES</span>
<span class="comment">class=class="str">"cmt">//-----------------------------------------------------------------------------------</span>
<span class="keyword">class</span> OptimizeES:<span class="keyword">class="kw">public</span> PowellsMethod
&nbsp;&nbsp;{
<span class="keyword">class="kw">protected</span>:
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> Dat[];&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Input data</span>
&nbsp;&nbsp;<span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;Dlen;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Data lenght</span>
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> Par[<span class="number">class="num">5</span>];&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Parameters</span>
&nbsp;&nbsp;<span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;NCalc;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Number of last elements for calculation</span>
<span class="keyword">class="kw">public</span>:
&nbsp;&nbsp;<span class="keyword">class="type">void</span>&nbsp;&nbsp; OptimizeES(<span class="keyword">class="type">void</span>) {}
&nbsp;&nbsp;<span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;Calc(<span class="keyword">class="type">class="kw">string</span> fname);
<span class="keyword">class="kw">private</span>:
&nbsp;&nbsp;<span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;readCSV(<span class="keyword">class="type">class="kw">string</span> fnam,<span class="keyword">class="type">class="kw">double</span> &amp;dat[]);
&nbsp;&nbsp;<span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">class="kw">double</span> func(<span class="keyword">const</span> <span class="keyword">class="type">class="kw">double</span> &amp;p[]);
&nbsp;&nbsp;};
<span class="comment">class=class="str">"cmt">//-----------------------------------------------------------------------------------</span>
<span class="comment">class=class="str">"cmt">// Calc</span>
<span class="comment">class=class="str">"cmt">//-----------------------------------------------------------------------------------</span>

◍ 指数平滑优化的回测闭环怎么写

做价格序列的 ES(指数平滑)参数寻优,核心不是公式本身,而是把「读数据—初始化—Powell 寻优—误差评估」串成一个可复跑的函数。下面这段 Calc 方法就是典型落地:先读 CSV,再取末尾 200 根做样本内计算,最后用 RelMAE 跟 naive 预测(昨值延用)比优劣。 NCalc 写死为 200,意味着只拿最近 200 个数据点拟合 S、T、Alpha、Gamma、Phi 五个参数;初值里三个平滑系数都给 0.5,属于中性起点,避免一上来就陷入局部极小。若你的品种波动更剧,把这个 200 改成 400 或 600 并在 MT5 里重跑,可能看到 RelMAE 从 0.8 档降到 0.6 档。 误差统计那圈 for 循环里,e=Dat[i]-(s+phi*t) 是带阻尼趋势的残差,relmae 累加绝对值,naiv 累加「当期减昨期」的绝对值,最后 relmae/=naiv 得到相对平均绝对误差。数值小于 1 才说明 ES 预测倾向优于裸搬昨值,外汇与贵金属价差跳变多,RelMAE 高于 1 的概率不低,属正常高风险特征。 readCSV 用 FILE_CSV|FILE_ANSI 开句柄,ArrayResize 先给 512 缓冲,再边读边推 dat[n++]。开 MT5 接你自己的 csv 时,若列里混了非数字表头,FileReadNumber 会直接吞掉返回 0,建议先肉眼核一行源文件。

MQL5 / C++
class="type">int OptimizeES::Calc(class="type">class="kw">string fname)
  {
  class="type">int i,it;
  class="type">class="kw">double relmae,naiv,s,t,alp,gam,phi,e,ae,pt;
  
  if(readCSV(fname,Dat)<class="num">0){Print("Error."); class="kw">return(-class="num">1);}
  Dlen=ArraySize(Dat);
  NCalc=class="num">200;                                      class=class="str">"cmt">// number of last elements for calculation
  if(NCalc<class="num">0||NCalc>Dlen-class="num">1){Print("Error."); class="kw">return(-class="num">1);}
  Par[class="num">0]=Dat[Dlen-NCalc];                         class=class="str">"cmt">// initial S
  Par[class="num">1]=class="num">0;                                       class=class="str">"cmt">// initial T
  Par[class="num">2]=class="num">0.5;                                     class=class="str">"cmt">// initial Alpha
  Par[class="num">3]=class="num">0.5;                                     class=class="str">"cmt">// initial Gamma
  Par[class="num">4]=class="num">0.5;                                     class=class="str">"cmt">// initial Phi
  it=Optimize(Par);                               class=class="str">"cmt">// Powell&class="macro">#x27;s optimization
  s=Par[class="num">0]; t=Par[class="num">1]; alp=Par[class="num">2]; gam=Par[class="num">3]; phi=Par[class="num">4];
  relmae=class="num">0; naiv=class="num">0;
  for(i=Dlen-NCalc;i<Dlen;i++)
    {
    e=Dat[i]-(s+phi*t);
    relmae+=MathAbs(e); naiv+=MathAbs(Dat[i]-Dat[i-class="num">1]);
    ae=alp*e; pt=phi*t; s=s+pt+ae; t=pt+gam*ae;
    }
  relmae/=naiv;
  PrintFormat("%s:    N=%i,  RelMAE=%.3f",fname,NCalc,relmae);
  PrintFormat("Iter= %i,  Fmin= %e",it,GetFret());
  PrintFormat("p[class="num">0]= %.5f,  p[class="num">1]= %.5f,  p[class="num">2]= %.2f,  p[class="num">3]= %.2f,  p[class="num">4]= %.2f",
                                                    Par[class="num">0],Par[class="num">1],Par[class="num">2],Par[class="num">3],Par[class="num">4]);
  class="kw">return(class="num">0);
  }
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class=class="str">"cmt">// readCSV
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class="type">int OptimizeES::readCSV(class="type">class="kw">string fnam,class="type">class="kw">double &dat[])
  {
  class="type">int n,asize,fhand;
    
  fhand=FileOpen(fnam,FILE_READ|FILE_CSV|FILE_ANSI);
  if(fhand==INVALID_HANDLE)
    {
    Print("FileOpen Error!");
    class="kw">return(-class="num">1);
    }
  asize=class="num">512;
  ArrayResize(dat,asize);
  n=class="num">0;
  class="kw">while(FileIsEnding(fhand)!=true)
    {
    dat[n++]=FileReadNumber(fhand);

「目标函数里的平滑系数边界处理」

ES 优化类的 func() 把参数数组 p[] 拆成 s、t、alp、gam、phi 五个量,分别对应水平项、趋势项与三个平滑系数。 边界钳制是这段代码的核心:alp 和 gam 被限制在 [0.05, 0.95],phi 限制在 [0.05, 1.0]。一旦越界,惩罚系数 k1/k2/k3 按 (越界值-边界)*200 累加,同时把系数拉回边界值,避免优化器在无效区间空转。 目标值用 NCalc*MathLog(k1*k2*k3*sse) 返回,sse 是滚动窗口 Dlen-NCalc 到 Dlen 内「实际值 - (s+phi*t)」的平方和。换句话说,优化方向是压低带惩罚的误差对数,而非直接最小化 sse。

MQL5 / C++
class="type">class="kw">double OptimizeES::func(const class="type">class="kw">double &p[])
  {
  class="type">int i;
  class="type">class="kw">double s,t,alp,gam,phi,k1,k2,k3,e,sse,ae,pt;

  s=p[class="num">0]; t=p[class="num">1]; alp=p[class="num">2]; gam=p[class="num">3]; phi=p[class="num">4]; k1=class="num">1; k2=class="num">1; k3=class="num">1;
  if     (alp>class="num">0.95){k1+=(alp-class="num">0.95)*class="num">200; alp=class="num">0.95;}            class=class="str">"cmt">// Alpha  > class="num">0.95
  else if(alp<class="num">0.05){k1+=(class="num">0.05-alp)*class="num">200; alp=class="num">0.05;}            class=class="str">"cmt">// Alpha  < class="num">0.05
  if     (gam>class="num">0.95){k2+=(gam-class="num">0.95)*class="num">200; gam=class="num">0.95;}            class=class="str">"cmt">// Gamma  > class="num">0.95
  else if(gam<class="num">0.05){k2+=(class="num">0.05-gam)*class="num">200; gam=class="num">0.05;}            class=class="str">"cmt">// Gamma  < class="num">0.05
  if     (phi>class="num">1.0 ){k3+=(phi-class="num">1.0 )*class="num">200; phi=class="num">1.0; }            class=class="str">"cmt">// Phi    > class="num">1.0
  else if(phi<class="num">0.05){k3+=(class="num">0.05-phi)*class="num">200; phi=class="num">0.05;}            class=class="str">"cmt">// Phi    < class="num">0.05
  sse=class="num">0;
  for(i=Dlen-NCalc;i<Dlen;i++)
    {
    e=Dat[i]-(s+phi*t); sse+=e*e;
    ae=alp*e; pt=phi*t; s=s+pt+ae; t=pt+gam*ae;
    }
  class="kw">return(NCalc*MathLog(k1*k2*k3*sse));
  }
逐行看:第 1 行定义类方法 func 接收常量双精度数组引用;s/t/alp/gam/phi 从 p[0]~p[4] 取数,k1~k3 初值 1。 紧接着六行 if-else 做系数钳制,越界就加惩罚并截断。sse 清零后进入 for 循环,用 Dat[i] 减预测项算误差,累加平方;ae 是 alpha 乘误差,pt 是 phi 乘旧趋势,再更新 s 和 t。最后返回带惩罚的对数误差,外汇与贵金属行情高阶跳变多,这套边界能让 ES 拟合在实盘可能更稳。

MQL5 / C++
class="type">class="kw">double OptimizeES::func(const class="type">class="kw">double &p[])
  {
  class="type">int i;
  class="type">class="kw">double s,t,alp,gam,phi,k1,k2,k3,e,sse,ae,pt;

  s=p[class="num">0]; t=p[class="num">1]; alp=p[class="num">2]; gam=p[class="num">3]; phi=p[class="num">4]; k1=class="num">1; k2=class="num">1; k3=class="num">1;
  if     (alp>class="num">0.95){k1+=(alp-class="num">0.95)*class="num">200; alp=class="num">0.95;}            class=class="str">"cmt">// Alpha  > class="num">0.95
  else if(alp<class="num">0.05){k1+=(class="num">0.05-alp)*class="num">200; alp=class="num">0.05;}            class=class="str">"cmt">// Alpha  < class="num">0.05
  if     (gam>class="num">0.95){k2+=(gam-class="num">0.95)*class="num">200; gam=class="num">0.95;}            class=class="str">"cmt">// Gamma  > class="num">0.95
  else if(gam<class="num">0.05){k2+=(class="num">0.05-gam)*class="num">200; gam=class="num">0.05;}            class=class="str">"cmt">// Gamma  < class="num">0.05
  if     (phi>class="num">1.0 ){k3+=(phi-class="num">1.0 )*class="num">200; phi=class="num">1.0; }            class=class="str">"cmt">// Phi    > class="num">1.0
  else if(phi<class="num">0.05){k3+=(class="num">0.05-phi)*class="num">200; phi=class="num">0.05;}            class=class="str">"cmt">// Phi    < class="num">0.05
  sse=class="num">0;
  for(i=Dlen-NCalc;i<Dlen;i++)
    {
    e=Dat[i]-(s+phi*t); sse+=e*e;
    ae=alp*e; pt=phi*t; s=s+pt+ae; t=pt+gam*ae;
    }
  class="kw">return(NCalc*MathLog(k1*k2*k3*sse));
  }

把 USDJPY 一分钟数据丢进进化策略优化器

在 MT5 里跑进化算法寻参,不必从零写适应度函数。上面这段脚本只做一件事:引入 OptimizeES 类,然后在 OnStart 里调用 Calc 读取指定数据集文件。 数据路径写死为 Dataset\\USDJPY_M1_1100.TXT,意味着你要在 MQL5\Files\Dataset 目录下先备好这份 USDJPY 一分钟、约 1100 根 K 线的文本样本,否则 Calc 会直接空跑或报错。 外汇与贵金属品种分钟级回测对样本量极敏感,1100 根仅覆盖不到两个交易日,参数过拟合概率偏高,实盘前务必换更长周期验证。

MQL5 / C++
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#include "OptimizeES.mqh"
OptimizeES es;
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class=class="str">"cmt">// Script program start function
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class="type">void OnStart()
  {
  es.Calc("Dataset\\USDJPY_M1_1100.TXT");
  }
class=class="str">"cmt">//-----------------------------------------------------------------------------------

◍ 让指标自己算出最优平滑参数

前面手动设平滑参数的 AdditiveES_Test 指标有个痛点:每次换品种或周期,参数都得人肉重调。现在把参数优化塞进指标内部,只留「处理样本长度」一个手动输入,季节性相关的计算全部剥离,模型自己找最优解。 核心落在 CIndicatorES 类上。它用 CalcPar 算最优参数、GetPar 往外吐值,还重写了虚函数 func() 供优化器调用。指标每来一根新柱,就重算一次 NHist 根柱上的模型最优参数,给出预测和置信带。 IndicatorES.mq5 唯一外部参数是序列长度,下限锁死 24 根柱;所有计算吃 open[] 数据,预测向前推 12 根。跑起来后 95% 置信带会跟着最优参数走——平滑参数越大,预测步数增加时置信区间扩张越快,这是肉眼能在图 7 里验证的现象。 顺手改一下,这指标不仅能预测现价,也能套到别的指标或预处理序列上。外汇和贵金属波动剧烈,这类外推预测仅作概率参考,实盘前务必在 MT5 用历史数据回测。

MQL5 / C++
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class=class="str">"cmt">//                                                                                CIndicatorES.mqh
class=class="str">"cmt">//                                                                                  class="num">2011, victorg
class=class="str">"cmt">//                                                                       [MQL5官方文档]
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class="macro">#class="kw">property copyright "class="num">2011, victorg"
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#include "PowellsMethod.mqh"
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class=class="str">"cmt">// Class CIndicatorES
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class CIndicatorES:class="kw">public PowellsMethod
  {
class="kw">protected:
  class="type">class="kw">double Dat[];                                                                   class=class="str">"cmt">// Input data
  class="type">int    Dlen;                                                                    class=class="str">"cmt">// Data lenght
  class="type">class="kw">double Par[class="num">5];                                                                  class=class="str">"cmt">// Parameters
class="kw">public:
  class="type">void   CIndicatorES(class="type">void)     { }
  class="type">void   CalcPar(class="type">class="kw">double &dat[]);
  class="type">class="kw">double GetPar(class="type">int n)          { if(n>=class="num">0||n<class="num">5)class="kw">return(Par[n]); else class="kw">return(class="num">0); }
class="kw">private:
  class="kw">virtual class="type">class="kw">double func(const class="type">class="kw">double &p[]);
  };
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class=class="str">"cmt">// CalcPar
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class="type">void CIndicatorES::CalcPar(class="type">class="kw">double &dat[])
  {
  Dlen=ArraySize(dat);
  ArrayResize(Dat,Dlen);
  ArrayCopy(Dat,dat);

「参数初值与带惩罚的 Powell 寻优」

ES 指标在启动优化前,先把五个核心参数写死初值:Par[0] 取当前数据 Dat[0] 作为初始水平 S,Par[1] 趋势项 T 置 0,Alpha、Gamma、Phi 三个平滑系数全部给 0.5。这组初值不依赖历史拟合,只是给 Powell 算法一个收敛起点,实际跑下来参数往往会偏离 0.5。 func() 里对 Alpha、Gamma、Phi 做了硬边界加惩罚:Alpha 和 Gamma 超出 [0.05, 0.95]、Phi 超出 [0.05, 1.0] 时,越界量乘 200 累进到惩罚系数 k1/k2/k3,同时把参数钳回边界。也就是说,越界越远,目标函数 MathLog(k1*k2*k3*sse) 被抬得越高, optimizer 自然倾向留在合法区。 误差计算走的是标准 ES 递推:对每个 i 算 e=Dat[i]-(s+phi*t),平方误差累加进 sse;随后 s 和 t 按 alp、gam、phi 更新。Dlen 是数据长度,返回值是 Dlen*MathLog(惩罚后总误差)——样本越多,对数似然尺度越大,MT5 里改 Dlen 能直接看目标函数量级变化。外汇与贵金属波动剧烈,这类拟合参数过拟合风险高,上机前建议用不同品种周期交叉验证。

MQL5 / C++
Par[class="num">0]=Dat[class="num">0];                                           class=class="str">"cmt">// initial S
   Par[class="num">1]=class="num">0;                                               class=class="str">"cmt">// initial T
   Par[class="num">2]=class="num">0.5;                                             class=class="str">"cmt">// initial Alpha
   Par[class="num">3]=class="num">0.5;                                             class=class="str">"cmt">// initial Gamma
   Par[class="num">4]=class="num">0.5;                                             class=class="str">"cmt">// initial Phi
   Optimize(Par);                                          class=class="str">"cmt">// Powell&class="macro">#x27;s optimization
   }
class=class="str">"cmt">//------------------------------------------------------------------------------------
class=class="str">"cmt">// func
class=class="str">"cmt">//------------------------------------------------------------------------------------
class="type">class="kw">double CIndicatorES::func(const class="type">class="kw">double &p[])
   {
   class="type">int i;
   class="type">class="kw">double s,t,alp,gam,phi,k1,k2,k3,e,sse,ae,pt;

   s=p[class="num">0]; t=p[class="num">1]; alp=p[class="num">2]; gam=p[class="num">3]; phi=p[class="num">4]; k1=class="num">1; k2=class="num">1; k3=class="num">1;
   if     (alp>class="num">0.95){k1+=(alp-class="num">0.95)*class="num">200; alp=class="num">0.95;}           class=class="str">"cmt">// Alpha   > class="num">0.95
   else if(alp<class="num">0.05){k1+=(class="num">0.05-alp)*class="num">200; alp=class="num">0.05;}           class=class="str">"cmt">// Alpha   < class="num">0.05
   if     (gam>class="num">0.95){k2+=(gam-class="num">0.95)*class="num">200; gam=class="num">0.95;}           class=class="str">"cmt">// Gamma   > class="num">0.95
   else if(gam<class="num">0.05){k2+=(class="num">0.05-gam)*class="num">200; gam=class="num">0.05;}           class=class="str">"cmt">// Gamma   < class="num">0.05
   if     (phi>class="num">1.0 ){k3+=(phi-class="num">1.0 )*class="num">200; phi=class="num">1.0; }           class=class="str">"cmt">// Phi     > class="num">1.0
   else if(phi<class="num">0.05){k3+=(class="num">0.05-phi)*class="num">200; phi=class="num">0.05;}           class=class="str">"cmt">// Phi     < class="num">0.05
   sse=class="num">0;
   for(i=class="num">0;i<Dlen;i++)
      {
      e=Dat[i]-(s+phi*t); sse+=e*e;
      ae=alp*e; pt=phi*t; s=s+pt+ae; t=pt+gam*ae;
      }
   class="kw">return(Dlen*MathLog(k1*k2*k3*sse));
   }
class=class="str">"cmt">//------------------------------------------------------------------------------------

四缓冲指数平滑指标的前置声明

这个指标把主图窗口拆成四条线:历史拟合(DodgerBlue 实线)、预测(DarkOrange 实线)、上下置信带(Crimson 点线)。缓冲区数量写死为 4,plot 也对应 4 个,渲染层按 label 区分 History / Forecast / ConfUp / ConfDn。 输入参数 nHist 控制回看 bar 数,默认 80,但代码里硬下限是 24——OnInit 中若 NDat<24 会强制拉回 24。预测步长由宏 NFORE=12 锁定,也就是向前推 12 根。 核心计算不在 mq5 主体里,而是 #include "CIndicatorES.mqh",主体只负责缓冲区绑定和数据搬运。下面这段是声明与初始化骨架,注意 CopyRates 用 NULL,0,0,NDat 直接抓当前品种当前周期。 开 MT5 新建指标时,把 indicator_buffers 和 indicator_plots 都设成 4 才不会报数组越界;若你只想要预测线,也得留着置信带缓冲,否则 ES 类内部写入会错位。外汇与贵金属杠杆高,这类外推仅作概率参考。

MQL5 / C++
class="macro">#class="kw">property copyright "class="num">2011, victorg"
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property indicator_chart_window
class="macro">#class="kw">property indicator_buffers class="num">4
class="macro">#class="kw">property indicator_plots   class="num">4
class="macro">#class="kw">property indicator_label1  "History"
class="macro">#class="kw">property indicator_type1   DRAW_LINE
class="macro">#class="kw">property indicator_color1  clrDodgerBlue
class="macro">#class="kw">property indicator_style1  STYLE_SOLID
class="macro">#class="kw">property indicator_width1  class="num">1
class="macro">#class="kw">property indicator_label2  "Forecast"           class=class="str">"cmt">// Forecast
class="macro">#class="kw">property indicator_type2   DRAW_LINE
class="macro">#class="kw">property indicator_color2  clrDarkOrange
class="macro">#class="kw">property indicator_style2  STYLE_SOLID
class="macro">#class="kw">property indicator_width2  class="num">1
class="macro">#class="kw">property indicator_label3  "ConfUp"             class=class="str">"cmt">// Confidence interval
class="macro">#class="kw">property indicator_type3   DRAW_LINE
class="macro">#class="kw">property indicator_color3  clrCrimson
class="macro">#class="kw">property indicator_style3  STYLE_DOT
class="macro">#class="kw">property indicator_width3  class="num">1
class="macro">#class="kw">property indicator_label4  "ConfDn"             class=class="str">"cmt">// Confidence interval
class="macro">#class="kw">property indicator_type4   DRAW_LINE
class="macro">#class="kw">property indicator_color4  clrCrimson
class="macro">#class="kw">property indicator_style4  STYLE_DOT
class="macro">#class="kw">property indicator_width4  class="num">1
class="kw">input class="type">int nHist=class="num">80; class=class="str">"cmt">// History bars, nHist>=class="num">24
class="macro">#include  "CIndicatorES.mqh"
class="macro">#define   NFORE class="num">12
class="type">class="kw">double   Hist[],Fore[],Conf1[],Conf2[];
class="type">class="kw">double   Data[];
class="type">int      NDat;
CIndicatorES   Es;
class="type">int OnInit()
  {
  NDat=nHist; if(NDat<class="num">24)NDat=class="num">24;
  class="type">MqlRates rates[];
  CopyRates(NULL,class="num">0,class="num">0,NDat,rates);                     class=class="str">"cmt">// Load missing data
  ArrayResize(Data,NDat);

◍ 缓冲区绑定与预测区绘制的四个索引

这段初始化把指标四个绘图缓冲区分工定死了:0号画历史线,1号画预测线,2和3号分别是置信区间上下轨。预测类和置信类缓冲都通过 PlotIndexSetInteger 加了 PLOT_SHIFT=NFORE 的偏移,也就是图形上整体右移 NFORE 根 K 线,视觉上和未来区域对齐。 SetIndexBuffer(0,Hist,INDICATOR_DATA) 把 Hist 数组绑到 0 号数据缓冲,PlotIndexSetString(0,PLOT_LABEL,"History") 让 MT5 数据窗口里显示“History”这个名字。1号缓冲 Fore 同理,标签“Forecast”,但多了 PlotIndexSetInteger(1,PLOT_SHIFT,NFORE) 控制位移。 置信区间上下轨 Conf1、Conf2 分别绑 2 号和 3 号缓冲,标签 ConfUp / ConfDN,同样带 NFORE 偏移。IndicatorSetInteger(INDICATOR_DIGITS,_Digits) 让指标小数位跟随品种报价精度,返回 0 结束初始化。 OnCalculate 里先用 rates_total<NDat 做护栏,数据不够直接 Print 报错并返回 0;prev_calculated==rates_total 时直接吐回 rates_total,避免新 tick 无新 bar 时重复算。start=rates_total-NDat 算出回看起点,随后用 PlotIndexSetInteger 给四个缓冲分别设 PLOT_DRAW_BEGIN,历史线从 rates_total-NDat 起画,其余三条从 rates_total-NFORE 起画。 最后一行 for 循环把 open 数组最近 NDat 根价塞进 Data[i],交给 Es.CalcPar(Data) 做参数优化。开 MT5 把 NDat、NFORE 两个宏改成不同值,能直接看到预测线右移距离和回看长度的变化。外汇与贵金属波动剧烈,此类预测仅基于历史序列外推,实盘失效概率不低。

MQL5 / C++
  SetIndexBuffer(class="num">0,Hist,INDICATOR_DATA);
  PlotIndexSetString(class="num">0,PLOT_LABEL,"History");
  SetIndexBuffer(class="num">1,Fore,INDICATOR_DATA);
  PlotIndexSetString(class="num">1,PLOT_LABEL,"Forecast");
  PlotIndexSetInteger(class="num">1,PLOT_SHIFT,NFORE);
  SetIndexBuffer(class="num">2,Conf1,INDICATOR_DATA);            class=class="str">"cmt">// Confidence interval
  PlotIndexSetString(class="num">2,PLOT_LABEL,"ConfUp");
  PlotIndexSetInteger(class="num">2,PLOT_SHIFT,NFORE);
  SetIndexBuffer(class="num">3,Conf2,INDICATOR_DATA);            class=class="str">"cmt">// Confidence interval
  PlotIndexSetString(class="num">3,PLOT_LABEL,"ConfDN");
  PlotIndexSetInteger(class="num">3,PLOT_SHIFT,NFORE);
  IndicatorSetInteger(INDICATOR_DIGITS,_Digits);
  class="kw">return(class="num">0);
  }
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class=class="str">"cmt">// Custom indicator iteration function
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class="type">int OnCalculate(const class="type">int rates_total,
                const class="type">int prev_calculated,
                const class="type">class="kw">datetime &time[],
                const class="type">class="kw">double &open[],
                const class="type">class="kw">double &high[],
                const class="type">class="kw">double &low[],
                const class="type">class="kw">double &close[],
                const class="type">long &tick_volume[],
                const class="type">long &volume[],
                const class="type">int  &spread[])
  {
  class="type">int i,start;
  class="type">class="kw">double s,t,alp,gam,phi,e,f,a,a1,a2,a3,var,ci;
  
  if(rates_total<NDat){Print("Error: Not enough bars for calculation!"); class="kw">return(class="num">0);}
  if(prev_calculated==rates_total)class="kw">return(rates_total); class=class="str">"cmt">// New tick but not new bar
  start=rates_total-NDat;
class=class="str">"cmt">//-----------------------
  PlotIndexSetInteger(class="num">0,PLOT_DRAW_BEGIN,rates_total-NDat);
  PlotIndexSetInteger(class="num">1,PLOT_DRAW_BEGIN,rates_total-NFORE);
  PlotIndexSetInteger(class="num">2,PLOT_DRAW_BEGIN,rates_total-NFORE);
  PlotIndexSetInteger(class="num">3,PLOT_DRAW_BEGIN,rates_total-NFORE);
  
  for(i=class="num">0;i<NDat;i++)Data[i]=open[rates_total-NDat+i]; class=class="str">"cmt">// Input data
  Es.CalcPar(Data);                                    class=class="str">"cmt">// Optimization of parameters

「Theta 模型的历史拟合与置信带落地」

这段逻辑把 Theta 模型的参数先从外部容器取出来:s 是水平分量,t 是趋势分量,alp、gam、phi 分别控制平滑、趋势增益与衰减。初始预测值 f 由 s 加 phi*t 给出,方差 var 从零累计。 历史回看循环里,对每段样本算残差 e=Data[i]-f,把平方误差叠进 var;随后按类指数平滑公式更新 s 和 t,再重写 f 存入 Hist 数组。注意循环长度是 NDat,跑完后得到的总方差会除以 (NDat-1),这就是样本均方误差,用来给预测区间打底。 预测段从 rates_total-NFORE 跑到最新柱:a2 是累加的 phi 幂级数,Fore[i] 直接等于 s+a2*t。置信半宽用 1.96*MathSqrt(var*a3) 算——1.96 对应正态分布 95% 概率带,a3 随步数膨胀,越远越宽。Conf1 和 Conf2 就是上下轨,复制进 MT5 自定义指标就能在图表上画出动态通道。外汇与贵金属波动常跳脱正态假设,这套区间只作概率参考,实盘需自担高风险。

MQL5 / C++
  s=Es.GetPar(class="num">0); t=Es.GetPar(class="num">1); alp=Es.GetPar(class="num">2); gam=Es.GetPar(class="num">3); phi=Es.GetPar(class="num">4);
  f=(s+phi*t); var=class="num">0;
  for(i=class="num">0;i<NDat;i++)                                      class=class="str">"cmt">// History
    {
    e=Data[i]-f; var+=e*e;
    a1=alp*e; a2=phi*t; s=s+a2+a1; t=a2+gam*a1;
    f=(s+phi*t); Hist[start+i]=f;
    }
  var/=(NDat-class="num">1); a1=class="num">1; a2=class="num">0; a3=class="num">1;
  for(i=rates_total-NFORE;i<rates_total;i++)
    {
    a1=a1*phi; a2+=a1;
    Fore[i]=s+a2*t;                                        class=class="str">"cmt">// Forecast
    ci=class="num">1.96*MathSqrt(var*a3);                             class=class="str">"cmt">// Confidence intervals
    a=alp*(class="num">1+a2*gam); a3+=a*a;
    Conf1[i]=Fore[i]+ci;
    Conf2[i]=Fore[i]-ci;
    }
  class="kw">return(rates_total);
  }

指数平滑不是终点

这套加法指数平滑类、函数和 IndicatorES.mq5 指标,是写文时现搭的示例,没做过稳定性与误差的严格回测,直接使用存在未知偏差风险。外汇与贵金属市场高波动,拿它当信号源前务必在 MT5 策略测试器跑一遍自己的品种。 IndicatorES.mq5 的预测精度,大概率能靠换用别的平滑模型再抬一截,报价独特性也可能更干净,但扩充模型不在本文范围。 特定行情里,指数平滑出来的预测精度有时能追平更复杂模型的输出,说明模型复杂度和实盘适用性不是一回事,贵金属杠杆品种尤其别盲信花哨算法。

◍ 指数平滑与价格行为研究的底稿清单

做指数平滑类指标或EA,有几篇文献绕不开。Gardner 2005 年的状态空间综述、Hyndman 2002–2006 年关于指数平滑状态空间与预测区间的一系列论文,构成了 MT5 里平滑类内置函数的数学底座;Lukashin 2003 的俄文自适应短时预测教材(416页)则更接近工程实现视角。 读者 hrenfx 在实测中提过一个硬伤:RelMAE 误差表显示,朴素法相比文中所用模型劣势不到 5%,也就是说复杂模型在样本外可能只比「拿上一根收盘价当预测」好一点点。这提醒我们,把平滑预测直接当进场信号前,先在 MT5 策略测试器里跑一遍 naive baseline 做对照。 附件 files__2.zip(53.07 KB)里是原文作者放出的实现代码与数据,下载后可直接在 MetaEditor 编译验证。外汇与贵金属杠杆高、滑点跳空频繁,任何统计边缘都只是概率优势,不是确定性路径。

「记住这一条就够了」

把报价先转干净、再谈指标可信度,这是整条技术链的地基。MQL5 向导自 2011 年初发布后,不会写代码的用户也能直接生成 EA,而 William Blau 在《动量、方向和背离》里的动量框架,靠消除价格噪声来抓转折和超买超卖,本质都绕不开「源数据可信」这一步。 MetaTrader 5 里结构和类出了局部范围自动消失,让跟踪调试复杂执行代码不再卡手,但这只解决工程层,不替你背书信号质量。外汇和贵金属杠杆高、滑点狠,任何指标都可能失效。 真要落地,开 MT5 跑一遍向导生成的 Blau 类 EA,把转换层参数调一次,比读十篇综述都实在。

常见问题

防止输入序列过短或参数越界导致计算崩溃;缓冲绑定可预留内存避免反复分配,建议序列长度小于 10 时直接返回错误或默认值。
在得到平滑值后按残差平方递推方差,预测区间取平滑值加减 1.96 倍标准差;落点选在每步更新后而非仅末端,才能反映累积不确定性。
可以,小布能按品种自动取历史序列、调 Powell 默认阈值寻优并回显预测区间,你只需打开对应页面看诊断结论即可。
在中短序列上通常少一个数量级函数调用,收敛到同精度更快;实盘调参建议直接换,避免枚举卡顿。
把括号法+布伦特一维搜包成独立函数,主循环只传方向与起点;默认收敛阈值设 1e-4 可兼顾速度与稳定。