未知概率密度函数的核密度估计(基础篇)
📘

未知概率密度函数的核密度估计(基础篇)

第 1/3 篇

用核密度估计绕开未知分布假设

在 MT5 里做价格行为统计时,样本的真实概率密度往往是未知的,直接套正态假设容易误判尾部风险。核密度估计(KDE)不预设分布形态,用样本点自身去平滑出密度曲线,对外汇和贵金属这种厚尾品种更贴近实际。 下面这段 MQL5 用高斯核做一维 KDE,带宽 h 手动给定。核函数取 exp(-0.5*(u/h)^2),遍历样本逐个累加,再除以样本数和带宽得到密度值。 // 高斯核密度估计函数 // array[] 为样本数据, n 为样本数, x 为待估点, h 为带宽 double KDE(double &array[], int n, double x, double h) { double sum=0.0; for(int i=0;i<n;i++) sum += MathExp(-0.5*MathPow((array[i]-x)/h,2)); // 高斯核 return sum/(n*h*MathSqrt(2*M_PI)); // 归一化 } 实盘前建议在 EURUSD 的 M15 收盘序列上跑一遍,h 从 0.0005 起调;若曲线在 3 倍 ATR 外仍非零,说明极端跳空的概率比正态模型暗示的高,贵金属 XAUUSD 同样适用但需放大 h 以容纳更大波动。杠杆品种高风险,密度只是概率参考,不是方向指令。

MQL5 / C++
class=class="str">"cmt">// 高斯核密度估计函数
class=class="str">"cmt">// array[] 为样本数据, n 为样本数, x 为待估点, h 为带宽
class="type">class="kw">double KDE(class="type">class="kw">double &array[], class="type">int n, class="type">class="kw">double x, class="type">class="kw">double h)
{
   class="type">class="kw">double sum=class="num">0.0;
   for(class="type">int i=class="num">0;i<n;i++)
      sum += MathExp(-class="num">0.5*MathPow((array[i]-x)/h,class="num">2)); class=class="str">"cmt">// 高斯核
   class="kw">return sum/(n*h*MathSqrt(class="num">2*M_PI)); class=class="str">"cmt">// 归一化
}

◍ 为什么要在 MT5 里自己估概率密度

MQL5 跑得越来越快,加上机器算力水涨船高,MT5 用户已经能把经济学、计量经济学和统计学里那些偏复杂的数学方法直接搬进行情分析。但不管用哪一派方法,基本都绕不开一个东西:概率密度函数。 很多常见分析模型其实是建立在“数据服从正态分布”或者“误以为正态”的前提上的。要评估这些模型靠不靠谱,你就得先搞清楚模型里各部分到底服从什么分布,这就引出一个刚需——造一个尽量通用的工具,去估那些未知的概率密度函数。 本文的思路是写一个 MQL5 类,把估未知密度这件事的通用算法尽量兜住。最早想的是全程不依赖外部方法,纯用 MQL5 搞定;后来发现估密度其实能拆成两半:一半是算,一半是画。算的部分用 MQL5 实现没问题,画的部分最终走的是生成 HTML 页面、丢进浏览器看矢量图。 好在计算和显示是分开实现的,你完全可以拿别的手段去可视化,比如等 MT5 官方图形库成熟了直接换掉显示层。不过得泼盆冷水:真·通用密度估计算法其实不存在。钟形分布里正态分布和指数分布的最优准则就完全不一样,事先知道点分布特征总能选到更合适的方案。 我们做市场数据,经常碰非平稳序列,所以短序列和中等长度序列的密度估计算是核心场景。直方图和 P 样条处理百万级长序列很轻松,但当你只有 10 到 20 个值时,直方图基本废了。本方案把火力集中在约 10 到 10 000 个值的序列上,外汇和贵金属这类高波动品种的小样本段尤其值得你开 MT5 跑一下验证。

「为什么核密度成了短序列的折中选择」

密度估计方法在网上随手能搜到,关键词如“概率密度估计”“密度估计”都能拉出一串,但真落到 MT5 实盘样本上,没有哪种被公认最优,各自都有代价。 直方图是传统路子,平滑直方图对长序列能给出高质量估计;可我们面对的是短序列——若硬拆成几十组,每组只剩两三个柱,根本看不出分布规律,所以这条路直接放弃。 核密度估计名气大,平滑思路在文献里也讲得透,虽然有偏差和边界问题,我们还是选了它做实现。另一类“期望最大化”算法能把序列拆成若干正态子块再求和,理论上更漂亮,但本文不碰,实践里也没法把海量方法逐个回测。 外汇与贵金属波动高风险,短样本密度若估偏,后续信号过滤会放大误判概率,选核密度只是工程妥协而非理论胜出。

用核平滑思路画价格分布

核密度估计和 MT5 里大家熟悉的移动平均线其实同源:都是滑窗加权,只是 MA 的窗可能非对称,而核平滑的窗是对称的。把它套到概率密度上,算法稍有变化——不再平滑序列本身,而是用对称核去估计任意点 X 周围样本出现的密集程度。 实际算的时候,先求输入序列的均值和标准差,把每个值减去均值再除以标准差,归一化到均值 0、标准差 1。这步不是算密度必须,但能让不同品种的价格分布都落在同一刻度上,方便横向比对。 归一化后找序列最大最小值,建两个数组:一个存等距检验点,一个存估计结果。默认检验点 200 个,也可用 NTpoints() 改成 300。检验点不一定要和原序列值重合,密度是对 X 与样本差值的核值求和得到的。 下面这段 CDens 类头文件定义了核心字段:X[] 装原始数据,T[] 装检验点,Y[] 装估计出的密度,Np 控制检验点数量(默认 200,要求 ≥10),Mean、Var、StDev 记录统计量。在外汇或贵金属上跑这套,分布形态可能暴露出非农前后的价格聚集区,但杠杆品种波动剧烈,核宽 h 选错会严重误导,属高风险验证。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 CDens.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=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Class Kernel Density Estimation                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CDens: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

◍ 核密度估计类的构造与密度求解入口

下面这段 MQL5 类实现把核密度估计(KDE)的骨架搭出来了:公开方法负责设测试点数量、跑密度,私有方法 kdens 留作带宽 h 下的具体核计算。 构造函数 CDens 里直接把测试点默认设成 200 个,意味着不手动改的话,输出的概率密度曲线会在这 200 个采样点上求值。 NTpoints 做了下限保护:传入 n 小于 10 时强制拉到 10,随后用 ArrayResize 给测试点数组 T 和结果数组 Y 分配空间,Y 存的就是 pdf 值。 Density 是主入口,先取输入数组长度 N,若 N 小于 8 直接 Print 报错并返回 -1——样本太少时做 KDE 没有意义。它内部排序后跑了两遍循环,一遍算均值(用递推式 Mean=Mean+(X[i]-Mean)/(i+1.0)),一遍准备方差,为后续带宽选择打底。外汇与贵金属价格序列波动大,样本低于 8 根 K 线就估分布,结论大概率失真,属高风险误用。 把这段代码贴进 MT5 的 include 头文件,建个 CDens 实例,传一段 EURUSD 的收盘价数组进去,就能在 Y 里拿到密度估计,验证分布是否双峰。

MQL5 / C++
  class="type">class="kw">double              H;                                        class=class="str">"cmt">// Bandwidth
class="kw">public:
   class="type">void                CDens(class="type">void);
   class="type">int                 Density(class="type">class="kw">double &x[],class="type">class="kw">double hh);
   class="type">void                NTpoints(class="type">int n);
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 CDens::CDens(class="type">void)
  {
   NTpoints(class="num">200);                                                    class=class="str">"cmt">// Default number of test points
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Setting number of test points                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CDens::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">//| Density                                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CDens::Density(class="type">class="kw">double &x[],class="type">class="kw">double hh)
  {
   class="type">int i;
   class="type">class="kw">double a,b,min,max,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);
   Mean=class="num">0;
   for(i=class="num">0;i<N;i++)Mean=Mean+(X[i]-Mean)/(i+class="num">1.0); class=class="str">"cmt">// Mean(average)
   Var=class="num">0;
   for(i=class="num">0;i<N;i++)
     {
      a=X[i]-Mean;

「核密度估计的落地与脚本启动」

上面那段收尾逻辑先把样本方差算出来,若小于 1e-250 直接报错退出,避免后续除以零标准差把归一化搞崩。归一化后 X 数组均值趋近 0、标准差为 1,再按 Np 个测试点线性铺开,带宽 h 下限锁在 0.001,最后调 kdens 跑高斯核。 kdens 里密度常数分母是 sqrt(2π)·N·h,对每个测试点把全部样本做 (T[i]-X[j])/h 的标准化距离,套 exp(-0.5·b²) 累加后除以常数得到概率密度 Y[i]。这套写法在 MT5 里编译后,外汇与贵金属样本的分布形态可能比直方图更平滑,但杠杆品种波动突变时高风险仍在。 脚本入口 OnStart 给了具体可抄的数字:ndata=1000 条输入、npoint=300 个估算点,三个数组用 ArrayResize 先撑好容量。你直接把 CDens 类挂进 EA 或脚本,改 ndata 到账户历史样本量,就能在 MT5 里复现这条密度曲线。

MQL5 / C++
X[i]=a;
Var+=a*a;
 }
 Var/=N; class=class="str">"cmt">// Variance
 if(Var<class="num">1.e-250) class=class="str">"cmt">// Variance is too small
  {
   Print(__FUNCTION__+": Error! The variance is too small or zero!");
   class="kw">return(-class="num">1);
  }
 StDev=MathSqrt(Var); class=class="str">"cmt">// Standard deviation
 for(i=class="num">0;i<N;i++)X[i]=X[i]/StDev; class=class="str">"cmt">// Data normalization(mean=class="num">0,stdev=class="num">1)
 min=X[ArrayMinimum(X)];
 max=X[ArrayMaximum(X)];
 b=(max-min)/(Np-class="num">1.0);
 for(i=class="num">0;i<Np;i++)T[i]=min+b*(class="type">class="kw">double)i; class=class="str">"cmt">// Create test points
class=class="str">"cmt">//-------------------------------- Bandwidth selection
 h=hh;
 if(h<class="num">0.001)h=class="num">0.001;
 H=h;
class=class="str">"cmt">//-------------------------------- Density estimation
 kdens(h);
 class="kw">return(class="num">0);
 }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Gaussian kernel density estimation                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CDens::kdens(class="type">class="kw">double h)
  {
  class="type">int i,j;
  class="type">class="kw">double a,b,c;
  c=MathSqrt(M_PI+M_PI)*N*h;
  for(i=class="num">0;i<Np;i++)
   {
   a=class="num">0;
   for(j=class="num">0;j<N;j++)
     {
     b=(T[i]-X[j])/h;
     a+=MathExp(-b*b*class="num">0.5);
     }
   Y[i]=a/c; class=class="str">"cmt">// pdf
   }
  }
class=class="str">"cmt">//--------------------------------------------------------------------
class="macro">#include "CDens.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Script program start function                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
  class="type">int i;
  class="type">int ndata=class="num">1000; class=class="str">"cmt">// Input data length
  class="type">int npoint=class="num">300; class=class="str">"cmt">// Number of test points
  class="type">class="kw">double X[]; class=class="str">"cmt">// Array for data 
  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">// Array for result
  ArrayResize(X,ndata);
  ArrayResize(T,npoint);
  ArrayResize(Y,npoint);

常见问题

可以,用核密度估计直接由历史样本平滑出密度曲线,绕开对分布形态的先验假设,适合短序列实盘诊断。
直方图对分箱敏感、跳变生硬;核密度用平滑核函数连续估计,小样本下波动更稳、便于找真实密集区。
可以,小布能加载你的估计算法脚本,自动输出密度曲线并高亮当前价位的密度百分位,省去手动来回切图。
优先用核密度估计:不依赖全局分布假设,对两三百点的短序列也能给出可用密度轮廓,但需警惕过平滑风险。
带宽过大把多峰抹成单峰、掩盖结构;过小则噪声当信号、曲线毛刺多。建议按样本量交叉试几个值再下结论。