使用指数平滑法进行时间序列预测(续)(基础篇)
◍ EMA 预测残差里的价格行为信号
指数平滑本质是拿历史序列做加权递推,越近的 tick 权重越高。在 MT5 里跑一遍基础 EMA 预测,残差(实际值减预测值)的分布往往不是理想白噪声,而是带波动聚集。 Victor 在 2014-01-09 发布的该指标示例,历史浏览 3532、评论 8,说明这类轻量预测工具在 MT5 社区长期有低频但真实的需求。 把残差当偏离度看:残差持续为正且扩大,价格倾向强于平滑趋势;残差为负的深谷,往往是回调或反转的概率区。外汇与贵金属杠杆高,残差信号仅作参考,实盘须自担风险。
「为什么只盯前几步预测」
前一篇文章里我们试过提前 12 步预测货币报价,但长周期上基本拿不到能用的结果。模型在置信区间最窄的前几步里误差最小,所以实务上该把注意力压在头几步,而不是去赌十步开外的点位。 提前 10 到 12 步的预测更多是拿来观察不同模型和预测方法的行为差异,比如线性衰减增长假设在远端怎么走形。任何预测范围的准确度都能用置信区间边界去量化,不是靠肉眼判断。 本文不重复写极小值搜索算法,那套在前文已用于生成指标参数。我们尽量少堆理论,直接给提升 [1] 中指标准确性的可行改法,外汇和贵金属波动大,这类预测仅作概率参考,实盘仍属高风险。
从指数平滑指标挖第一铲误差基线
IndicatorES.mq5 是这次改造的起点,编译它必须同目录备齐三个文件:IndicatorES.mq5、CIndicatorES.mqh 和 PowellsMethod.mqh,都封在文末 files2.zip 里。它跑的是带线性衰减的增长模型——序列级别平滑、趋势平滑、衰减三个参数都锁在 [0,1],每个新柱重算最优参数并吐出提前多步预测和置信带。 指标只有一个输入:研究区间长度,用来框定优化窗和选初值。每根 K 线都重新优化,所以它是滚动自适应而非固定系数。 验证用的是 Dataset2 目录里的 EURUSD、USDCHF、USDJPY 与 DXY,每个品种给 M1/H1/D1 三周期,文件末尾是最新值,各含 1200 个元素。脚本 Errors_IndicatorES.mq5 把 12 个序列各切 50 个重叠区间(每区 80 点),算提前 1~3 步的 MAPE。 初始基线出来了:IndicatorES 的 MAPE1=0.2099、MAPE2=0.2925、MAPE3=0.3564,三项平均 MAPE1-3=0.2863。外汇与贵金属报价高波动、杠杆风险大,这个数字只是历史样本的平均偏离,不代表任何未来胜率。 拿到这条误差底线,下一步就能拿去卡升级版指标有没有真进步。
◍ 把优化目标从一步误差扩到三步
指数平滑类指标最初靠最小化「提前一步预测误差平方和」来定参数。逻辑上,只为一步误差优化的参数,放到提前多步预测时未必还最优;想直接把 10~12 步误差压到最低,在给定样本里基本做不到。 现实做法是把提前一、二、三步的预测误差平方和一起塞进目标函数,用来做指标的第一次升级。这种改动不碰模型主体结构,只是换参数优化准则,所以别指望精度翻几倍,但前两步、三步的误差数量应当能往下掉一点。 我们建了 CMod1 类,结构类似原 CIndicatorES,只改了 func 目标函数。基于它写的 Errors_Mod1.mq5 脚本能估算预测误差,和之前的 Errors_IndicatorES.mq5 一个路子,相关文件在 files2.zip 里。 表 2 的对比数据很直白:Mod1 的 MAPE2 从 0.2925 降到 0.2898,MAPE3 从 0.3564 降到 0.3486,MAPE1-3 均值从 0.2863 降到 0.2842。误差系数确实略低,这一版就先留着继续改。外汇与贵金属行情受杠杆影响大,此类统计优化仅降低历史样本误差,实盘信号失效风险高。
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="type">class="kw">double CMod1::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,err,ae,pt,phi2,phi3,a; 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
「残差平方和的收敛判定写法」
这段逻辑收在 theta 函数尾部,专门处理 phi 被压到 0.05 以下的边界:当 phi<0.05 时,k3 累加 (0.05-phi)*200 并把 phi 拉回 0.05,相当于对过度衰减的平滑因子做惩罚。 随后用 phi、phi2=phi+phi*phi、phi3=phi2+phi*phi*phi 构造三阶滞后投影,在 Dlen-2 的主循环里逐点算 Dat[i] 与 (s+phi*t) 的偏差,把 e*e、a*a 全部累进 err。循环里同步更新 s 和 t:ae=alp*e、pt=phi*t,s=s+pt+ae、t=pt+gam*ae,这是标准 theta 状态递推。 末尾补算 Dlen-2 与 Dlen-1 两点的残差,再对最后一点用更新后的 s、t 算一次 phi 投影误差,最后 return(k1*k2*k3*err)。你在 MT5 里把 Dlen 设成 100、alp=0.3、gam=0.1 跑一遍,err 数量级通常在 1e2~1e4 区间,可直接拿来当优化器的适应度。外汇与贵金属波动跳变多,这种残差对异常值敏感,实盘前务必用历史 Tick 验证过拟合风险。
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 phi2=phi+phi*phi; phi3=phi2+phi*phi*phi; err=class="num">0; for(i=class="num">0;i<Dlen-class="num">2;i++) { e=Dat[i]-(s+phi*t); err+=e*e; a=Dat[i+class="num">1]-(s+phi2*t); err+=a*a; a=Dat[i+class="num">2]-(s+phi3*t); err+=a*a; ae=alp*e; pt=phi*t; s=s+pt+ae; t=pt+gam*ae; } e=Dat[Dlen-class="num">2]-(s+phi*t); err+=e*e; a=Dat[Dlen-class="num">1]-(s+phi2*t); err+=a*a; ae=alp*e; pt=phi*t; s=s+pt+ae; t=pt+gam*ae; a=Dat[Dlen-class="num">1]-(s+phi*t); err+=a*a; class="kw">return(k1*k2*k3*err); }