利用指数平滑法进行时间序列预测·综合运用
- 初始化里那些防呆边界与缓冲绑定
- 预测区间与方差的计算落点
- 用鲍威尔法替掉暴力枚举
- Powell 寻优类的接口与默认收敛阈值
- Powell 法里的括号法与布伦特一维搜
- 一维投影与接口封装怎么落进 Powell 主循环
- 用鲍威尔法跑一个二维极小值验证
- 用惩罚函数绕开参数边界限制
- 指数平滑优化的回测闭环怎么写
- 目标函数里的平滑系数边界处理
- 把 USDJPY 一分钟数据丢进进化策略优化器
- 让指标自己算出最优平滑参数
- 参数初值与带惩罚的 Powell 寻优
- 四缓冲指数平滑指标的前置声明
- 缓冲区绑定与预测区绘制的四个索引
- Theta 模型的历史拟合与置信带落地
- 指数平滑不是终点
- 指数平滑与价格行为研究的底稿清单
- 记住这一条就够了
「初始化里那些防呆边界与缓冲绑定」
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 数。外汇与贵金属杠杆高,这类预测仅作概率参考,实盘须自担风险。
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 穿出带子的次数就行。
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 的头声明和常量定义,先看宏与类骨架就能明白后面搜索方向怎么控。
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 看迭代次数怎么跳。 |
|---|
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 备份进 Pcom、Xicom,以 ax=0.0、xx=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 对比收敛速度。
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 量级可能更快收敛,但精度倾向下降。
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 再编译执行,能直观对比不同起点的迭代次数差异,这是检验优化器鲁棒性的最快办法。
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 跑同一脚本就能直接比对。
<span class="comment">class=class="str">"cmt">//-----------------------------------------------------------------------------------</span> <span class="comment">class=class="str">"cmt">// OptimizeES.mqh</span> <span class="comment">class=class="str">"cmt">// class="num">2011, victorg</span> <span class="comment">class=class="str">"cmt">// [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 <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 { <span class="keyword">class="kw">protected</span>: <span class="keyword">class="type">class="kw">double</span> Dat[]; <span class="comment">class=class="str">"cmt">// Input data</span> <span class="keyword">class="type">int</span> Dlen; <span class="comment">class=class="str">"cmt">// Data lenght</span> <span class="keyword">class="type">class="kw">double</span> Par[<span class="number">class="num">5</span>]; <span class="comment">class=class="str">"cmt">// Parameters</span> <span class="keyword">class="type">int</span> NCalc; <span class="comment">class=class="str">"cmt">// Number of last elements for calculation</span> <span class="keyword">class="kw">public</span>: <span class="keyword">class="type">void</span> OptimizeES(<span class="keyword">class="type">void</span>) {} <span class="keyword">class="type">int</span> Calc(<span class="keyword">class="type">class="kw">string</span> fname); <span class="keyword">class="kw">private</span>: <span class="keyword">class="type">int</span> readCSV(<span class="keyword">class="type">class="kw">string</span> fnam,<span class="keyword">class="type">class="kw">double</span> &dat[]); <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> &p[]); }; <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,建议先肉眼核一行源文件。
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。
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)); }
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 根仅覆盖不到两个交易日,参数过拟合概率偏高,实盘前务必换更长周期验证。
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 用历史数据回测。
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 能直接看目标函数量级变化。外汇与贵金属波动剧烈,这类拟合参数过拟合风险高,上机前建议用不同品种周期交叉验证。
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 类内部写入会错位。外汇与贵金属杠杆高,这类外推仅作概率参考。
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 两个宏改成不同值,能直接看到预测线右移距离和回看长度的变化。外汇与贵金属波动剧烈,此类预测仅基于历史序列外推,实盘失效概率不低。
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 自定义指标就能在图表上画出动态通道。外汇与贵金属波动常跳脱正态假设,这套区间只作概率参考,实盘需自担高风险。
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,把转换层参数调一次,比读十篇综述都实在。