使用计量经济学方法分析图表·进阶篇
(2/3)· 价格序列的肥尾与杠杆效应常被肉眼误读,本篇用非线性模型给出可量化的诊断路径
「用对数回报替代价格做序列研究」
做时序建模时,直接预测价格序列往往不如预测回报序列。价格变化的对数(即连续复合回报)定义为 ln(P_t / P_{t-1}),其中 P_t 为 t 时刻价格、P_{t-1} 为前一时刻价格。回报序列相比原始价格具备更平稳的统计特性,外盘贵金属与外汇的高杠杆品种尤其明显,用回报能削弱异方差带来的干扰。 回报数组天然比主序列少一个元素,所以指标从第 1 个索引(第二个柱)才开始计算,第 0 位固定为 0。下面这段 ReturnsIndicator 的 OnCalculate 就是按这个逻辑写的,新手可对照自定义指标文档在 MT5 里建一个 mq5 验证。 别把首元素当有效信号 首根 K 线的回报缓冲写 0 只是占位,回测或采样时若从 i=0 起算,会把无效 0 带进均值/方差,扭曲分布估计。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Custom indicator iteration function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnCalculate(const class="type">int rates_total, class=class="str">"cmt">// size of the array price[] const class="type">int prev_calculated, class=class="str">"cmt">// number of bars available at the previous call const class="type">int begin, class=class="str">"cmt">// index of the array price[] the reliable data starts from const class="type">class="kw">double& price[]) class=class="str">"cmt">// array for the calculation itself { class=class="str">"cmt">//--- class="type">int start; if(prev_calculated<class="num">2) start=class="num">1; class=class="str">"cmt">// start filling ReturnsBuffer[] from the class="num">1-st index, not class="num">0. else start=prev_calculated-class="num">1; class=class="str">"cmt">// set &class="macro">#x27;start&class="macro">#x27; equal to the last index in the arrays for(class="type">int i=start;i<rates_total;i++) { ReturnsBuffer[i]=MathLog(price[i]/price[i-class="num">1]); } class=class="str">"cmt">//--- class="kw">return value of prev_calculated for next call class="kw">return(rates_total); } class=class="str">"cmt">//+------------------------------------------------------------------+
用 Ljung-Box Q 检验挑出序列自相关
判定一段价格回报序列该不该上 GARCH 类模型,先得过统计检验这关。Ljung-Box-Pierce Q 检验干的事很直接:看序列自相关是随机噪声还是真有结构关联。同一序列在 t1、t2 的取值概率相连,相邻时刻错开一个单位去比,这种偏移就叫滞后,滞后值可以是任意正数。 MQL5 和标准 C++ 一样,没有现成的统计计算库。作者没借 Matlab 或 STATISTICA 偷懒,而是手搓了一个复数类 CComplex 来支持 Q 检验里的复数运算——因为 MQL5 不能重载算术运算符,所以 a+b 写法编译器会报错,只能调 c.opPlus(a,b) 这类方法。类外还挂了三个引用传参的辅助函数:getComplexArr、setComplexArr、setComplexArr2,专门在头文件 Complex_class.mqh 里兜底数组转换。 傅里叶变换走的是 CFFT 类(FFT_class.mqh),硬性要求是数组长度凑成 2^N。比如原始长度 n=73585,nextpow2 返回 17,因为 2^17=131072 才盖住;nFFT 再翻一倍取 2^(17+1)=262144,多出来的元素全填 0。自相关函数算完指定滞后的 ACF,就能喂进 Q 检验。 检验函数吐出的是逻辑数组:某滞后位为 false,代表零假设成立、无自相关;为 true 则拒绝零假设、存在自相关。常见做法是把第 10、15、20 个滞后打包看。若原始回报序列没显著自相关,就改测回报平方——相当于人为放大非线性成分,在原有置信限内再确认一遍。 实盘验证路径:MT5 里开 USDJPY 线图加载 ReturnsIndicator,跑 GarchTest 脚本(图解像素按屏设,例 700*250)。H4 上原始回报 ACF 蓝柱零星越红限,平方序列蓝柱压倒性占优,说明平方序列自相关明确。窗口底部给检验耗时,true 标志越多,该品种越像非线性 GARCH 的适用对象;全 false 就退回到更简模型。外汇与贵金属波动剧烈,检验结果只提示概率倾向,实盘仍需自担高风险。
class Complex { class="kw">public: class="type">class="kw">double re,im; class=class="str">"cmt">//re -real component of the complex number, im - imaginary class="kw">public: class="type">void Complex(){}; class=class="str">"cmt">//class="kw">default constructor class="type">void setComplex(class="type">class="kw">double rE,class="type">class="kw">double iM){re=rE; im=iM;}; class=class="str">"cmt">//set method(class="num">1-st variant) class="type">void setComplex(class="type">class="kw">double rE){re=rE; im=class="num">0;}; class=class="str">"cmt">//set method(class="num">2-nd variant) class="type">void ~Complex(){}; class=class="str">"cmt">//destructor class="type">void opEqual(const Complex &y){re=y.re;im=y.im;}; class=class="str">"cmt">//class="kw">operator=
◍ 复数运算与FFT类的接口骨架
在 MT5 里做频域分析,先得把复数这一层封装利索。上面这段声明把加减乘、共轭和模长归一都拆成了独立方法,其中 opMultEq 还给了两个重载:一个吃复数、一个吃 double 标量,实盘里对振幅做整体缩放会直接调第二个。 CFFT 类才是真正干活的地方。Input[] 和 Output[] 都是 Complex 数组,长度必须自己用 uint N 传进去;Forward 跑正变换,InverseT 带加权还原(Scale 默认 true),InverseF 则不加权(Scale 默认 false),三种路径对应不同的信号重建需求。 setCFFT 同样有两个版本,一种显式给 N,一种靠数组本身长度推断。你在 EA 里初始化时若数组大小固定,用第二版更省事;若每根 K 线重算窗长,第一版更不容易越界。外汇与贵金属波动剧烈,高频 FFT 重算有滑点风险,参数先上模拟盘验证。
class="type">void opPlus(const Complex &x,const Complex &y); class=class="str">"cmt">//class="kw">operator+ class="type">void opPlusEq(const Complex &y); class=class="str">"cmt">//class="kw">operator+= class="type">void opMinus(const Complex &x,const Complex &y); class=class="str">"cmt">//class="kw">operator- class="type">void opMult(const Complex &x,const Complex &y); class=class="str">"cmt">//class="kw">operator* class="type">void opMultEq(const Complex &y); class=class="str">"cmt">//class="kw">operator*= (class="num">1-st variant) class="type">void opMultEq(const class="type">class="kw">double y); class=class="str">"cmt">//class="kw">operator*= (class="num">2-nd variant) class="type">void conjugate(const Complex &y); class=class="str">"cmt">//conjugation of complex numbers class="type">class="kw">double norm(); class=class="str">"cmt">//normalization }; class CFFT { class="kw">public: Complex Input[]; class=class="str">"cmt">//class="kw">input array of complex numbers Complex Output[]; class=class="str">"cmt">//output array of complex numbers class="kw">public: class="type">bool Forward(const class="type">uint N); class=class="str">"cmt">//direct Fourier transformation class="type">bool InverseT(const class="type">uint N,const class="type">bool Scale=true); class=class="str">"cmt">//weighted reverse Fourier transformation class="type">bool InverseF(const class="type">uint N,const class="type">bool Scale=class="kw">false); class=class="str">"cmt">//non-weighted reverse Fourier transformation class="type">void setCFFT(Complex &data1[],Complex &data2[],const class="type">uint N); class=class="str">"cmt">//set method(class="num">1-st variant) class="type">void setCFFT(Complex &data1[],Complex &data2[]); class=class="str">"cmt">//set method(class="num">2-nd variant) class="kw">protected:
「用 FFT 算序列自相关的一段实现」
这段 MT5 代码给出了一维随机时间序列选择性自相关函数(ACF)的第一种写法,核心思路是先对去均值后的观测序列做复化,再借快速傅里叶变换绕开直接套公式的 O(N²) 开销。 nFFT 取的是 2 的 nextpow2(ArraySize(res))+1 次幂,例如 res 长度为 500 时 nextpow2 得 9,nFFT 即为 2 的 10 次方 = 1024,所有中间数组都按这个尺寸重设。 代码里先把 res 拷进 rets1 并逐点减掉算术平均 m,再用 setComplexArr 封装成复数输入 Data1;随后声明 CFFT 实例 F、F1,通过 F.setCFFT(Data1,Data2) 绑定输入输出,后续由类内部 Rearrange / Perform / Scale 三步完成变换与加权。 对外汇或贵金属这类高波动品种跑这套,序列非平稳会让 ACF 滞后阶估计偏误,结果仅作概率层面的周期参考,实盘前务必在 MT5 策略测试器用真实 tick 复核。
class="type">void Rearrange(const class="type">uint N); class=class="str">"cmt">// regrouping class="type">void Perform(const class="type">uint N,const class="type">bool Inverse); class=class="str">"cmt">// implementation of transformation class="type">void Scale(const class="type">uint N); class=class="str">"cmt">// weighting }; class="type">int nFFT=pow(class="num">2,nextpow2(ArraySize(res))+class="num">1); class=class="str">"cmt">//power rate of two class="type">void autocorr(class="type">class="kw">double &ACF[],class="type">class="kw">double &res[],class="type">int nLags) class=class="str">"cmt">//class="num">1-st variant of function class=class="str">"cmt">/* selective autocorrelation function(ACF) for unidimensional stochastic time series ACF - output array of calculated values of the autocorrelation function; res - array of observation of stochastic time series; nLags - maximum number of lags the ACF is calculated for. */ { Complex Data1[],Data21[], class=class="str">"cmt">//class="kw">input arrays of complex numbers Data2[],Data22[], class=class="str">"cmt">//output arrays of complex numbers cData[]; class=class="str">"cmt">//array of conjugated complex numbers class="type">class="kw">double rA[][class="num">2]; class=class="str">"cmt">//auxiliary two-dimensional array of real numbers class="type">int nFFT=pow(class="num">2,nextpow2(ArraySize(res))+class="num">1); class=class="str">"cmt">//power rate of two ArrayResize(rA,nFFT);ArrayResize(Data1,nFFT); class=class="str">"cmt">//correction of array sizes ArrayResize(Data2,nFFT);ArrayResize(Data21,nFFT); ArrayResize(Data22,nFFT);ArrayResize(cData,nFFT); class="type">class="kw">double rets1[]; class=class="str">"cmt">//an auxiliary array for observing the series class="type">class="kw">double m=mean(res); class=class="str">"cmt">//arithmetical mean of the array res ArrayResize(rets1,nFFT); class=class="str">"cmt">//correction of array size for(class="type">int t=class="num">0;t<ArraySize(res);t++) class=class="str">"cmt">//copy the initial array of observation class=class="str">"cmt">// to the auxiliary one with correction by average rets1[t]=res[t]-m; setComplexArr(Data1,rets1); class=class="str">"cmt">//set class="kw">input array of complex numbers CFFT F,F1; class=class="str">"cmt">//initialize instances of the CFFT class F.setCFFT(Data1,Data2); class=class="str">"cmt">//initialize data-members for the instance F