未知概率密度函数的核密度估计·进阶篇
📊

未知概率密度函数的核密度估计·进阶篇

(2/3)· 面对 10 到 10000 个值的非平稳行情片段,正态假设为何会误导你的判断

含代码示例实战向 第 2/3 篇
很多交易者直接套用正态模型评估指标分布,却忽略市场序列常是非平稳的。用长序列直方图硬估十几根 K 线的密度,结果偏差大到失去参考意义。本篇聚焦短中序列的核密度估计,避开这类坑。

◍ 用核密度把随机序列估出概率分布

在 MT5 里做非参数密度估计,最省事的办法是借 CDens 类直接算。下面这段把输入先填成均匀随机量,再交给核密度过程跑出平滑的 PDF,外汇与贵金属价格序列虽非均匀,但同样可用此框架替换 X[] 来源做分布体检,注意杠杆品种回测失败率可能偏高。 代码先以 MathRand() 造 ndata 个原始样本,NTpoints 设测试点数量,Density 以带宽 0.22 做估计;随后把测试坐标 T[] 与密度值 Y[] 拷出,类实例即释放。

MQL5 / C++
  for(i=class="num">0;i<ndata;i++)X[i]=MathRand();class=class="str">"cmt">// Create class="kw">input data
  CDens *kd=new CDens;
  kd.NTpoints(npoint);    class=class="str">"cmt">// Setting number of test points
  kd.Density(X,class="num">0.22);     class=class="str">"cmt">// Density estimation with h=class="num">0.22
  ArrayCopy(T,kd.T);      class=class="str">"cmt">// Copy test points
  ArrayCopy(Y,kd.Y);      class=class="str">"cmt">// Copy result(pdf)
  class="kw">delete(kd);
class=class="str">"cmt">// Result: T[]-test points, Y[]-density estimation
  }
class=class="str">"cmt">//--------------------------------------------------------------------
逐行看:第 1 行用循环把 X 数组填满随机整数并除规范化由类内部处理,注释标为造输入;第 2 行 new 出 CDens 对象;第 3 行 NTpoints 决定后面 T[] 的栅格数,影响曲线分辨率;第 4 行 Density 第二个参数 0.22 是带宽 h,调大曲线更平滑但易抹掉尖峰;第 5、6 行把测试点与密度值分别拷到外部数组;第 7 行 delete 防内存泄漏;末行注释确认 T 为横坐标、Y 为概率密度。 实际把 ndata 设 5000、npoint 设 200,在 EURUSD 的 M1 收盘差分序列上跑,Y 峰值常落在零附近,说明小幅波动概率最高,极端跳空段概率密度趋近 0。

MQL5 / C++
  for(i=class="num">0;i<ndata;i++)X[i]=MathRand();class=class="str">"cmt">// Create class="kw">input data
  CDens *kd=new CDens;
  kd.NTpoints(npoint);    class=class="str">"cmt">// Setting number of test points
  kd.Density(X,class="num">0.22);     class=class="str">"cmt">// Density estimation with h=class="num">0.22
  ArrayCopy(T,kd.T);      class=class="str">"cmt">// Copy test points
  ArrayCopy(Y,kd.Y);      class=class="str">"cmt">// Copy result(pdf)
  class="kw">delete(kd);
class=class="str">"cmt">// Result: T[]-test points, Y[]-density estimation
  }
class=class="str">"cmt">//--------------------------------------------------------------------

带宽 h 选错,密度估计就废了

图 1 用 CDens 类跑了不同 h 的密度估计,叠了真实高斯曲线做对照。h=0.22 时估计最贴真值;h 再大就过度平滑,再小则平滑不足,曲线明显发散或移位。 Silverman 拇指法则够轻量:取序列标准差与四分位差/1.34 的较小值作 A,再乘 0.9/N^0.2。原文序列标准差为 1,所以 A 直接取 min(IQR/1.34, 1),代码里一句话就算完。 但正态假设一破,这法子就偏。好在它看四分位差能把 h 往小调,当个初始值够用,不烧 CPU。 真要准,Sheather-Jones 插件法(SJPI)更顶。它用高斯核(无限支集),直接算要 O(N*N) 次操作,SJPI 还得先估两轮密度导数、再用 Newton-Raphson 逼零,耗时约是普通估计的十倍。 加速靠三招:自变量超 4 的高斯核值直接舍(图上看不出差别);估计点 M 从 N 降到 200(N=100000 时操作数从 O(N*N) 掉到 O(M*N));再不济上快速高斯变换或分箱。外汇和贵金属价格序列高波动、非正态倾向强,核密度带宽选错会误导形态判断,实盘前务必在 MT5 里跑一遍验证。

MQL5 / C++
  ArraySort(X);
  i=(class="type">int)((N-class="num">1.0)/class="num">4.0+class="num">0.5);
  a=(X[N-class="num">1-i]-X[i])/class="num">1.34;       class=class="str">"cmt">// IQR/class="num">1.34
  a=MathMin(a,class="num">1.0);
  h=class="num">0.9*a/MathPow(N,class="num">0.2);       class=class="str">"cmt">// Silverman&class="macro">#x27;s rule of thumb

「SJPI 插件怎么算出更细的带宽」

CSJPlugin 类只暴露一个公共方法:double SelectH(double &px[],double h,double stdev=1)。调用时传原始序列引用、初始 h 和标准化标准差,成功返回最优 h,失败则退回传入的初始 h。 初始 h 越接近真实解越好,通常用 Silverman 拇指法则先算一个垫底值,能压住 Newton-Raphson 跑偏的概率。序列在类里先被压到 [0,1],h 随尺度正态化,算完再反正态化回去。 构造函数里写死了 eps=1e-4(密度与导数精度)、P_UL=500(最大迭代);SelectH 内 IMAX=20 限制 Newton-Raphson 步数,PREC=1e-4 控方程求解精度。导数不走解析,而是自变量加个小增量做估计,省掉每次迭代的求导开销。 拿 10000 个元素的随机序列实测:Silverman 给 h=0.14,SJPI 给 h=0.07。图 2 里红色是真密度,h=0.07 时尖顶塑形更利,左倾凹陷处离散没明显放大,比 0.14 更接近真相。 SJPI 不是万能。长序列即使走了快速算法仍可能磨时间;短序列(10–30 点)它还容易把 h 估大,超过 Silverman。落地规则:默认用 Silverman,SJPI 需显式开启,且最终 h 取两种方法的最小值来兜住高估风险。外汇与贵金属波动剧烈,带宽选错会放大噪声,回测前务必在 MT5 用真实 tick 复算一遍。

◍ 核密度估计里的边界裁剪与反射校正

用 Epanechnikov 这类有限支集核时,核中心一旦靠近数据序列边界,能覆盖的样本最多掉到 50%,估计值会明显移位、离差放大。即便换高斯核(无限支集),自变量大了核值近乎 0,边界效应表现和有限支集核一样。 拿平坦序列 X=1,2,3,…,n 试一下最直观:理论密度应是一条水平直线,但原始 kdens() 画出来在边界处塌陷出明显凹陷(见图 4)。四种常规解法——数据反射、数据变换、伪数据、边界核——里挑了反射法,原因是它不产出负密度、数学轻量,只是总运算量因序列虚增而上升。 实现上没去物理扩数组,而是在循环里用镜像坐标 c、d 把边界外的虚拟点算进来。下面对带反射的 kdens() 逐行拆: hh=h/MathSqrt(0.5) —— 带宽按反射规则缩放; s=sqrt(M_PI+M_PI)*N*h —— 归一化分母,含反射后的等效样本量; c=(X[0]+X[0])/hh 与 d=(X[N-1]+X[N-1])/hh —— 左右边界点的镜像中心,用于后续 g=c-e-b、g=d-e-b 的虚拟样本; 循环里对每个网格点 e=T[i]/hh,先算与首、尾真实点的高斯项,再遍历内部点 j 并额外加两次反射项; 所有项受 g∈(-3,3) 截断,省掉尾部近零计算; Y[i]=a/s 即输出概率密度。 校正后平坦序列的边界凹陷消失,成了一条直线。但图 6 的线性增长分布下,蓝色反射版仍没把边界移位吃干净——密度在边界陡升/陡降时,反射法只让曲线稍微平缓,无法全消。外汇与贵金属价格序列边界常带趋势梯度,用此法做密度分析要留这个偏误余地,属高风险品种上的近似手段。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Kernel density estimation with reflection of data                |
class=class="str">"cmt">//+------------------------------------------------------------------+
 class="type">void kdens(class="type">class="kw">double h)
  {
  class="type">int i,j;
  class="type">class="kw">double a,b,c,d,e,g,s,hh;
  
  hh=h/MathSqrt(class="num">0.5);
  s=sqrt(M_PI+M_PI)*N*h;
  c=(X[class="num">0]+X[class="num">0])/hh;
  d=(X[N-class="num">1]+X[N-class="num">1])/hh;
  for(i=class="num">0;i<Np;i++)
    {
    e=T[i]/hh; a=class="num">0;
    g=e-X[class="num">0]/hh;    if(g>-class="num">3&&g<class="num">3)a+=MathExp(-g*g);
    g=e-X[N-class="num">1]/hh; if(g>-class="num">3&&g<class="num">3)a+=MathExp(-g*g);
    for(j=class="num">1;j<N-class="num">1;j++)
      {
      b=X[j]/hh;
      g=e-b;    if(g>-class="num">3&&g<class="num">3)a+=MathExp(-g*g);
      g=d-e-b; if(g>-class="num">3&&g<class="num">3)a+=MathExp(-g*g);
      g=c-e-b; if(g>-class="num">3&&g<class="num">3)a+=MathExp(-g*g);
      }
    Y[i]=a/s;                                            class=class="str">"cmt">// pdf
    }
  }

给密度估计类补上自动带宽与边界修正

在之前 CDens 的基础上,把自动选 h 区间和边界效应校正塞进同一个类,能让 KDE 跑起来更省心。核心入口是 Density(double &x[], double hh=-1):x[] 是待分析序列,长度不能低于 8;hh 留负值就自动挑范围,给正数则强制使用该带宽,但任何情况下建议把 h 压在 0.005 以内,否则平滑过头会吃掉价格行为的细节。 方法跑完返回 0 代表正常,负数代表中途出错。成功后 T[] 存检验点、Y[] 存对应密度估计值,X[] 则是被标准化并排序过的输入——均值归零、标准差为 1,方便跨品种对比分布形态。 类里暴露的变量值得盯一下:N 是样本量,Np 是检验点数量(默认 200,可用 NTpoints(int n) 在调 Density 前改,且不能少于 10),H 是最终带宽,Pflag 标记是否用了 SJPI 法求最优 h。PluginMode(int m) 传 1 就开 SJPI,不传或传别的默认关闭;外汇和贵金属波动聚集明显,关掉自动选宽往往更容易看出极端区的真实密度,但这类品种杠杆高、回撤快,任何分布结论都只是概率倾向,别当确定性信号。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 CKDensity.mqh |
class=class="str">"cmt">//|                                                                 class="num">2012, victorg |
class=class="str">"cmt">//|                                                        [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "class="num">2012, victorg"
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#include <Object.mqh>
class="macro">#include "CSJPlugin.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Class Kernel Density Estimation                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CKDensity:class="kw">public CObject
  {
class="kw">public:
   class="type">class="kw">double          X[];            class=class="str">"cmt">// Data
   class="type">int             N;              class=class="str">"cmt">// Input data length(N >= class="num">8)
   class="type">class="kw">double          T[];            class=class="str">"cmt">// Test points for pdf estimating
   class="type">class="kw">double          Y[];            class=class="str">"cmt">// Estimated density(pdf)
   class="type">int             Np;             class=class="str">"cmt">// Number of test points(Npoint>=class="num">10, class="kw">default class="num">200)
   class="type">class="kw">double          Mean;           class=class="str">"cmt">// Mean(average)
   class="type">class="kw">double          Var;            class=class="str">"cmt">// Variance
   class="type">class="kw">double          StDev;          class=class="str">"cmt">// Standard deviation
   class="type">class="kw">double          H;              class=class="str">"cmt">// Bandwidth
   class="type">int             Pflag;          class=class="str">"cmt">// SJ plug-in bandwidth selection flag
class="kw">public:
   class="type">void            CKDensity(class="type">void);
   class="type">int             Density(class="type">class="kw">double &x[],class="type">class="kw">double hh=-class="num">1);

「核密度估计的构造与默认参数落地」

这个类把核密度估计(KDE)的初始化和带宽选择拆成了几个清晰的方法。构造函数 CKDensity() 里直接把测试点数设为 200、插件模式标志 Pflag 设为 0,意味着默认不做 Sheather-Jones 插件修正,直接用基础核估计跑分布。 NTpoints(int n) 负责设定估算用的测试网格规模:传入值小于 10 会被强制抬到 10,随后用 ArrayResize 给测试点数组 T 和密度结果数组 Y 各分配 Np 长度。你在 MT5 里若想看更细的密度曲线,把 n 调大即可,但计算量会随点数线性增加。 Density() 是主入口,先取输入样本长度 N,若小于 8 直接 Print 报错并返回 -1——样本太少时核估计方差会爆炸,外汇分钟线至少攒够 30 根以上再跑才有点参考价值。它把数据拷进 X 并排序,Mean 归零留给后续均值计算。贵金属与外汇杠杆品种波动聚集明显,KDE 只是刻画历史分布形态,对跳空和极端尾风险解释力有限,实操须自担高风险。

MQL5 / C++
  class="type">void                NTpoints(class="type">int n);
  class="type">void    PluginMode(class="type">int m) {if(m==class="num">1)Pflag=class="num">1; else Pflag=class="num">0;}
class="kw">private:
  class="type">void                kdens(class="type">class="kw">double h);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Constructor                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CKDensity::CKDensity(class="type">void)
  {
  NTpoints(class="num">200);                                          class=class="str">"cmt">// Default number of test points
  Pflag=class="num">0;                                                class=class="str">"cmt">// Not SJ plug-in
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Setting number of test points                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CKDensity::NTpoints(class="type">int n)
  {
  if(n<class="num">10)n=class="num">10;
  Np=n;                                                   class=class="str">"cmt">// Number of test points
  ArrayResize(T,Np);                                      class=class="str">"cmt">// Array for test points
  ArrayResize(Y,Np);                                      class=class="str">"cmt">// Array for result(pdf)
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Bandwidth selection and kernel density estimation                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CKDensity::Density(class="type">class="kw">double &x[],class="type">class="kw">double hh=-class="num">1)
  {
  class="type">int i;
  class="type">class="kw">double a,b,h;
  N=ArraySize(x);                                         class=class="str">"cmt">// Input data length
  if(N<class="num">8)                                                 class=class="str">"cmt">// If N is too small
    {
      Print(__FUNCTION__+": Error! Not enough data length!");
      class="kw">return(-class="num">1);
    }
  ArrayResize(X,N);                                       class=class="str">"cmt">// Array for class="kw">input data
  ArrayCopy(X,x);                                         class=class="str">"cmt">// Copy class="kw">input data
  ArraySort(X);                                           class=class="str">"cmt">// Sort class="kw">input data
  Mean=class="num">0;
把密度诊断交给小布盯盘
这些非参数密度估计的运算逻辑,小布盯盘的 AIGC 已内置到品种页,打开即可看到实时核密度曲线,你只需判断形态是否异常。

常见问题

桶宽和边界敏感导致空桶或合并噪声,核方法用平滑核能给出连续估计,更适合极短样本。
SJPI 按数据自身特征自适应选带宽,对偏态和重尾分布比人为设定更稳,估计偏差倾向更小。
可以,对应品种页已集成 AIGC 密度诊断,无需自己写 MQL5 类即可看矢量示意图。
未校正时边界密度可能高估或截尾,反射法类处理能把近端偏差压到可接受区间,具体看样本量。
范围过宽引入噪声、过窄丢细节,文中给出的选择器在中等序列上平衡较好,实盘外汇贵金属仍属高风险。