使用指数平滑法进行时间序列预测(续)(基础篇)
📘

使用指数平滑法进行时间序列预测(续)(基础篇)

第 1/3 篇

◍ 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。误差系数确实略低,这一版就先留着继续改。外汇与贵金属行情受杠杆影响大,此类统计优化仅降低历史样本误差,实盘信号失效风险高。

MQL5 / C++
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 验证过拟合风险。

MQL5 / C++
  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);
  }

常见问题

把预测值和实线价差画成副图,残差连续反向放大且超过近20根平均2倍时,大概率不是噪声而是价格行为转弱,贵金属和外汇杠杆高,先减仓观察。
指数平滑的记忆衰减快,三步外预测方差飙升、实用信息少;把诊断精力放在1-3步残差,更容易抓到当下盘口偏离。
小布可对你打开的品种自动算EMA预测残差基线,越线时推送提醒,你只管判断是不是真信号,不用自己写指标跑数据。
小时图用最近50-100根算残差均值和标准差够用;样本太短易被单根针扫到,太长则跟不上品种波动 regime 切换。
相邻两次迭代平方和变动小于1e-4且连续5次不降,可判收敛;外汇高风险,别为省计算硬跑过百次,边际收益很低。