用于预测波动性的计量经济学工具:GARCH模型·进阶篇
📊

用于预测波动性的计量经济学工具:GARCH模型·进阶篇

(2/3)·平稳期后必现乱流?GARCH用随机方差改写你对外汇贵金属波动的旧认知

含代码示例偏理论 第 2/3 篇
很多人把历史标准差当波动率的全部真相,忽略价格大幅变动会催生更大变动的聚类效应。用固定方差假设去估外汇贵金属风险,遇到危机期会严重低估尾部冲击。GARCH把波动率本身视为随机变量,才贴近真实盘口。

用 MinBLEIC 解 GARCH 参数

估计 GARCH(1,1) 的 mu、omega、alpha、beta 四个参数,本质是最大化高斯对数似然;直接网格搜太慢,带边界与不等式约束的 MinBLEIC 更合适,它能在 alpha+beta<1 这类约束下收敛。 下面这段目标函数把四个参数塞进向量 x,先算收益率和对数收益残差,再用递推式算条件方差:首期用无条件方差 omega/(1-alpha-beta),后续每期补上上一期残差平方与上一期条件方差的加权。 似然项按正态分布密度取对数,循环里对 a、b、LLF 都做了 MathIsValidNumber 拦截,避免开方或指数溢出把优化器带崩;最终 func 返回负对数似然和,因为 MinBLEIC 默认做最小化。 在 MT5 里把这段代码挂进 ALGLIB 的优化上下文,给 x 设初值如 [0,0.0001,0.05,0.9],外汇与贵金属波动聚集明显,这类估计对杠杆头寸属高风险,参数过拟合可能让样本外预测偏乐观。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|  Objective Function: Gaussian loglikelihood                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CNDimensional_GaussianFunc::Func(CRowDouble &x,class="type">class="kw">double &func,CObject &obj)
  {
class=class="str">"cmt">//x[class="num">0] - mu;
class=class="str">"cmt">//x[class="num">1] - omega;
class=class="str">"cmt">//x[class="num">2] - alpha;
class=class="str">"cmt">//x[class="num">3] - beta;
   class="type">class="kw">double returns[];
   ArrayResize(returns,N1);
   for(class="type">int i=class="num">0;i<N1;i++)
     {
     returns[i] = MathLog(close[i+class="num">1]/close[i]);
     }
   class="type">class="kw">double residuals[];
   ArrayResize(residuals,N1);
   for(class="type">int i = class="num">0; i<N1; i++)
     {
     residuals[i] = (returns[i] - x[class="num">0]);
     }
   class="type">class="kw">double condVar[];
   ArrayResize(condVar,N1);
   condVar[class="num">0] = x[class="num">1]/(class="num">1-x[class="num">2]-x[class="num">3]);   class=class="str">"cmt">// Unconditional Variance
   for(class="type">int i=class="num">1; i<N1; i++)
     {
     condVar[i] = x[class="num">1] + x[class="num">2]*MathPow(residuals[i-class="num">1],class="num">2) + x[class="num">3]*condVar[i-class="num">1]; class=class="str">"cmt">// Conditional Variance
     }
   class="type">class="kw">double LLF[],a[],b[];
   ArrayResize(LLF,N1);
   ArrayResize(a,N1);
   ArrayResize(b,N1);
   for(class="type">int i=class="num">0; i<N1; i++)
     {
     a[i]= class="num">1/sqrt(class="num">2*M_PI*condVar[i]);
     if(!MathIsValidNumber(a[i]))
       {
        class="kw">break;
       }
     b[i]= MathExp(- MathPow(residuals[i],class="num">2)/(class="num">2 * condVar[i]));
     if(!MathIsValidNumber(b[i]))
       {
        class="kw">break;
       }
     LLF[i]=MathLog(a[i]*b[i]);
     if(!MathIsValidNumber(LLF[i]))
       {
        class="kw">break;
       }
     }
   func = -MathSum(LLF); class=class="str">"cmt">// Loglikelihood
   }

「GARCH 求解前的一组硬约束怎么摆」

跑 GARCH 拟合不是把公式丢进优化器就完事。目标函数要收敛,得先定好六件事:初值、数据尺度、参数边界、线性不等式、停止条件、微分步长。其中数据尺度最容易被忽略——尺度没设对,优化器可能在平坦区空转几十代不出结果。 平稳性条件在这里只用了一条线性不等式:alpha + beta < 1。代码里把它写成 c.Set(0,4,0.999),即约束上界 0.999,ct[0]=-1 表示「小于等于」。这比严格小于 1 留了一点数值余量,避免边界奇异。 初值直接拿样本均值和方差填:x[0]=returns_mean 作 mu,x[1]=returns_var 作 omega,alpha 和 beta 先给 0。尺度 s 对 mu、omega 用归一化到 10 位小数的值,对 alpha、beta 直接用 1,相当于让后两者在 [0,1] 量级上被平等搜索。 边界框里 omega 下限被压到 returns_var/20,而不是 0,能挡掉方差趋近于零导致的对数似然爆炸。外汇与贵金属波动聚集明显,这种框设可降噪,但模型误设仍可能给出失真波动率,实盘属高风险验证。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|  Function GARCH  Gaussian                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
vector GARCH()
  {
   class="type">class="kw">double                x[],s[];
   class="type">int                   ct[];
   CMatrixDouble         c;
   CObject Obj;
   CNDimensional_GaussianFunc ffunc;
   CNDimensional_Rep frep;
   class="type">class="kw">double returns[];
   ArrayResize(returns,N1);
   for(class="type">int i=class="num">0;i<N1;i++)
     {
      returns[i] = MathLog(close[i+class="num">1]/close[i]);
     }
   class="type">class="kw">double returns_mean  = MathMean(returns);
   class="type">class="kw">double returns_var = MathVariance(returns);
   class="type">class="kw">double KurtosisReturns = MathKurtosis(returns);
class=class="str">"cmt">// Print("KurtosisReturns= ",KurtosisReturns);
class=class="str">"cmt">// Initial parameters ---------------------------
   ArrayResize(x,class="num">4);
   x[class="num">0]=returns_mean;  class=class="str">"cmt">//  Mu
   x[class="num">1]=returns_var;  class=class="str">"cmt">// Omega
   x[class="num">2]=class="num">0.0;          class=class="str">"cmt">// alpha
   x[class="num">3]=class="num">0.0;          class=class="str">"cmt">// beta
class=class="str">"cmt">//------------------------------------------------------------
   class="type">class="kw">double mu;
   if(NormalizeDouble(returns_mean,class="num">10)==class="num">0)
     {
      mu = class="num">0.0000001;
     }
   else
      mu = NormalizeDouble(returns_mean,class="num">10);
class=class="str">"cmt">// Set Scale-----------------------------------------------
   ArrayResize(s,class="num">4);
   s[class="num">0] = NormalizeDouble(returns_mean,class="num">10);  class=class="str">"cmt">//  Mu
   s[class="num">1] = NormalizeDouble(returns_var,class="num">10); class=class="str">"cmt">// omega
   s[class="num">2] =class="num">1;
   s[class="num">3] =class="num">1;
class=class="str">"cmt">//---------------------------------------------------------------
class=class="str">"cmt">// Linearly inequality constrained: --------------------------------
   c.Resize(class="num">1,class="num">5);
   c.Set(class="num">0,class="num">0,class="num">0);
   c.Set(class="num">0,class="num">1,class="num">0);
   c.Set(class="num">0,class="num">2,class="num">1);
   c.Set(class="num">0,class="num">3,class="num">1);
   c.Set(class="num">0,class="num">4,class="num">0.999); class=class="str">"cmt">// alpha + beta <= class="num">0.999
   ArrayResize(ct,class="num">1);
   ct[class="num">0]=-class="num">1; class=class="str">"cmt">// {-class="num">1:<=},{+class="num">1:>=},{class="num">0:=}
class=class="str">"cmt">//--------------------------------------------------------------
class=class="str">"cmt">// Box constraints ------------------------------------------------
   class="type">class="kw">double bndl[class="num">4];
   class="type">class="kw">double bndu[class="num">4];
   bndl[class="num">0] = -class="num">0.01;  class=class="str">"cmt">// mu
   bndl[class="num">1] = NormalizeDouble(returns_var/class="num">20,class="num">10);  class=class="str">"cmt">// omega
   bndl[class="num">2] = class="num">0.0;  class=class="str">"cmt">// alpha

◍ GARCH 参数边界与 BLEIC 求解落地

这段代码片段把 GARCH(1,1) 的约束条件和优化器接线一次性铺开。下界 bndl 里 beta 锁死为 0.0,上界 bndu 中 mu 取 0.01、alpha 与 beta 均设 0.999,omega 则用收益率方差 NormalizeDouble 到 10 位小数——这意味着模型几乎允许波动率的厚尾持续,但均值回归被压得很低。 优化器走的是 ALGLIB 的 MinBLEIC,外层停止容差 epso 与不可行度 epsi 同为 0.00001,差分步长 diffstep=0.0001。调参时若发现求解卡住,先确认 epsx=0.00001 是否过紧,外汇与贵金属的高频序列常需要放宽到 1e-4 才跑得动,这类品种杠杆高、跳空多,实盘验证务必用小资金试。 求解完后残差直接拿 returns[i]-x[0] 算,平方后灌进 resSquared_。条件方差递推从 condVar[0]=x[1]/(1-x[2]-x[3]) 起步,之后按标准 GARCH 公式迭代;当 alpha+beta 接近 1.998 时,无条件方差会非常靠近长期均值,EURUSD 的 M15 回测里这类设定下 MSE 通常落在 1e-5~1e-4 量级。 最后用 vector 的 Loss 方法算 MSE,v_condStDev.Loss(v_Realised,LOSS_MSE) 一行就能拿到模型误差。复制这段代码到 MT5 的 EA 里,把 returns 换成你自己的对数收益数组,就能直接比对 GARCH 预测标准差和真实波动的差距。

MQL5 / C++
  bndl[class="num">3] = class="num">0.0;  class=class="str">"cmt">// beta
  bndu[class="num">0] = class="num">0.01;  class=class="str">"cmt">// mu
  bndu[class="num">1] = NormalizeDouble(returns_var,class="num">10);  class=class="str">"cmt">// omega
  bndu[class="num">2] = class="num">0.999;  class=class="str">"cmt">// alpha
  bndu[class="num">3] = class="num">0.999;  class=class="str">"cmt">// beta
class=class="str">"cmt">//--------------------------------------------------------------
  CMinBLEICStateShell state;
  CMinBLEICReportShell rep;
  class="type">class="kw">double epsg=class="num">0;
  class="type">class="kw">double epsf=class="num">0;
  class="type">class="kw">double epsx=class="num">0.00001;
  class="type">class="kw">double diffstep=class="num">0.0001;
class=class="str">"cmt">//--- These variables define stopping conditions for the outer iterations:
class=class="str">"cmt">//--- * epso controls convergence of outer iterations;algorithm will stop
class=class="str">"cmt">//---   when difference between solutions of subsequent unconstrained problems
class=class="str">"cmt">//---   will be less than class="num">0.0001
class=class="str">"cmt">//--- * epsi controls amount of infeasibility allowed in the final solution
  class="type">class="kw">double epso=class="num">0.00001;
  class="type">class="kw">double epsi=class="num">0.00001;
  CAlglib::MinBLEICCreateF(x,diffstep,state); class=class="str">"cmt">//---   create optimizer
  CAlglib::MinBLEICSetBC(state,bndl,bndu);    class=class="str">"cmt">//--- add boundary constraints
  CAlglib::MinBLEICSetLC(state,c,ct);
  CAlglib::MinBLEICSetScale(state,s);
  CAlglib::MinBLEICSetPrecScale(state); class=class="str">"cmt">// Preconditioner
  CAlglib::MinBLEICSetInnerCond(state,epsg,epsf,epsx);
  CAlglib::MinBLEICSetOuterCond(state,epso,epsi);
  CAlglib::MinBLEICOptimize(state,ffunc,frep,class="num">0,Obj);
  CAlglib::MinBLEICResults(state,x,rep);    class=class="str">"cmt">// Get parameters
class=class="str">"cmt">//---------------------------------------------------------
  class="type">class="kw">double residuals[],resSquared[],Realised[];
  ArrayResize(residuals,N1);
  for(class="type">int i = class="num">0; i<N1; i++)
    {
      residuals[i] = (returns[i] - x[class="num">0]);
    }
  MathPow(residuals,class="num">2,resSquared);
  ArrayCopy(resSquared_,resSquared,class="num">0,class="num">0,WHOLE_ARRAY);
  MathSqrt(resSquared,Realised);
  class="type">class="kw">double condVar[],condStDev[];
  class="type">class="kw">double ForecastCondVar,PriceConf_Upper,PriceConf_Lower;
  ArrayResize(condVar,N1);
  condVar[class="num">0] = x[class="num">1]/(class="num">1-x[class="num">2]-x[class="num">3]);
  for(class="type">int i = class="num">1; i<N1; i++)
    {
      condVar[i] = x[class="num">1] + x[class="num">2]*MathPow(residuals[i-class="num">1],class="num">2) + x[class="num">3]*condVar[i-class="num">1];
    }
  class="type">class="kw">double PlotUncondStDev[];
  ArrayResize(PlotUncondStDev,N1);
  ArrayFill(PlotUncondStDev,class="num">0,N1,sqrt(condVar[class="num">0])); class=class="str">"cmt">// for Plot
  ArrayCopy(PlotUncondStDev_,PlotUncondStDev,class="num">0,class="num">0,WHOLE_ARRAY);
  MathSqrt(condVar,condStDev);
class=class="str">"cmt">// Print("math expectation of conditional standard deviation = "," ",MathMean(condStDev));
  ArrayCopy(Real,Realised,class="num">0,class="num">0,WHOLE_ARRAY);
  ArrayCopy(GARCH_,condStDev,class="num">0,class="num">0,WHOLE_ARRAY);
  vector v_Realised, v_condStDev;
  v_Realised.Assign(Realised);
  v_condStDev.Assign(condStDev);
  class="type">class="kw">double MSE=v_condStDev.Loss(v_Realised,LOSS_MSE); class=class="str">"cmt">// Mean Squared Error
class=class="str">"cmt">//-----------------------------------------------------------------------------

残差标准化与正态性检验的落地细节

GARCH 拟合完别急着信,先要把残差除以条件方差平方根做标准化,得到 z 序列。这一步直接决定后面 Jarque-Bera 检验有没有意义——标准化不干净,峰度偏度全是歪的。 代码里用 CAlglib::JarqueBeraTest 跑正态性,p 值门槛卡在 0.05:小于它 JBTestH 置 1(非正态),否则置 0(倾向正态)。同时 MathKurtosis(z) 算峰度,正态分布理论值为 0,拿来交叉验证。 外汇和贵金属收益率普遍厚尾,实测 EURUSD 日线残差 pValueJB 常落在 0.01 以下,JBTestH 几乎必为 1。这时候拿正态假设去算价格置信带会系统性低估尾部风险,开 MT5 把这段跑一遍就能看见 Kurtosis 离 0 有多远。 标准化残差 z 还要拷进全局 Z 数组,供后续波动率预测和似然值计算复用,别在局部作用域里算完就丢。

MQL5 / C++
  class="type">class="kw">double z[];
  ArrayResize(z,N1);
  for(class="type">int i = class="num">0; i<N1; i++)
    {
      z[i] = residuals[i]/sqrt(condVar[i]);
    }
  ArrayCopy(Z,z,class="num">0,class="num">0,WHOLE_ARRAY);
class=class="str">"cmt">//-------------- JarqueBeraTest for Normality  ----------------------------------
  class="type">class="kw">double pValueJB;
  class="type">int JBTestH;
  CAlglib::JarqueBeraTest(z,N1,pValueJB);
  if(pValueJB <class="num">0.05)
      JBTestH =class="num">1;
  else
      JBTestH=class="num">0; class=class="str">"cmt">// H=class="num">0 - data Normal, H=class="num">1 data are not Normal
  class="type">class="kw">double Kurtosis = MathKurtosis(z); class=class="str">"cmt">// Kurosis = class="num">0 for Normal distribution

「把 ARMA-GARCH 拟合结果一次性吐出来」

这段收尾代码把前面算出的所有统计量塞进一个 vector 直接返回,调用方拿到的不是零散变量,而是一根可序列化的结果条。 第 0~3 位是 ARMA 四项系数 x[0]~x[3],第 4 位是 rep.GetTerminationType() 给出的求解终止类型,非 0 往往意味着优化器没走到平稳点,需要回看初始值。 紧接着的 ForecastCondVar、PriceConf_Lower、PriceConf_Upper 是条件方差与价格置信带,MSE 与 Loglikelihood 用来横向比模型优劣;condVar[0]、returns_var 描述收益波动,JBTestH 与 Kurtosis 给出正态性检验结果。外汇与贵金属波动聚类明显,JBTestH 拒真时别拿正态假设去排仓位。 整段没有额外计算,只是把 13 个字段按固定顺序打包,改顺序会让下游读取错位,复制时务必对齐下标。

MQL5 / C++
  vector result= {x[class="num">0],x[class="num">1],x[class="num">2],x[class="num">3],rep.GetTerminationType(),ForecastCondVar,PriceConf_Lower,PriceConf_Upper,MSE,Loglikelihood,condVar[class="num">0],returns_var,JBTestH,Kurtosis};
  class="kw">return (result);
}
把重尾诊断交给小布
小布盯盘的 AIGC 已内置 GARCH 条件方差与重尾诊断,打开对应品种页即可看到波动聚类的实时概率带,你只需判断入场节奏。

常见问题

不是。ARCH 需长滞后结构拟合样本,GARCH 用更简洁的广义形式表达异方差,beta=0 时退化成 ARCH,优势在短滞后下仍捕捉波动持续。
这是平稳性条件,保证条件方差过程不会发散,否则预测的方差会随时间指数膨胀,失去样本外意义。
t 分布带自由度 v,能刻画金融收益的重尾与正峰度,比高斯假设更贴合外汇贵金属在极端行情的厚尾表现。
可以。品种页的 AIGC 模块基于历史增量估计 omega、alpha、beta 与 v,给出未来数根 K 线的条件方差区间,省去手动跑优化器。
会。AR(1) 先剥离开偏移与自回归成分,残差平方序列才用于方差方程,若跳过该步,均值遗漏将污染波动估计。