通过差异化和熵值分析来探索市场"记忆"·进阶篇
◍ 分数差分怎样留住市场记忆
把价格序列做差分时,权重向量 ω 决定了历史样本对当前值的贡献方式。当差分阶数 d 取正整数,权重在有限项后归零,序列记忆被硬性裁剪,等价于只用最近若干根 K 线。 具体看 d = 1 的一阶差分,权重集合为 {1, −1, 0, 0, …},即当前值减前一值、更早起样本权重全为 0,市场更早的结构信息直接丢弃。 而分数型差分允许 d 为非整数,权重按二项展开衰减但永不严格为零,远端历史以极小权重持续参与计算,从而保留长程市场记忆。外汇与贵金属波动具聚集性与长依赖,这类记忆保留对建模有实际意义,但杠杆品种高风险,参数失配可能放大回撤。
用固定窗砍掉长尾系数
| 分数型差分默认是顺着整条时间序列往前卷,计算量随样本拉长而变重,变换后的序列还会带负偏移。Marcos Lopez De Prado 在《金融机器学习的进展》里改了玩法:只开一个固定宽度的观察窗,一旦系数模值 | ωk | 掉到阈值 τ 以下,这一项直接丢弃。 |
|---|
相比扩展窗口逐期累算,固定窗让每个原始点权重一致,复杂度降下来,还顺手消除了回漂。变换结果保留了价位记忆和噪声记忆,对做价差识别有用。 因为记忆性、不对称和过度峰度都在,这套输出不是高斯分布;但它可以是平稳的,意味着在 MT5 里拿来做特征工程比裸价格序列更耐造。外汇与贵金属杠杆高,平稳不等于能裸跑,验证前先上 demo 账户。
「用脚本把分数差分d跑成动画」
在 MT5 里写个脚本,就能把 0<d<1 的分数型差分过程直接动画化:d 从 0.05 起每次加 0.05 循环到 1.0,每帧间隔 500 毫秒重绘一次。肉眼能看到一个明确现象——d 越大,曲线越平稳,但旧价格的“记忆”衰减越快,这是长记忆性被逐步抹掉的直观证据。 权重向量 ω 由二项式展开系数递推得到,在整个序列上固定不变,不需要随新 tick 重算,所以分数差分比常规滚动标准化更省算力。下面两段函数是核心:get_weight_ffd 按阈值 thres 截断权重,frac_diff_ffd 用固定权重对对数收盘价做滑动点积。 //+------------------------------------------------------------------+ void get_weight_ffd(double d, double thres, int lim, double &w[]) { // 定义函数:输入差分度d、截断阈值thres、长度上限lim,输出权重数组w(引用传参) ArrayResize(w,1); // 先把w扩成1个元素 ArrayInitialize(w,1.0); // 初始权重设为1.0(即ω0=1) ArraySetAsSeries(w,true); // 把w设为时间序列方向(索引0为最新) int k = 1; // 递推项数计数器k从1开始 int ctr = 0; // 当前数组下标ctr从0开始 double w_ = 0; // 暂存本次算出的权重 while (ctr != lim - 1) { // 循环直到达到长度上限 w_ = -w[ctr] / k * (d - k + 1); // 用递推式 ω_k = -ω_{k-1}/k * (d-k+1) 计算新权重 if (MathAbs(w_) < thres) break; // 权重绝对值小于阈值就截断退出,避免无穷级数 ArrayResize(w,ArraySize(w)+1); // 数组扩容1格 w[ctr+1] = w_; // 写入新权重 k += 1; // k递增 ctr += 1; // 下标递增 } } //+------------------------------------------------------------------+ void frac_diff_ffd(double &x[], double d, double thres, double &output[]) { // 分数差分主函数:x为原序列,d和thres同上,output为输出(引用) double w[]; // 局部权重数组 get_weight_ffd(d, thres, ArraySize(x), w); // 先算权重,长度上限用x的长度 int width = ArraySize(w) - 1; // 窗口宽度=权重数-1 ArrayResize(output, width); // 先给output预留width空间 ArrayInitialize(output,0.0); // 清零 ArraySetAsSeries(output,true); // output时间序列方向 ArraySetAsSeries(x,true); // x时间序列方向 ArraySetAsSeries(w,true); // w时间序列方向 int o = 0; // output写入下标 for(int i=width;i<ArraySize(x);i++) { // 从width位置开始,保证窗口不越界 ArrayResize(output,ArraySize(output)+1); // 每算一个点就扩output for(int l=0;l<ArraySize(w);l++) // 遍历所有权重 output[o] += w[l]*x[i-width+l]; // 点积:权重乘对应滞后价格累加 o++; } // 写完一个点,o前进 ArrayResize(output,ArraySize(output)-width); // 去掉开头预留的width个无效单元 } //+------------------------------------------------------------------+
| // | Script program start function |
|---|
//+------------------------------------------------------------------+ void OnStart() // 脚本入口 { for(double i=0.05; i<1.0; plotFFD(i+=0.05,1e-5)) // 从0.05到1.0,步长0.05,每轮画一帧d值,阈值1e-5 } //+------------------------------------------------------------------+ void plotFFD(double fd, double thresh) { // 绘图函数:fd为当前d,thresh为权重阈值 double prarr[], out[]; // 价格数组与输出数组 CopyClose(_Symbol, 0, 0, hist, prarr); // 复制当前品种0周期近hist根收盘价(hist需提前定义) for(int i=0; i < ArraySize(prarr); i++) prarr[i] = log(prarr[i]); // 转对数价,使差分更接近稳态 frac_diff_ffd(prarr, fd, thresh, out); // 算分数差分 GraphPlot(out,1); Sleep(500); // 画图并停500毫秒形成动画;GraphPlot为自定义绘图辅助函数 } 把上面代码丢进 MT5 脚本跑一遍,调 hist 大小(比如 500)和 thres(默认 1e-5),就能验证 d 接近 1 时序列最平但滞后最严重。外汇与贵金属价格具高波动与跳空风险,分数差分仅改变序列统计特性,不预示方向,实盘使用前请在历史数据上自测。
class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void get_weight_ffd(class="type">class="kw">double d, class="type">class="kw">double thres, class="type">int lim, class="type">class="kw">double &w[]) { ArrayResize(w,class="num">1); ArrayInitialize(w,class="num">1.0); ArraySetAsSeries(w,true); class="type">int k = class="num">1; class="type">int ctr = class="num">0; class="type">class="kw">double w_ = class="num">0; class="kw">while (ctr != lim - class="num">1) { w_ = -w[ctr] / k * (d - k + class="num">1); if (MathAbs(w_) < thres) class="kw">break; ArrayResize(w,ArraySize(w)+class="num">1); w[ctr+class="num">1] = w_; k += class="num">1; ctr += class="num">1; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void frac_diff_ffd(class="type">class="kw">double &x[], class="type">class="kw">double d, class="type">class="kw">double thres, class="type">class="kw">double &output[]) { class="type">class="kw">double w[]; get_weight_ffd(d, thres, ArraySize(x), w); class="type">int width = ArraySize(w) - class="num">1; ArrayResize(output, width); ArrayInitialize(output,class="num">0.0); ArraySetAsSeries(output,true); ArraySetAsSeries(x,true); ArraySetAsSeries(w,true); class="type">int o = class="num">0; for(class="type">int i=width;i<ArraySize(x);i++) { ArrayResize(output,ArraySize(output)+class="num">1); for(class="type">int l=class="num">0;l<ArraySize(w);l++) output[o] += w[l]*x[i-width+l]; o++; } ArrayResize(output,ArraySize(output)-width); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Script program start function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { for(class="type">class="kw">double i=class="num">0.05; i<class="num">1.0; plotFFD(i+=class="num">0.05,class="num">1e-5)) } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void plotFFD(class="type">class="kw">double fd, class="type">class="kw">double thresh) { class="type">class="kw">double prarr[], out[]; CopyClose(_Symbol, class="num">0, class="num">0, hist, prarr); for(class="type">int i=class="num">0; i < ArraySize(prarr); i++) prarr[i] = log(prarr[i]); frac_diff_ffd(prarr, fd, thresh, out); GraphPlot(out,class="num">1); Sleep(class="num">500); }
◍ 用分数型差分做可调指标
把分数型差分塞进指标,核心是为了在 EA 里直接调参用,而不是每次手算。三个参数必须暴露给用户:差分度 d、剔除过度权重的阈值、以及历史显示深度。完整代码不贴,但权重计算函数和之前推导的一致,没改逻辑。 指标缓冲区填充由 frac_diff_ffd 完成,它吃进价格序列和权重向量,按显示深度往缓冲区写值。图例里幂指数 0.3 对比 0.9 能直观看到:d 越小越贴近原价格,d 越大序列被磨平得越厉害。 这个指标的价值在于它量化了‘动态变化里还剩多少信息’。d 拉高,价位层面的噪声丢失,序列更平稳;但周期性成分往往留下来了,反而可能成为后续预测的锚点。外汇和贵金属这种高波动品种,用前先在小周期回看,避免把平稳误判为信号消失。 下一步自然走到信息论:用熵来度量缓冲区内真实的数据量,比肉眼看平整度靠谱。
frac_diff_ffd(weights, price, ind_buffer, hist_display, prev_calculated != class="num">0);
从 Shannon 熵到 SampEn 的演化路径
信息熵由 Claude Shannon 引入,用来度量事件所携带的信息量:事件越确定,熵越低。对取有限个值的随机变量 X,其熵按分布概率取对数求和得到,单位随对数底不同可为 bits、nat 或 hartleys。Shannon 框架对短期、含噪的金融序列并不友好,Steve Pincus 与 Rudolf Kalman 因此提出近似熵(ApEn),专门刻画价格偏离恒定值时的不规则性与不可预测性。 一个直观对照能说明问题:序列 A 为 (10,20,10,20,10,20…) 严格交替,序列 B 为 (10,10,20,10,20,20…) 中 10 与 20 各以 1/2 概率随机出现。两者均值与方差完全一致,但 A 可凭前值完美预测下一值,B 则无任何预测可能——标准偏差测的是离散程度,ApEn 才测得出这种规则与随机的本质分野。 ApEn 后来被 Joshua Richman 和 Randall Moorman 指出缺陷:熵值依赖样本长度,且相关序列的数值一致性差。他们给出改进版样本熵(SampEn),计算更轻量、对长度依赖更弱。后续我们直接采用 SampEn 做行情不规则度测算,外汇与贵金属属高风险品种,熵值仅指示无序程度,不构成方向判断。
「用样本熵给价格增量测复杂度」
样本熵(SampEn)是近似熵的改进版,用来量化一段价格序列的不规则程度。给定嵌入维数 m 和容差 r,它比较长度为 m 与 m+1 的向量在切比雪夫距离下相互匹配的概率比,再取对数。A 是 m+1 长度匹配的对数,B 是 m 长度匹配的对数,A 恒小于等于 B,所以算出来的值只会是零或正数。 值越低,说明这段行情自相似性越强、噪声越低,趋势或周期成分可能更突出;值偏高则随机游走倾向更大。实战里常用 m=2、r=0.2*std(std 为序列标准差),对外汇或贵金属这种高杠杆品种,样本熵只是描述性指标,不预示方向,请始终把它当风控辅助而非信号本身。 下面这段 MQL5 把单序列样本熵算清楚了:err 先用标准差乘 r 得到容差;双重循环扫所有不重叠模板对,先判 m 长匹配记 Cm,再判多一位的匹配记 Cm1;最后返回 log(Cm/Cm1)。若两个计数器有值就出熵,否则返 0。 double sample_entropy(double &data[], int m, double r, int N, double sd) { int Cm = 0, Cm1 = 0; double err = 0.0, sum = 0.0; err = sd * r; for (int i = 0; i < N - (m + 1) + 1; i++) { for (int j = i + 1; j < N - (m + 1) + 1; j++) { bool eq = true; for (int k = 0; k < m; k++) { if (MathAbs(data[i+k] - data[j+k]) > err) { eq = false; break; } } if (eq) Cm++; int k = m; if (eq && MathAbs(data[i+k] - data[j+k]) <= err) Cm1++; } } if (Cm > 0 && Cm1 > 0) return log((double)Cm / (double)Cm1); else return 0.0; } 顺手补的交叉样本熵(cross_SampEn)能比两条序列(比如金价与美指)的同步复杂度,注释里写了 u/v 双输入与 m/r 含义;不过单序列版本已够日常筛查用,交叉版留着做相关性异常探测即可。
class="type">class="kw">double sample_entropy(class="type">class="kw">double &data[], class="type">int m, class="type">class="kw">double r, class="type">int N, class="type">class="kw">double sd) { class="type">int Cm = class="num">0, Cm1 = class="num">0; class="type">class="kw">double err = class="num">0.0, sum = class="num">0.0; err = sd * r; for (class="type">int i = class="num">0; i < N - (m + class="num">1) + class="num">1; i++) { for (class="type">int j = i + class="num">1; j < N - (m + class="num">1) + class="num">1; j++) { class="type">bool eq = true; for (class="type">int k = class="num">0; k < m; k++) { if (MathAbs(data[i+k] - data[j+k]) > err) { eq = class="kw">false; class="kw">break; } } if (eq) Cm++; class="type">int k = m; if (eq && MathAbs(data[i+k] - data[j+k]) <= err) Cm1++; } } if (Cm > class="num">0 && Cm1 > class="num">0) class="kw">return log((class="type">class="kw">double)Cm / (class="type">class="kw">double)Cm1); else class="kw">return class="num">0.0; } class=class="str">"cmt">// Calculate the cross-sample entropy of class="num">2 signals class=class="str">"cmt">// u : signal class="num">1 class=class="str">"cmt">// v : signal class="num">2 class=class="str">"cmt">// m : length of the patterns that compared to each other class=class="str">"cmt">// r : tolerance class=class="str">"cmt">// class="kw">return the cross-sample entropy value class="type">class="kw">double cross_SampEn(class="type">class="kw">double &u[], class="type">class="kw">double &v[], class="type">int m, class="type">class="kw">double r) { class="type">class="kw">double B = class="num">0.0; class="type">class="kw">double A = class="num">0.0; if (ArraySize(u) != ArraySize(v)) Print("Error : lenght of u different than lenght of v"); class="type">int N = ArraySize(u); for(class="type">int i=class="num">0;i<(N-m);i++) { for(class="type">int j=class="num">0;j<(N-m);j++) { class="type">class="kw">double ins[]; ArrayResize(ins, m); class="type">class="kw">double ins2[]; ArrayResize(ins2, m); ArrayCopy(ins, u, class="num">0, i, m); ArrayCopy(ins2, v, class="num">0, j, m); B += cross_match(ins, ins2, m, r) / (N - m); ArrayResize(ins, m+class="num">1); ArrayResize(ins2, m+class="num">1);