广义赫斯特指数与方差比检验:用分形特征筛出均值回复品种(基础篇)
(1/3)· 从 R/S 到 GHE,基础篇讲清幂律矩估计与 MQL5 落地的第一步
多数交易者把赫斯特指数等同于那套老旧的 R/S 重标定区间法,遇到不同时间尺度下波动结构突变就束手无策。广义赫斯特指数把阶矩参数 q 放开,才真正能区分趋势、随机与均值回复三种状态。本篇是三步系列的第一部分,先把 GHE 数学骨架与 MQL5 实现底座搭好,为后面识别外汇品种和写信号指标铺路。
◍ 用 MT5 跑通广义赫斯特与方差比
想在 MT5 里判断一段外汇或贵金属价格是趋势市还是均值回归市,广义赫斯特指数 H 和方差比检验是两条硬指标。H 大于 0.5 倾向趋势延续,小于 0.5 倾向反转,等于 0.5 接近随机游走;方差比偏离 1 越远,序列越不像有效市场。 下面这段 MQL5 脚本把两种算法直接压进一个函数里,加载到 EURUSD 的 H1 周期就能出数。外汇与贵金属杠杆高、跳空频繁,结论只作概率参考,实盘前务必在策略测试器里跑历史样本。 调用时传入价格数组与滞后阶数 q,函数先算重标极差再算方差比,返回结构体里带 H 值和 VR(q)。把 q 从 2 调到 10,能看出不同周期尺度下的记忆性差异——这是单看均线给不了的信息。
「用广义赫斯特指数筛均值回复品种」
原版赫斯特指数常用的 R/S 重标定范围法,对短周期金融序列容易给出偏高的持续性误判。本文换用广义赫斯特指数(GHE),它把分形维的估计从单一标度扩展到多标度,能更细地区分序列是趋势游走、随机游走还是均值回复。 光看 GHE 数值还不够稳,我们引入方差比检验(VRT)做统计交叉验证:若 VRT 在多个滞后阶上显著拒绝随机游走原假设,且 GHE 落于 0 到 0.5 区间,才可把该品种暂列为均值回复候选。外汇与贵金属杠杆高、跳空频繁,这类统计特征只代表历史样本倾向,实盘仍需控仓。 本系列后续会直接给出一套基于 GHE 的进场/出场指标代码,并在最基础的 EA 里跑一遍,让你开 MT5 就能复现候选品种的筛选结果。
广义赫斯特指数到底在算什么
赫斯特指数衡量时间序列的缩放特性:当时间尺度变化时,序列内部模式如何自相似地重复。对平稳序列而言,它相对几何随机游走变化更慢,我们用方差充当扩散率指标,看后续值偏离起点的程度。 公式里的 K 是任意滞后期,可取小于序列长度的任何正整数;最大滞后由你自行决定,所以赫斯特指数本质是在看不同滞后下方差的缩放行为,按幂律定义。 广义赫斯特指数(GHE)把原式中的 2 换成一个可变阶数 q,变成对 q 阶矩的缩放分析。q=1 时描述绝对偏差的缩放;q=2 对研究长程依赖性最关键。实际在 MT5 里,你可以取一段 XAUUSD 的 H1 收盘价,自己算不同 q 下的 GHE,看贵金属的高风险走势是否存在长记忆倾向。
◍ MT5里把GHE写成可调用函数
在 MT5 写广义赫斯特指数(GHE),核心就是封装一个 gen_hurst(),放在 GHE.mqh 里,头部先 include 一个 VectorMatrixTools.mqh,里面提供 arange、slice 这类向量矩阵初始化辅助函数。gen_hurst() 给了两种入口:直接喂 vector,或喂 array;必填整数 q(阶数,不能小于1),可选 lower / upper 限定滞后区间,默认值以外不能超过数据长度一半,lower 自身不能小于2且必须小于 upper。 函数进来先跑参数体检,任一不满足就向“专家”标签页打印错误字符串并返回 EMPTY_VALUE,避免脏数据进计算。主体是一个 lower→upper 的外循环:每个滞后 i 用 arange 造长度为 i 的 x_vector,把 mcord 矩阵扩成 i 行1列;内循环调 diff_array() 算差分存进 dv 和 Y,得到该滞后的方差写回 mcord。外循环末尾更新主 H 矩阵,最后返回 H 矩阵除以 q 的均值——这就是该 q 阶下的 GHE 估计。 验证别只靠眼睛。官方配套 GHE.ex5 是个交互 EA,能生成指定特征的随机序列、实时拖参数看 GHE 输出,还支持先对数变换再算,方便比对预处理有没有用。真实行情噪音重、样本量一变估计就飘,所以要做方差比(VR)检验看显著性,而不是盯着一个数就下结论。外汇和贵金属波动受事件驱动,样本敏感度高,GHE 结论只代表历史序列的概率特征,实盘仍属高风险。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| VectorMatrixTools.mqh | class=class="str">"cmt">//| Copyright class="num">2023, MetaQuotes Ltd. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2023, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|Vector arange initialization | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">template<class="kw">typename T> class="type">void arange(vector<T> &vec,T value=class="num">0.0,T step=class="num">1.0) { for(class="type">ulong i=class="num">0; i<vec.Size(); i++,value+=step) vec[i]=value; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Vector sliced initialization | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">template<class="kw">typename T> class="type">void slice(vector<T> &vec,class="kw">const vector<T> &toCopyfrom,class="type">ulong start=class="num">0,class="type">ulong stop=ULONG_MAX, class="type">ulong step=class="num">1) { start = (start>=toCopyfrom.Size())?toCopyfrom.Size()-class="num">1:start; stop = (stop>=toCopyfrom.Size())?toCopyfrom.Size()-class="num">1:stop; step = (step==class="num">0)?class="num">1:step; class="type">ulong numerator = (stop>=start)?stop-start:start-stop; class="type">ulong size = (numerator/step)+class="num">1; if(!vec.Resize(size)) { Print(__FUNCTION__ " invalid slicing parameters for vector initialization"); class="kw">return; } if(stop>start) { for(class="type">ulong i =start, k = class="num">0; i<toCopyfrom.Size() && k<vec.Size() && i<=stop; i+=step, k++) vec[k] = toCopyfrom[i]; } else {
「切片赋值与 OLS 设计矩阵的底层拼装」
把数组按起止位置和步长灌进 vector,核心在 assign 模板。它先夹紧 start 和 stop 不越界,step 为 0 时强制置 1,再用 numerator/step+1 算目标长度;若和原 vector 尺寸不符就 Resize,失败直接 Print 报错返回。 正向切片走 i+=step 从 start 到 stop,反向则把 i 转成 long 做 i-=step,从 start 递减到 stop,两种分支都靠 k 同步写入 vec[k],因此 vec 里元素的顺序严格跟随遍历方向。 rangetrend 用两列拼 matrix:col1 是 arange 生成的等差列(默认从 0.0 步长 1.0),col2 是全 1 列,矩阵固定 Resize 成 r×2。这个结构正好是带截距项的一元回归输入骨架。 olsdmatrix 进一步用 assign 从外部数组切出 col0 作为第一 explanatory 列,行数 r 由切片结果决定;后面只需再补一列常数就能交給 OLS 求解。外汇与贵金属行情受杠杆与跳空影响,回测这类矩阵拟合结果仅代表历史样本关系,实盘胜率可能明显衰减,务必小仓位验证。
class="kw">template<class="kw">typename T> class="type">void assign(vector<T> &vec,class="kw">const T &toCopyfrom[],class="type">ulong start=class="num">0,class="type">ulong stop=ULONG_MAX, class="type">ulong step=class="num">1) { start = (start>=toCopyfrom.Size())?toCopyfrom.Size()-class="num">1:start; stop = (stop>=toCopyfrom.Size())?toCopyfrom.Size()-class="num">1:stop; step = (step==class="num">0)?class="num">1:step; class="type">ulong numerator = (stop>=start)?stop-start:start-stop; class="type">ulong size = (numerator/step)+class="num">1; if(size != vec.Size() && !vec.Resize(size)) { Print(__FUNCTION__ " invalid slicing parameters for vector initialization"); class="kw">return; } if(stop>start) { for(class="type">ulong i =start, k = class="num">0; i<class="type">ulong(toCopyfrom.Size()) && k<vec.Size() && i<=stop; i+=step, k++) vec[k] = toCopyfrom[i]; } else { for(class="type">long i = class="type">long(start), k = class="num">0; i>-class="num">1 && k<class="type">long(vec.Size()) && i>=class="type">long(stop); i-=class="type">long(step), k++) vec[k] = toCopyfrom[i]; } } class="kw">template<class="kw">typename T> class="type">void rangetrend(matrix<T> &mat,T value=class="num">0.0,T step=class="num">1.0) { class="type">ulong r = mat.Rows(); vector col1(r,arange,value,step); vector col2 = vector::Ones(r); if(!mat.Resize(r,class="num">2) || !mat.Col(col1,class="num">0) || !mat.Col(col2,class="num">1)) { Print(__FUNCTION__ " matrix initialization error: ", GetLastError()); class="kw">return; } } class="kw">template<class="kw">typename T> class="type">void olsdmatrix(matrix<T> &mat,class="kw">const T &toCopyfrom[],class="type">ulong start=class="num">0,class="type">ulong stop=ULONG_MAX, class="type">ulong step=class="num">1) { vector col0(class="num">1,assign,toCopyfrom,start,stop,step); class="type">ulong r = col0.Size();
把向量喂给 Hurst 计算前的搬运与差分
向量到数组的搬运看似琐碎,却是后面所有统计函数的必经路。vecToArray 先卡住空向量(Size<1 直接报错返回 false),再比对目标数组长度,不一致就 ArrayResize 到输入长度;任一环节失败都打印 GetLastError() 方便在 MT5 Experts 日志里定位。 difference 函数做的是一阶差分:用 slice 从原向量分别取 [1, Size-1] 和 [0, Size-2] 两段等长子向量,相减得到长度为 Size-1 的新序列。这一步对应价格行为里把收盘价序列转成收益率扰动,Hurst 估计对原始级数敏感,差分后能压住趋势项干扰。 general_hurst 的重载版接收 vector 引用,内部调 vecToArray 把数据倒进 double 数组,失败则返回 EMPTY_VALUE(宏值通常是 -1.7976931348623158e+308)。外汇与贵金属杠杆高、跳空频繁,差分后样本若含极端跳点,Hurst 指数可能偏向 0.5 伪随机,建议先用小周期 XAUUSD 的 M5 收盘序列手跑一遍验证。
if(!r) { Print(__FUNCTION__," failed to initialize first column "); class="kw">return; } vector col1 = vector::Ones(r); if(!mat.Resize(r,class="num">2) || !mat.Col(col0,class="num">0) || !mat.Col(col1,class="num">1)) { Print(__FUNCTION__ " matrix initialization error: ", GetLastError()); class="kw">return; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|vector to array | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool vecToArray(class="kw">const vector &in, class="type">class="kw">double &out[]) { class=class="str">"cmt">//--- if(in.Size()<class="num">1) { Print(__FUNCTION__," Empty vector"); class="kw">return false; } class=class="str">"cmt">//--- if(class="type">ulong(out.Size())!=in.Size() && ArrayResize(out,class="type">int(in.Size()))!=class="type">int(in.Size())) { Print(__FUNCTION__," resize error ", GetLastError()); class="kw">return false; } class=class="str">"cmt">//--- for(class="type">uint i = class="num">0; i<out.Size(); i++) out[i]=in[i]; class=class="str">"cmt">//--- class="kw">return true; class=class="str">"cmt">//--- } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| difference a vector | class=class="str">"cmt">//+------------------------------------------------------------------+ vector difference(class="kw">const vector &in) { class=class="str">"cmt">//--- if(in.Size()<class="num">1) { Print(__FUNCTION__," Empty vector"); class="kw">return vector::Zeros(class="num">1); } class=class="str">"cmt">//--- vector yy,zz; class=class="str">"cmt">//--- yy.Init(in.Size()-class="num">1,slice,in,class="num">1,in.Size()-class="num">1,class="num">1); class=class="str">"cmt">//--- zz.Init(in.Size()-class="num">1,slice,in,class="num">0,in.Size()-class="num">2,class="num">1); class=class="str">"cmt">//--- class="kw">return yy-zz; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+--------------------------------------------------------------------------+ class=class="str">"cmt">//|overloaded gen_hurst() function that works with series contained in vector| class=class="str">"cmt">//+--------------------------------------------------------------------------+ class="type">class="kw">double general_hurst(vector &data, class="type">int q, class="type">int lower=class="num">0,class="type">int upper=class="num">0) { class="type">class="kw">double series[]; if(!vecToArray(data,series)) class="kw">return EMPTY_VALUE; else