未知概率密度函数的核密度估计(基础篇)
用核密度估计绕开未知分布假设
在 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 以容纳更大波动。杠杆品种高风险,密度只是概率参考,不是方向指令。
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 选错会严重误导,属高风险验证。
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 里拿到密度估计,验证分布是否双峰。
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 里复现这条密度曲线。
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);