利用指数平滑法进行时间序列预测·进阶篇
📈

利用指数平滑法进行时间序列预测·进阶篇

(2/3)· 跳过预处理直接用货币报价测试 Holt-Brown 模型,看误差到底藏在哪里

案例拆解 第 2/3 篇
很多人把汇率报价直接丢进预测函数就等结果,忽略稳定性与离群值的影响,出来的曲线只是自我安慰。外汇贵金属杠杆高、跳空频繁,用未预处理序列硬测模型,误差会被放大到无法决策。先把本文的评估步骤过一遍,再谈信号可信度。

用 RelMAE 看穿预测到底比随机游走强多少

给序列建模做一步预测时,先选了参数和初值,接下来就必须回答一个问题:这预测到底准不准。常见做法是看 MSE(均方误差),公式是把每一步误差平方后取平均;但它对偶发大误差特别敏感,因为平方会放大离群值。 改用 MAE(平均绝对误差)能稳一些,不平方只取绝对值。不过 MSE 和 MAE 都只是绝对数,比如算出 MAE=0.03,你根本没法说这算好还是坏,更不能拿去横向比不同品种。 真正能跨序列比的是相对评估:RelMSE 和 RelMAE。思路是用朴素法(假设下一根 K 线等于当前值)当基准,把你的误差估算除以朴素法的误差。比值等于 1 说明跟瞎猜差不多;小于 1 才算真有点用;大于 1 就是还不如直接拿现价填坑。 拿线性增长单参数模型跑了 8 个品种的最近 200 根测试,alpha 取 0.3/0.4/0.5 下 RelMAE 全部大于 1:EURUSD M1 在 alpha=0.5 时为 1.10,XAUUSD M30 在 alpha=0.4 时为 1.12。外汇与贵金属杠杆高、滑点跳空频繁,这类回测只说明该模型在此样本内未战胜朴素法,实盘风险依旧很高。 把提前一步误差换成两步或更多步的误差,同一套 Rel 算法也能估多步预测质量。开 MT5 自己拽一段 EURUSD M1 历史,写个朴素法基准对比,就能复算表 1 的数。

「加法指数平滑的六种展开」

前文提到的过程级别加线性趋势加随机变量的结构,只是指数平滑的起点。把周期和季节分量也作为总和叠进去,就得到了加法模型这一族;与之并列的还有乘法模型,但本篇只啃加法。 简单指数平滑对应 ARIMA(0,1,1);线性增长加法模型用了两个独立平滑参数,分别管级别和趋势,等效 ARIMA(0,2,2);线性衰减增长模型则多一个衰减系数,趋势斜率按该系数在每个预测步递减,文献图 4 显示衰减系数越小趋势失效越快。 把季节分量以总和形式加进上述三个模型,再得三个加法季节模型:简单季节、线性增长季节、衰减增长加线性增长季节。季节模型虽也有 ARIMA 等价式,但实用价值低,此处不展开。 公式标记里四个平滑参数 α、β、γ、φ 都落在 [0:1];p 为季节周期数,m 为提前步数。末个模型公式把六个变量全用上了。若在衰减增长加线性增长季节模型里令 γ=0,季节被忽略;β=0 退化为线性增长,β=φ=0 则成衰减增长;简单指数平滑即 α 外其余为 0。 概率预测区间用文献 [3] 的派生分析法:以全样本大小 n 的提前一步误差平方和均值作方差估计,提前 m 步估计方差按模 p 取 1 或 0 的权重累加,95% 区间限制值据此算出,可称置信区间。短区间测试序列未见稳定循环,故季节初始化示例从略。 下面这段 MQL5 类声明骨架,就是要把上述表达式落到代码里,方便在 MT5 里直接接自己的报价序列跑。

◍ AdditiveES 类的方法与简单模型反而更稳

AdditiveES 类以线性衰减增长模型加线性增长加法季节模型为底子,是所有派生模型里参数最杂的一个。它的 Init 方法必须第一个调用,负责塞入平滑级别、趋势、alpha、gamma、phi、delta 以及季节周期数 nses;注意 Init 本身不支持非零季节指数,只能把季节项置零,要非零得随后用 IniIs(m, is) 单独设。 跑数时每次来新值就调 NewY(y),返回提前一步预测;之后再调 Fcast(m) 拿提前 m 步的预测,且不影响内部平滑状态。VarCoefficient(m) 给出的是预测方差相对一步方差的放大系数,能直接看出远端预测的不确定性膨胀速度。 GetS、GetT、GetF、GetIs 只是受保护变量的读接口,方便外部拿平滑级别、趋势、一步预测和指定序号的季节指数。 复杂模型参数多,直觉上该更准,但实际常相反:参数少的简单模型在多数样本里预测误差方差更小、运行更平。并行跑全部模型后按最低误差加参数惩罚选优,阿凯克信息论准则(AIC)就是这类标准之一,倾向挑出最稳的那个而不是最花哨的。 为验证,原文附带 AdditiveES_Test.mq5 指标,所有平滑参数手设、季节初值恒为零。随着新值流入,季节影响从零慢慢爬到稳态,所需柱数取决于 delta——delta 越小,爬得越慢。指标还画了过去值的一步预测线及 95% 置信带,带宽基于近期 nTest 根柱的一步误差方差,避开初始值不精确的干扰。外汇与贵金属行情跳变频繁,这类指数平滑在外盘高风险环境下仅作概率参考,实盘前务必在 MT5 用历史数据复跑。

MQL5 / C++
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class=class="str">"cmt">//                                                                              AdditiveES.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=class="str">"cmt">//-----------------------------------------------------------------------------------
class=class="str">"cmt">// Forecasting. Exponential smoothing. Additive models.
class=class="str">"cmt">// References:
class=class="str">"cmt">// class="num">1. Everette S. Gardner Jr. Exponential smoothing: The state of the art – Part II.
class=class="str">"cmt">//    June class="num">3, class="num">2005.
class=class="str">"cmt">// class="num">2. Rob J Hyndman. Forecasting based on state space models for exponential
class=class="str">"cmt">//    smoothing. class="num">29 August class="num">2002.
class=class="str">"cmt">// class="num">3. Rob J Hyndman et al. Prediction intervals for exponential smoothing
class=class="str">"cmt">//    class="kw">using two new classes of state space models. class="num">30 January class="num">2003.
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class AdditiveES:class="kw">public CObject
  {
class="kw">protected:
  class="type">class="kw">double Alpha;    class=class="str">"cmt">// Smoothed parameter for the level of the series
  class="type">class="kw">double Gamma;    class=class="str">"cmt">// Smoothed parameter for the trend
  class="type">class="kw">double Phi;      class=class="str">"cmt">// Autoregressive or damping parameter
  class="type">class="kw">double Delta;    class=class="str">"cmt">// Smoothed parameter for seasonal indices
  class="type">int    nSes;     class=class="str">"cmt">// Number of periods in the seasonal cycle
  class="type">class="kw">double S;        class=class="str">"cmt">// Smoothed level of the series, computed after last Y is observed
  class="type">class="kw">double T;        class=class="str">"cmt">// Smoothed additive trend

加法型指数平滑的类骨架与初始化逻辑

把季节性拆成加法项的指数平滑模型,在 MT5 里通常以一个 C++ 风格类承载。下面这段声明给出了核心成员:Ises[] 存平滑后的季节指数,p_Ises 是它的移位寄存器指针,F 是原点 t 向后一期的预测值。 构造函数 AdditiveES() 把默认参数定死了:Alpha=0.5、Gamma=0、Delta=0、Phi=1、nSes=1,并把 Ises 数组 resize 后清零。也就是说,不传参时它先当一个无季节项、无阻尼的简单平滑器跑起来。 Init() 才是真正注入状态的地方。它接收水平 s、趋势 t,以及 alpha/gamma/phi/delta/nses 全套系数,且对每个系数做了硬截断——Alpha、Gamma、Phi、Delta 超出 [0,1] 会被夹回边界,nSes 小于 1 强制为 1。最后 F 按 S+Phi*T 算出来返回,这一步决定了首期预测的基准。 别把默认 Alpha 当最优 构造里 Alpha 写死 0.5 只是占位,黄金小时图这类噪声大的品种,直接 0.5 可能让水平线过度追随杂波;进 Init() 前最好先按样本回测调,例如从 0.2 起扫到 0.8 看残差方差。外汇与贵金属杠杆高,模型误差会被放大,任何预测都只是概率倾向,不是方向保证。

MQL5 / C++
  class="type">class="kw">double Ises[];   class=class="str">"cmt">// Smoothed seasonal indices
  class="type">int    p_Ises;   class=class="str">"cmt">// Pointer for Ises[] shift register
  class="type">class="kw">double F;        class=class="str">"cmt">// Forecast for class="num">1 period ahead from origin t
class="kw">public:
       AdditiveES();
  class="type">class="kw">double Init(class="type">class="kw">double s,class="type">class="kw">double t,class="type">class="kw">double alpha=class="num">1,class="type">class="kw">double gamma=class="num">0,
              class="type">class="kw">double phi=class="num">1,class="type">class="kw">double delta=class="num">0,class="type">int nses=class="num">1);
  class="type">class="kw">double GetS()                { class="kw">return(S); }
  class="type">class="kw">double GetT()                { class="kw">return(T); }
  class="type">class="kw">double GetF()                { class="kw">return(F); }
  class="type">class="kw">double GetIs(class="type">int m);
  class="type">void   IniIs(class="type">int m,class="type">class="kw">double is);   class=class="str">"cmt">// Initialization of smoothed seasonal indices
  class="type">class="kw">double NewY(class="type">class="kw">double y);           class=class="str">"cmt">// Next calculating step
  class="type">class="kw">double Fcast(class="type">int m);             class=class="str">"cmt">// m-step ahead forecast
  class="type">class="kw">double VarCoefficient(class="type">int m);    class=class="str">"cmt">// Coefficient for calculating prediction intervals
  };
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class=class="str">"cmt">// Constructor
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class="type">void AdditiveES::AdditiveES()
  {
  Alpha=class="num">0.5; Gamma=class="num">0; Delta=class="num">0; Phi=class="num">1; nSes=class="num">1;
  ArrayResize(Ises,nSes);
  ArrayInitialize(Ises,class="num">0);
  p_Ises=class="num">0; S=class="num">0; T=class="num">0;
  }
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class=class="str">"cmt">// Initialization
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class="type">class="kw">double AdditiveES::Init(class="type">class="kw">double s,class="type">class="kw">double t,class="type">class="kw">double alpha=class="num">1,class="type">class="kw">double gamma=class="num">0,
                     class="type">class="kw">double phi=class="num">1,class="type">class="kw">double delta=class="num">0,class="type">int nses=class="num">1)
  {
  S=s; T=t;
  Alpha=alpha; if(Alpha<class="num">0)Alpha=class="num">0; if(Alpha>class="num">1)Alpha=class="num">1;
  Gamma=gamma; if(Gamma<class="num">0)Gamma=class="num">0; if(Gamma>class="num">1)Gamma=class="num">1;
  Phi=phi; if(Phi<class="num">0)Phi=class="num">0; if(Phi>class="num">1)Phi=class="num">1;
  Delta=delta; if(Delta<class="num">0)Delta=class="num">0; if(Delta>class="num">1)Delta=class="num">1;
  nSes=nses; if(nSes<class="num">1)nSes=class="num">1;
  ArrayResize(Ises,nSes);
  ArrayInitialize(Ises,class="num">0);
  p_Ises=class="num">0;
  F=S+Phi*T;
  class="kw">return(F);
  }
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class=class="str">"cmt">// Calculations for the new Y

「加性指数平滑的季节性更新与预测内核」

加性指数平滑(Additive ES)把水平、趋势和季节三项拆开迭代。NewY 函数在每次来一个新观测 y 时,先算误差 e=y-F,再用 Alpha 更新水平 S、用 Gamma 约束趋势 T 的阻尼,同时把季节偏差按 Delta*(1-Alpha)*e 推进环形缓冲 Ises。 GetIs 和 IniIs 负责季节指数的环形存取:用 MathMod(m+p_Ises,nSes) 把任意超前步数 m 映射到当前季节窗口内的下标,初始化时只允许写前 nSes 个槽位。这样多周期(比如日内的 24 小时或周内的 5 个交易日)季节项不会越界。 Fcast 做 m 步向前预测:h 步的水平贡献是 S 加一个等比数列求和 v*T(公比 Phi),再加 GetIs(h) 对应的季节项。当 Phi=1 时 v 正好等于 h,退化为无阻尼线性外推。 VarCoefficient 给出预测区间的系数,不是简单累加。它在 i=1..h-1 上累加 a²,其中 a 同时含趋势项 Alpha*(1+v*Gamma) 和季节项 k*Delta*(1-Alpha);仅当 MathMod(i,nSes)==0 时 k=1,说明跨过一个完整季节周期才计入季节不确定度。h=1 时直接返回 1,即单步方差不放大。 外汇与贵金属价格具备高波动、高杠杆风险,这套平滑内核仅用于辅助刻画序列结构,实盘信号须结合风控与多周期验证。

MQL5 / C++
class="type">class="kw">double AdditiveES::NewY(class="type">class="kw">double y)
  {
  class="type">class="kw">double e;

  e=y-F;
  S=S+Phi*T+Alpha*e;
  T=Phi*T+Alpha*Gamma*e;
  Ises[p_Ises]=Ises[p_Ises]+Delta*(class="num">1-Alpha)*e;
  p_Ises++; if(p_Ises>=nSes)p_Ises=class="num">0;
  F=S+Phi*T+GetIs(class="num">0);
  class="kw">return(F);
  }
class=class="str">"cmt">// Return smoothed seasonal index
class="type">class="kw">double AdditiveES::GetIs(class="type">int m)
  {
  if(m<class="num">0)m=class="num">0;
  class="type">int i=(class="type">int)MathMod(m+p_Ises,nSes);
  class="kw">return(Ises[i]);
  }
class=class="str">"cmt">// Initialization of smoothed seasonal indices
class="type">void AdditiveES::IniIs(class="type">int m,class="type">class="kw">double is)
  {
  if(m<class="num">0)m=class="num">0;
  if(m<nSes)
    {
    class="type">int i=(class="type">int)MathMod(m+p_Ises,nSes);
    Ises[i]=is;
    }
  }
class=class="str">"cmt">// m-step-ahead forecast
class="type">class="kw">double AdditiveES::Fcast(class="type">int m)
  {
  class="type">int i,h;
  class="type">class="kw">double v,v1;
  if(m<class="num">1)h=class="num">1; else h=m;  
  v1=class="num">1; v=class="num">0;
  for(i=class="num">0;i<h;i++){v1=v1*Phi; v+=v1;}
  class="kw">return(S+v*T+GetIs(h));
  }
class=class="str">"cmt">// Coefficient for calculating prediction intervals
class="type">class="kw">double AdditiveES::VarCoefficient(class="type">int m)
  {
  class="type">int i,h;
  class="type">class="kw">double v,v1,a,sum,k;

  if(m<class="num">1)h=class="num">1; else h=m;
  if(h==class="num">1)class="kw">return(class="num">1);
  v=class="num">0; v1=class="num">1; sum=class="num">0;
  for(i=class="num">1;i<h;i++)
    {
    v1=v1*Phi; v+=v1;
    if((class="type">int)MathMod(i,nSes)==class="num">0)k=class="num">1; else k=class="num">0;
    a=Alpha*(class="num">1+v*Gamma)+k*Delta*(class="num">1-Alpha);
    sum+=a*a;
    }
  class="kw">return(class="num">1+sum);
  }

◍ 把阻尼指数平滑接进主图

前面把加性指数平滑的算法拆开了,这一节直接看指标壳子怎么把模型挂上 MT5 主图。它声明了 4 个绘图缓冲:历史拟合线、预测线、以及上下两条预测区间线,颜色分别用了 DodgerBlue、DarkOrange 和 CadetBlue,视觉上能一眼区分已发生与未发生的部分。 核心参数里 Alpha=0.2 管水平平滑、Gamma=0.2 管趋势平滑、Phi=0.8 是阻尼系数,意味着趋势外推会被逐步收敛而不是直线延长。nHist=250 要求历史柱不少于 100,nTest=150 落在 50 到 nHist 之间做样本内检验,nFore=12 给出至少 2 根的未来 horizon。 代码通过 #include "AdditiveES.mqh" 把前面说的类拉进来,实例化一个 fc 对象,再由 NHist、NFore、NTest 三个整型变量接管输入值。你把这个 mqh 和 mq5 放同一目录,改 Phi 到 0.9 或 0.7,能在 EURUSD 的 H1 上直观看到预测线弯曲程度的差异,外汇品种杠杆高、信号失效也快,仅作概率参考。

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  "PInterval+"     class=class="str">"cmt">// Prediction interval
class="macro">#class="kw">property indicator_type3   DRAW_LINE
class="macro">#class="kw">property indicator_color3  clrCadetBlue
class="macro">#class="kw">property indicator_style3  STYLE_SOLID
class="macro">#class="kw">property indicator_width3  class="num">1
class="macro">#class="kw">property indicator_label4  "PInterval-"     class=class="str">"cmt">// Prediction interval
class="macro">#class="kw">property indicator_type4   DRAW_LINE
class="macro">#class="kw">property indicator_color4  clrCadetBlue
class="macro">#class="kw">property indicator_style4  STYLE_SOLID
class="macro">#class="kw">property indicator_width4  class="num">1
class="type">class="kw">double HIST[];
class="type">class="kw">double FORE[];
class="type">class="kw">double PINT1[];
class="type">class="kw">double PINT2[];
class="kw">input class="type">class="kw">double Alpha=class="num">0.2;     class=class="str">"cmt">// Smoothed parameter for the level
class="kw">input class="type">class="kw">double Gamma=class="num">0.2;     class=class="str">"cmt">// Smoothed parameter for the trend
class="kw">input class="type">class="kw">double Phi=class="num">0.8;       class=class="str">"cmt">// Damping parameter
class="kw">input class="type">class="kw">double Delta=class="num">0;       class=class="str">"cmt">// Smoothed parameter for seasonal indices
class="kw">input class="type">int    nSes=class="num">1;        class=class="str">"cmt">// Number of periods in the seasonal cycle
class="kw">input class="type">int    nHist=class="num">250;     class=class="str">"cmt">// History bars, nHist>=class="num">100
class="kw">input class="type">int    nTest=class="num">150;     class=class="str">"cmt">// Test interval, class="num">50<=nTest<nHist
class="kw">input class="type">int    nFore=class="num">12;      class=class="str">"cmt">// Forecasting horizon, nFore>=class="num">2
class="macro">#include "AdditiveES.mqh"
AdditiveES fc;
class="type">int     NHist;              class=class="str">"cmt">// history bars
class="type">int     NFore;              class=class="str">"cmt">// forecasting horizon
class="type">int     NTest;              class=class="str">"cmt">// test interval
class="type">class="kw">double ALPH;               class=class="str">"cmt">// alpha
把重复劳动交给小布
这些诊断与小布盯盘的 AIGC 已内置,打开对应品种页即可看到序列平稳性和预测误差的实时提示,你只管判断模型在该品种上是否还成立。

常见问题

作者有意保留原始序列的缺失与跳空,用来暴露加法模型在真实报价下的薄弱点;完整预处理流程见《利用指数平滑法进行时间序列预测·基础篇》。
可以,小布盯盘对应品种页内置了平稳性诊断与简化版平滑预测,省去手写 AdditiveES 类的麻烦,但仍需你确认参数区间。
若序列季节波动幅度不随水平放大,加法模型更稳;报价受事件冲击时幅度漂移,倾向先测加法再比误差。
MAPE 对零值敏感,汇率报价可搭配 RMSE 与 MAE 交叉看;样本外区间越长,概率上越接近实盘表现。