用于预测波动性的计量经济学工具: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],外汇与贵金属波动聚集明显,这类估计对杠杆头寸属高风险,参数过拟合可能让样本外预测偏乐观。
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,能挡掉方差趋近于零导致的对数似然爆炸。外汇与贵金属波动聚集明显,这种框设可降噪,但模型误设仍可能给出失真波动率,实盘属高风险验证。
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 预测标准差和真实波动的差距。
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 数组,供后续波动率预测和似然值计算复用,别在局部作用域里算完就丢。
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 个字段按固定顺序打包,改顺序会让下游读取错位,复制时务必对齐下标。
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);
}