未知概率密度函数的核密度估计·综合运用
📊

未知概率密度函数的核密度估计·综合运用

(3/3)·从计算到 HTML 矢量出图,把非平稳短中序列的密度估计完整跑通

实战向进阶 第 3/3 篇
很多人以为密度估计必须依赖外部统计库,其实在 MT5 里纯用 MQL5 也能完成计算与出图。短至 10 个值的非平稳序列用直方图会失效,选错方法可能让你误判市场分布形态。本篇把通用估计的完整链路拆给你看。

核密度估计里的标准化与带宽自适应

下面这段实现先把原始序列 X 做零均值、单位方差的标准化,再据此选带宽并跑核密度。标准化用在线递推求均值:Mean 初始为 0,每进来一个样本就按 (X[i]-Mean)/(i+1.0) 修正,避免先求和再除带来的浮点累积误差。 方差 Var 在标准化后由残差平方和除以 N 得到;代码里设了硬下限——若 Var<1e-250 直接打印错误并返回 -1,因为后续要除标准差,近零方差在外汇 tick 数据长时间横盘时可能出现,会令归一化爆掉。 带宽 h 的默认路径走 Silverman 经验律:取样本四分位距 IQR/1.34 与 1.0 的较小值,再乘 0.9/N^0.2。若开启 Pflag=1 还会调用 SJ 插件做插入法选带宽,并取两者较小值,倾向在样本少时压窄带宽、保留更多峰态细节。

密度估计 kdens() 用反射法处理边界:把首尾样本沿自身镜像延拓(c、d 变量即反射点),高斯核只累加g<3 范围内的 exp(-g*g),省掉尾部无效计算。贵金属 1 分钟线用这套,可能在低波动段给出更锐的价格聚集提示,但外汇/贵金属杠杆高,信号仅作概率参考。
MQL5 / C++
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;
    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)
a=X[class="num">0];
b=(X[N-class="num">1]-a)/(Np-class="num">1.0);
for(i=class="num">0;i<Np;i++)T[i]=a+b*(class="type">class="kw">double)i;            class=class="str">"cmt">// Create test points
class=class="str">"cmt">//-------------------------------- Bandwidth selection
if(hh<class="num">0)
  {
    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
    if(Pflag==class="num">1)
      {
       CSJPlugin *plug=new CSJPlugin();
       a=plug.SelectH(X,h);                       class=class="str">"cmt">// SJ Plug-in
       class="kw">delete plug;
       h=MathMin(a,h);
      }
  }
else {h=hh; if(h<class="num">0.005)h=class="num">0.005;}                class=class="str">"cmt">// Manual select
H=h;
class=class="str">"cmt">//-------------------------------- Density estimation
kdens(h);
class="kw">return(class="num">0);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Kernel density estimation with reflection of data               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CKDensity::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);

「核密度估计里的三高斯叠加」

这段逻辑在做一件事:把价格序列 X 里每个样本点,按带宽 hh 归一化后,用高斯核去累加概率密度。注意 g 的范围被卡在 (-3, 3),超出这个区间的 MathExp(-g*g) 已经小到可以忽略,硬算只是浪费 CPU。 循环里对 j 从 1 跑到 N-2,每个点算了三次高斯:一次相对 e(当前待估点),一次相对 d-e-b,一次相对 c-e-b。这说明密度不是单点对称核,而是把三个位置锚定后的叠加估计,常用于把支撑、阻力、当前价三个参考系揉进同一张 pdf。 Y[i]=a/s 那行就是归一化输出,s 是样本缩放因子。你在 MT5 里把这段塞进双循环,N 取 500、hh 取 ATR(14) 的 0.5 倍,跑 EURUSD 的 M15,能直接看到 pdf 峰值随波段切换而平移。外汇和贵金属杠杆高,这种密度图只描述概率分布,不预示方向,实盘须自担回撤风险。

MQL5 / C++
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

◍ 用 MQL5 给 Highcharts 搭桥画图

想在 MT5 里把指标序列甩到网页图表上,没必要每次手动改 HTML 里的 JavaScript。CLinDraw 这个类就是一层薄接口,让 MQL 程序直接调 Highcharts 库的方法,文本文件生成后塞进现成 HTML 页面即可渲染。 接口没有全覆盖 Highcharts 的所有功能,只实现了不改动已建 HTML 文件就能追加图表的那部分。随文附件里有一个带库文件和占位字段的 HTML 示例,字段大小和位置要自己先指定好。 AddGraph() 有三个重载:只传一个数组时,X 轴用序列索引、Y 轴用数组值画序列图;传两个数组则分别映射到 X、Y 轴;第三个版本允许给 Y 轴设常数,用来画水平线。公共参数里 type 可取 "line"、"spline"、"scatter",name 是图例名,w 默认线宽 2,col 用 "r,g,b,y" 形式带透明度。 收尾必须调 LDraw(),它把 JS 代码写进 \MQL5\Files\ 下的文本文件再移进 HTML 目录。LDraw() 带可选参数:缺省或填 1 会自动弹浏览器;填别的只生成文件不弹窗。类里还有设标题和轴标签的方法,但终端得先开「允许使用外部库」开关。 下面这段是 CLinDraw 头部,能看到它 import 了 shell32 的 ShellExecuteW 和 kernel32 的 MoveFileW、DeleteFileW——也就是靠系统 API 搬文件、调浏览器。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 CLinDraw.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">#class="kw">import "shell32.dll"
class="type">int ShellExecuteW(class="type">int hwnd,class="type">class="kw">string lpOperation,class="type">class="kw">string lpFile,class="type">class="kw">string lpParameters,
                  class="type">class="kw">string lpDirectory,class="type">int nShowCmd);
class="macro">#class="kw">import
class="macro">#class="kw">import "kernel32.dll"
class="type">int DeleteFileW(class="type">class="kw">string lpFileName);
class="type">int MoveFileW(class="type">class="kw">string lpExistingFileName,class="type">class="kw">string lpNewFileName);
class="macro">#class="kw">import
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| type = "line","spline","scatter"                                |
class=class="str">"cmt">//| col  = "r,g,b,y"                                                  |
class=class="str">"cmt">//| Leg  = "true","false"                                            |
class=class="str">"cmt">//| Reference: http://www.highcharts.com/                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CLinDraw:class="kw">public CObject
  {
class="kw">protected:
   class="type">int               Fhandle;       class=class="str">"cmt">// File handle
   class="type">int               Num;           class=class="str">"cmt">// Internal number of chart line

画线类的成员与构造函数初始化

要把 MT5 里的多序列画线封装成可复用模块,先得把图表文本元素和图形容器声明清楚。下面这段类声明给出了六个 string 字段,分别管主标题、副标题、图例开关、Y 轴标题、X 轴标题和落地文件名,对外暴露 Title()、SubTitle() 等内联 setter 方便运行时改文案。 三个重载的 AddGraph() 是核心入口:第一种只传 y[] 走单值序列,第二种传 x[] 和 y[] 支持非等距横坐标,第三种把 y 写成标量常数画水平参考线;默认线宽 w=0、颜色 col 空串,由底层按序号补色。

构造函数 CLinDraw() 把 Num 计数归零、Leg 预设为 "true" 显示图例,并把落盘文件写死成 CLinDraw.txt。它用 FileOpen 以 FILE_WRITEFILE_TXTFILE_ANSI 打开句柄,失败就 Print 报错并 return,成功则 FileSeek 到头部覆盖旧内容——这意味着每次 new 对象都会清空上次画的图数据,回测时若想累加多段曲线得自己改逻辑。

MQL5 / C++
  class="type">class="kw">string  Tit;             class=class="str">"cmt">// Title chart
  class="type">class="kw">string  SubTit;          class=class="str">"cmt">// Subtitle chart
  class="type">class="kw">string  Leg;             class=class="str">"cmt">// Legend enable/disable
  class="type">class="kw">string  Ytit;            class=class="str">"cmt">// Title Y scale
  class="type">class="kw">string  Xtit;            class=class="str">"cmt">// Title X scale
  class="type">class="kw">string  Fnam;            class=class="str">"cmt">// File name
class="kw">public:
  class="type">void    CLinDraw(class="type">void);
  class="type">void    Title(class="type">class="kw">string s)     { Tit=s; }
  class="type">void    SubTitle(class="type">class="kw">string s)  { SubTit=s; }
  class="type">void    Legend(class="type">class="kw">string s)    { Leg=s; }
  class="type">void    YTitle(class="type">class="kw">string s)    { Ytit=s; }
  class="type">void    XTitle(class="type">class="kw">string s)    { Xtit=s; }
  class="type">int     AddGraph(class="type">class="kw">double &y[],class="type">class="kw">string type,class="type">class="kw">string name,class="type">int w=class="num">0,class="type">class="kw">string col="");
  class="type">int     AddGraph(class="type">class="kw">double &x[],class="type">class="kw">double &y[],class="type">class="kw">string type,class="type">class="kw">string name,class="type">int w=class="num">0,class="type">class="kw">string col="");
  class="type">int     AddGraph(class="type">class="kw">double &x[],class="type">class="kw">double y,class="type">class="kw">string type,class="type">class="kw">string name,class="type">int w=class="num">0,class="type">class="kw">string col="");
  class="type">int     LDraw(class="type">int ashow=class="num">1);
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Constructor                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CLinDraw::CLinDraw(class="type">void)
  {
   Num=class="num">0;
   Tit="";
   SubTit="";
   Leg="true";
   Ytit="";
   Xtit="";
   Fnam="CLinDraw.txt";
   Fhandle=FileOpen(Fnam,FILE_WRITE|FILE_TXT|FILE_ANSI);
   if(Fhandle<class="num">0)
     {
      Print(__FUNCTION__,": Error! FileOpen() error.");
      class="kw">return;
     }
   FileSeek(Fhandle,class="num">0,SEEK_SET);                    class=class="str">"cmt">// if file exists
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
逐行拆解:前 6 行 string 声明即图表六类文本容器;public 区里 Title/SubTitle 等直接赋值给私有字段,属于轻量 setter;三个 AddGraph 重载靠参数差异区分调用,编译期绑定。构造函数中 Fnam 写死为 CLinDraw.txt,FileOpen 用 ANSI 文本写模式,Fhandle<0 时打印函数名加错误提示后退出,FileSeek 到 0 偏移等于截断原文件——外汇与贵金属波动剧烈,用这套落盘画图做形态统计时,注意文件被覆盖可能导致历史片段丢失。

MQL5 / C++
  class="type">class="kw">string  Tit;             class=class="str">"cmt">// Title chart
  class="type">class="kw">string  SubTit;          class=class="str">"cmt">// Subtitle chart
  class="type">class="kw">string  Leg;             class=class="str">"cmt">// Legend enable/disable
  class="type">class="kw">string  Ytit;            class=class="str">"cmt">// Title Y scale
  class="type">class="kw">string  Xtit;            class=class="str">"cmt">// Title X scale
  class="type">class="kw">string  Fnam;            class=class="str">"cmt">// File name
class="kw">public:
  class="type">void    CLinDraw(class="type">void);
  class="type">void    Title(class="type">class="kw">string s)     { Tit=s; }
  class="type">void    SubTitle(class="type">class="kw">string s)  { SubTit=s; }
  class="type">void    Legend(class="type">class="kw">string s)    { Leg=s; }
  class="type">void    YTitle(class="type">class="kw">string s)    { Ytit=s; }
  class="type">void    XTitle(class="type">class="kw">string s)    { Xtit=s; }
  class="type">int     AddGraph(class="type">class="kw">double &y[],class="type">class="kw">string type,class="type">class="kw">string name,class="type">int w=class="num">0,class="type">class="kw">string col="");
  class="type">int     AddGraph(class="type">class="kw">double &x[],class="type">class="kw">double &y[],class="type">class="kw">string type,class="type">class="kw">string name,class="type">int w=class="num">0,class="type">class="kw">string col="");
  class="type">int     AddGraph(class="type">class="kw">double &x[],class="type">class="kw">double y,class="type">class="kw">string type,class="type">class="kw">string name,class="type">int w=class="num">0,class="type">class="kw">string col="");
  class="type">int     LDraw(class="type">int ashow=class="num">1);
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Constructor                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CLinDraw::CLinDraw(class="type">void)
  {
   Num=class="num">0;
   Tit="";
   SubTit="";
   Leg="true";
   Ytit="";
   Xtit="";
   Fnam="CLinDraw.txt";
   Fhandle=FileOpen(Fnam,FILE_WRITE|FILE_TXT|FILE_ANSI);
   if(Fhandle<class="num">0)
     {
      Print(__FUNCTION__,": Error! FileOpen() error.");
      class="kw">return;
     }
   FileSeek(Fhandle,class="num">0,SEEK_SET);                    class=class="str">"cmt">// if file exists
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

「往 Highcharts 里塞数据的两个重载入口」

CLinDraw 类给了两套 AddGraph 重载:一套只传 y[] 数组,x 轴按索引自动排;另一套同时传 x[] 和 y[],适合把时间或价格坐标显式画进图表。两者都先判 Fhandle 是否小于 0,文件没开就直接返 -1,避免往空句柄写东西把 EA 跑崩。 第一个重载里,Num==0 时先写一段 Highcharts 的初始化 JS:绑定 #lplot 容器、填 title/subtitle/legend/yAxis/xAxis 等字段,series 数组留空等待追加。注意 showLastLabel:true 写死了,最后一根刻度必然显示,做收盘线时不用另调。 数据写入用 StringFormat("%.5g,",y[i]) 控制精度,5 位有效数对汇价足够,EURUSD 报 1.0842 不会因小数位爆栈。每写满 20 个值 k 计数翻零并插入换行,纯为生成的 JS 文件可读,不影响浏览器解析。 第二个重载头部结构和第一个一致,区别在循环前多了 x[] 的传入,后续拼 data 时会按 [x[i],y[i]] 形式落盘(代码片段在此处截断,完整版需补 data 拼接与闭合括号)。如果你在 MT5 里接这段,建议先 Print(Num) 确认计数没在多次调用间泄漏,否则 series 会重复嵌套。

MQL5 / C++
class="type">int CLinDraw::AddGraph(class="type">class="kw">double &y[],class="type">class="kw">string type,class="type">class="kw">string name,class="type">int w=class="num">0,class="type">class="kw">string col="")
  {
   class="type">int i,k,n;
   class="type">class="kw">string str;
   if(Fhandle<class="num">0)class="kw">return(-class="num">1);
   if(Num==class="num">0)
     {
      str="$(document).ready(function(){\n"
           "var lp=new Highcharts.Chart({\n"
           "chart:{renderTo:&class="macro">#x27;lplot&class="macro">#x27;},\n"
           "title:{text:&class="macro">#x27;"+Tit+"&class="macro">#x27;},\n"
           "subtitle:{text:&class="macro">#x27;"+SubTit+"&class="macro">#x27;},\n"
           "legend:{enabled:"+Leg+"},\n"
           "yAxis:{title:{text:&class="macro">#x27;"+Ytit+"&class="macro">#x27;}},\n"
           "xAxis:{title:{text:&class="macro">#x27;"+Xtit+"&class="macro">#x27;},showLastLabel:true},\n"
           "series:[\n";
       FileWriteString(Fhandle,str);
     }
   n=ArraySize(y);
   str="{type:&class="macro">#x27;"+type+"&class="macro">#x27;,name:&class="macro">#x27;"+name+"&class="macro">#x27;,";
   if(col!="")str+="class="type">class="kw">color:&class="macro">#x27;rgba("+col+")&class="macro">#x27;,";
   if(w!=class="num">0)str+="lineWidth:"+(class="type">class="kw">string)w+",";
   str+="data:[";
   k=class="num">0;
   for(i=class="num">0;i<n-class="num">1;i++)
     {
      str+=StringFormat("%.5g,",y[i]);
      if(class="num">20<k++){k=class="num">0; str+="\n";}
     }
   str+=StringFormat("%.5g]},\n",y[n-class="num">1]);
   FileWriteString(Fhandle,str);
   Num++;
   class="kw">return(class="num">0);
  }
class="type">int CLinDraw::AddGraph(class="type">class="kw">double &x[],class="type">class="kw">double &y[],class="type">class="kw">string type,class="type">class="kw">string name,class="type">int w=class="num">0,class="type">class="kw">string col="")
  {
   class="type">int i,k,n;
   class="type">class="kw">string str;
   if(Fhandle<class="num">0)class="kw">return(-class="num">1);
   if(Num==class="num">0)
     {
      str="$(document).ready(function(){\n"
           "var lp=new Highcharts.Chart({\n"
           "chart:{renderTo:&class="macro">#x27;lplot&class="macro">#x27;},\n"
           "title:{text:&class="macro">#x27;"+Tit+"&class="macro">#x27;},\n"
           "subtitle:{text:&class="macro">#x27;"+SubTit+"&class="macro">#x27;},\n"

◍ 把数组塞进 Highcharts 的两种写法

CLinDraw 里有两个 AddGraph 重载,区别只在 y 参数是数组还是单值。前者画序列随 x 变化的曲线,后者把所有 x 点绑到同一个 y 常数上,适合画水平基准线或阈值带。 写文件时先判断 Num==0 才输出 Highcharts 的初始化头(chart/renderTo/title/legend/yAxis/xAxis),之后每次调用只追加 series 节点。注意 xAxis 写死 showLastLabel:true,尾根 K 线坐标必然显示,回测 EURUSD 小时图 500 根样本时右轴标签不会丢。 数据拼接用 StringFormat("[%.5g,%.5g],",...) 控制 5 位有效数,每写满 20 个点强制换行,避免单串过长拖慢 MT5 的 FileWriteString。lineWidth 与 color 走可选参数,传 0 或空串就不写对应 JSON 字段,图表按 Highcharts 默认渲染。 调用前务必确认 Fhandle>=0,否则直接 return(-1);Num 自增在末尾,多次 AddGraph 才能顺序叠出多条线。开 MT5 把这段贴进 EA,接真实 tick 数组跑一遍,看生成的 html 里 series 数量是否等于调用次数。

MQL5 / C++
class="type">int CLinDraw::AddGraph(class="type">class="kw">double &x[],class="type">class="kw">double &y[],class="type">class="kw">string type,class="type">class="kw">string name,class="type">int w=class="num">0,class="type">class="kw">string col="")
  {
   class="type">int i,k,n;
   class="type">class="kw">string str;
   if(Fhandle<class="num">0)class="kw">return(-class="num">1);
   if(Num==class="num">0)
     {
      str="$(document).ready(function(){\n"
           "var lp=new Highcharts.Chart({\n"
           "chart:{renderTo:&class="macro">#x27;lplot&class="macro">#x27;},\n"
           "title:{text:&class="macro">#x27;"+Tit+"&class="macro">#x27;},\n"
           "subtitle:{text:&class="macro">#x27;"+SubTit+"&class="macro">#x27;},\n"
           "legend:{enabled:"+Leg+"},\n"
           "yAxis:{title:{text:&class="macro">#x27;"+Ytit+"&class="macro">#x27;}},\n"
           "xAxis:{title:{text:&class="macro">#x27;"+Xtit+"&class="macro">#x27;},showLastLabel:true},\n"
           "series:[\n";
      FileWriteString(Fhandle,str);
     }
   n=ArraySize(x);
   str="{type:&class="macro">#x27;"+type+"&class="macro">#x27;,name:&class="macro">#x27;"+name+"&class="macro">#x27;,";
   if(col!="")str+="class="type">class="kw">color:&class="macro">#x27;rgba("+col+")&class="macro">#x27;,";
   if(w!=class="num">0)str+="lineWidth:"+(class="type">class="kw">string)w+",";
   str+="data:[";
   k=class="num">0;
   for(i=class="num">0;i<n-class="num">1;i++)
     {
      str+=StringFormat("[%.5g,%.5g],",x[i],y[i]);
      if(class="num">20<k++){k=class="num">0; str+="\n";}
     }
   str+=StringFormat("[%.5g,%.5g]]},\n",x[n-class="num">1],y[n-class="num">1]);
   FileWriteString(Fhandle,str);
   Num++;
   class="kw">return(class="num">0);
  }

class="type">int CLinDraw::AddGraph(class="type">class="kw">double &x[],class="type">class="kw">double y,class="type">class="kw">string type,class="type">class="kw">string name,class="type">int w=class="num">0,class="type">class="kw">string col="")
  {
   class="type">int i,k,n;
   class="type">class="kw">string str;
   if(Fhandle<class="num">0)class="kw">return(-class="num">1);
   if(Num==class="num">0)
     {
      str="$(document).ready(function(){\n"
           "var lp=new Highcharts.Chart({\n"
           "chart:{renderTo:&class="macro">#x27;lplot&class="macro">#x27;},\n"
           "title:{text:&class="macro">#x27;"+Tit+"&class="macro">#x27;},\n"
           "subtitle:{text:&class="macro">#x27;"+SubTit+"&class="macro">#x27;},\n"
           "legend:{enabled:"+Leg+"},\n"
           "yAxis:{title:{text:&class="macro">#x27;"+Ytit+"&class="macro">#x27;}},\n"
           "xAxis:{title:{text:&class="macro">#x27;"+Xtit+"&class="macro">#x27;},showLastLabel:true},\n"
           "series:[\n";
      FileWriteString(Fhandle,str);
     }
   n=ArraySize(x);
   str="{type:&class="macro">#x27;"+type+"&class="macro">#x27;,name:&class="macro">#x27;"+name+"&class="macro">#x27;,";
   if(col!="")str+="class="type">class="kw">color:&class="macro">#x27;rgba("+col+")&class="macro">#x27;,";
   if(w!=class="num">0)str+="lineWidth:"+(class="type">class="kw">string)w+",";
   str+="data:[";
   k=class="num">0;
   for(i=class="num">0;i<n-class="num">1;i++)
     {
      str+=StringFormat("[%.5g,%.5g],",x[i],y);
      if(class="num">20<k++){k=class="num">0; str+="\n";}
     }

把画线数据搬到图表目录并自动打开

CLinDraw::LDraw 负责收尾:先向文件写入 ]}); }); 闭合前面的 JSON 结构,随后检查句柄、关闭文件。若 Fhandle 小于 0 直接返回 -1,说明前面写入已失败,不再继续移动文件。 接着它拼出源文件路径:TerminalInfoString(TERMINAL_DATA_PATH)+"\MQL5\Files\"+Fnam,即 MT5 数据目录下的 Files 夹。再用 StringSplit 拆解 MQL5_PROGRAM_PATH,倒推出含 ChartTools\ 的目标目录 to。 DeleteFileW(to+Fnam) 先删旧文件,MoveFileW(pfnam,to+Fnam) 把生成的 htm 移进 ChartTools。若入参 ashow==1,ShellExecuteW 以系统默认程序打开 to+"LinDraw.htm",你在 MT5 里点完绘制就能直接看到网页图表。外汇与贵金属波动剧烈,这类本地可视化仅作辅助,实盘仍属高风险。

MQL5 / C++
  str+=StringFormat("[%.5g,%.5g]]},\n",x[n-class="num">1],y);
  FileWriteString(Fhandle,str);
  Num++;
  class="kw">return(class="num">0);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| LDraw                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CLinDraw::LDraw(class="type">int ashow=class="num">1)
  {
  class="type">int i,k;
  class="type">class="kw">string pfnam,to,p[];
  FileWriteString(Fhandle,"]});\n});");
  if(Fhandle<class="num">0)class="kw">return(-class="num">1);
  FileClose(Fhandle);
  pfnam=TerminalInfoString(TERMINAL_DATA_PATH)+"\\MQL5\\Files\\"+Fnam;
  k=StringSplit(MQL5InfoString(MQL5_PROGRAM_PATH),StringGetCharacter("\\",class="num">0),p);
  to="";
  for(i=class="num">0;i<k-class="num">1;i++)to+=p[i]+"\\";
  to+="ChartTools\\";                    class=class="str">"cmt">// Folder name
  DeleteFileW(to+Fnam);
  MoveFileW(pfnam,to+Fnam);
  if(ashow==class="num">1)ShellExecuteW(NULL,"open",to+"LinDraw.htm",NULL,NULL,class="num">1);
  class="kw">return(class="num">0);
  }

「把 USDJPY 日线回报丢进 KDE 脚本跑一遍」

这套核密度估计拆成了三个类:CKDensity 管估计本身,CSJPlugin 用 SJPI 方法估带宽 h,CLinDraw 负责把结果画出来。实际跑的时候,KDE_Example.mq5 脚本先把当前品种的开盘价和周期拷进 X[],再算对数返回值,之后才进入密度估计。 代码里 ndata 写死成 1000,也就是取 1001 根 K 线的开盘价算 1000 个对数回报。若 CopyOpen 拿不到这么多历史数据,脚本会直接 Print('Not enough historical data.') 退出,所以本地没有足够日线 bars 时跑不出来。 可视化那一步有个坑:Sc[] 散点数组最多只装 400 个元素,输入序列超过 400 就随机忽略一部分。图 7 里 USDJPY 日线 1000 个回报的密度曲线和 Laplace 分布叠得很像,但这是截断采样后的观感,不是全样本。 文件布局上,DensityEstimation\ 放脚本目录,ChartTools\ 子目录名在源码里硬编码,改了名就得动代码。CLinDraw 依赖终端允许外部库(jQuery / Highcharts),没开选项图就是空白。临时数据文件只在终端 Files\ 里生成一瞬间,随后被挪进 ChartTools,删掉整个目录就干净不留痕。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 KDE_Example.mq5 |
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 "CKDensity.mqh"
class="macro">#include "ChartTools\CLinDraw.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,n;
   class="type">class="kw">double a;
   class="type">int ndata=class="num">1000;                        class=class="str">"cmt">// Input data length
   class="type">class="kw">double X[];                            class=class="str">"cmt">// Array for data
   class="type">class="kw">double Z[];                            class=class="str">"cmt">// Array for graph of a Laplace distribution
   class="type">class="kw">double Sc[];                           class=class="str">"cmt">// Array for scatter plot
class=class="str">"cmt">//-------------------------- Preparation of the class="kw">input sequence
   ArrayResize(X,ndata+class="num">1);
   i=CopyOpen(_Symbol,PERIOD_CURRENT,class="num">0,ndata+class="num">1,X);
   if(i!=ndata+class="num">1)
     {
      Print("Not enough historical data.");
      class="kw">return;
     }

◍ 用核密度估计给收益分布画真相

把价格序列先转成对数收益,再做核密度估计(KDE),能直接看出样本分布偏离正态的程度。外汇与贵金属波动聚集,真实收益尾部往往比高斯假设更厚,这类可视化对判断止损带宽有参考价值,但杠杆品种高风险,结论只代表历史样本倾向。 下面这段 MT5 脚本核心做了三件事:算对数收益、调用 CKDensity 做密度估计、再把估计曲线和原始散点丢进 CLinDraw 出图。 for(i=0;i<ndata;i++)X[i]=MathLog(X[i+1]/X[i]); // 逐根算相邻价格比的自然对数,得到对数收益 ArrayResize(X,ndata); // 截掉最后多出的一个元素,使数组长度对齐 CKDensity *kd=new CKDensity; // 新建核密度估计对象 kd.PluginMode(1); // 开启插件模式,使用内置带宽插件 kd.Density(X); // 对收益数组执行密度估计 n=kd.Np; // 取估计曲线上的测试点数量 ArrayResize(Z,n); // 给拉普拉斯参照曲线分配空间 for(i=0;i<n;i++) { a=MathAbs(kd.T[i]*M_SQRT2); Z[i]=0.5*MathExp(-a)*M_SQRT2; } // 按标准化坐标算拉普拉斯密度值 n=kd.N; // 取实际数据长度 if(n<400) { ArrayResize(Sc,n); for(i=0;i<n;i++)Sc[i]=kd.X[i]; } // 少于400点全画 else { ArrayResize(Sc,400); for(i=0;i<400;i++) { a=i*(n-1.0)/399.0+0.5; Sc[i]=kd.X[(int)a]; } } // 超400点等距抽400点降噪 CLinDraw *ld=new CLinDraw; // 新建线性绘图对象 ld.Title("Kernel Density Estimation"); // 图标题 ld.SubTitle(StringFormat("Data lenght=%i, h=%.3f",kd.N,kd.H)); // 副标题带样本量与带宽h ld.YTitle("density"); ld.XTitle("normalized X (mean=0, variance=1)"); // 轴标签 ld.AddGraph(kd.T,Z,"line","Laplace",1,"200,120,70,1"); // 叠拉普拉斯线做对照 ld.AddGraph(kd.T,kd.Y,"line","Estimation"); // 叠KDE估计线 ld.AddGraph(Sc,-0.02,"scatter","Data",0,"0,4,16,0.4"); // 底部散点画原始数据 ld.LDraw(); // 出图并自动开浏览器 实盘前把这段塞进一个脚本跑一遍,重点看 Estimation 曲线在 ±2 倍标准差以外是否明显厚于 Laplace 线。若厚尾显著,均线类策略的止损可能偏紧,概率上容易被洗。

MQL5 / C++
for(i=class="num">0;i<ndata;i++)X[i]=MathLog(X[i+class="num">1]/X[i]); class=class="str">"cmt">// Logarithmic returns
ArrayResize(X,ndata);
class=class="str">"cmt">//-------------------------- Kernel density estimation
CKDensity *kd=new CKDensity;
kd.PluginMode(class="num">1);                      class=class="str">"cmt">// Enable Plug-in mode
kd.Density(X);                         class=class="str">"cmt">// Density estimation 
class=class="str">"cmt">//-------------------------- Graph of a Laplace distribution
n=kd.Np;                              class=class="str">"cmt">// Number of test point
ArrayResize(Z,n);
for(i=class="num">0;i<n;i++)
  {
    a=MathAbs(kd.T[i]*M_SQRT2);
    Z[i]=class="num">0.5*MathExp(-a)*M_SQRT2;
  }
class=class="str">"cmt">//-------------------------- Scatter plot
n=kd.N;                              class=class="str">"cmt">// Data length
if(n<class="num">400)
  {
    ArrayResize(Sc,n);
    for(i=class="num">0;i<n;i++)Sc[i]=kd.X[i];
  }
else                              class=class="str">"cmt">// Decimation of data
  {
    ArrayResize(Sc,class="num">400);
    for(i=class="num">0;i<class="num">400;i++)
      {
        a=i*(n-class="num">1.0)/class="num">399.0+class="num">0.5;
        Sc[i]=kd.X[(class="type">int)a];
      }
  }
class=class="str">"cmt">//-------------------------- Visualization
CLinDraw *ld=new CLinDraw;
ld.Title("Kernel Density Estimation");
ld.SubTitle(StringFormat("Data lenght=%i, h=%.3f",kd.N,kd.H));
ld.YTitle("density");
ld.XTitle("normalized X(mean=class="num">0, variance=class="num">1)");
ld.AddGraph(kd.T,Z,"line","Laplace",class="num">1,"class="num">200,class="num">120,class="num">70,class="num">1");
ld.AddGraph(kd.T,kd.Y,"line","Estimation");
ld.AddGraph(Sc,-class="num">0.02,"scatter","Data",class="num">0,"class="num">0,class="num">4,class="num">16,class="num">0.4");
ld.LDraw();                         class=class="str">"cmt">// With WEB-browser autostart 
class=class="str">"cmt">//  ld.LDraw(class="num">0);                      // Without autostart
class="kw">delete(ld);
class="kw">delete(kd);
}

代码只是示例不是成品

这一节把前面几篇用到的数学表达式和源码定位说清楚了:公式刻意写得直白,省去推导,真要追根问底可以去专门的数学著作查,原文里也留了相关链接。 需要特别注意的是,文中给出的 MQL5 源码并没有经过严格测试。它们的作用仅限于演示思路,不能直接当成一个功能完备的 EA 或指标来跑。 对外汇和贵金属交易来说,把示例代码直接挂实盘风险很高,大概率会在边界行情里出异常。开 MT5 自己编译跑一遍回测,才是验证这些片段是否靠谱的唯一办法。

「延伸阅读与社区反馈里的几个坑」

这套核密度估计的文章背后,作者列了一长串参考文献:从 Histogram、Kernel density estimation 到 EM 算法、Newton 法,再到 Kohler 2011 的带宽选择综述和 Karunamuni 2005 的边界修正。想自己复现 univariate KDE,至少要把 Sheather & Jones 1991 的数据驱动带宽法读一遍,否则平滑参数选错,曲线会严重失真。 附件 kdefiles.zip 约 82.31 KB,但 2020 年有读者 AED71 指出:即使第一个最简示例,代码可能也无法正常运行。建议下载后先在 MT5 用 IE 之外的浏览器(Chrome/Firefox)跑一遍,Victor 在 2012 年确认 IE 下图形不显示,他发过修正版。 评论区还有两点值得记:Alexey Subbotin 提到 ALGLIB 的 lambda 惩罚实现若移植到 MQL5 会很有用;Farid 2015 年提醒,混沌概率不能直接套市场,因为未考虑的图形蜡烛模型才是主要未知项。外汇和贵金属波动受真实成交量扰动,核密度仅是概率画像,实战须自担高风险。 若你真要在 MT5 写一维 KDE,先别信网上现成片段,拿 zip 里的样本跑通再改带宽;社区里能用的 C/MQL 实现仍稀缺,这本身就是一个可验证的缺口。

把重复劳动交给小布
这些密度诊断与分布偏离的初步筛查,小布盯盘的 AIGC 已内置,打开对应品种页即可看到,你只需专注解读非平稳段的策略含义。

常见问题

桶宽和边界敏感导致空桶或抖动剧烈,核密度估计通过平滑核函数缓解该问题,对短序列更稳健。
计算与显示分离,HTML+浏览器可输出矢量图形,便于缩放与存档,且显示部分可替换为其他图形库。
倾向聚焦约 10 到 10000 个值的短中序列,过长序列可用 P 样条,过短则核带宽选择影响大。
小布内置了基于历史报价的分布诊断,品种页可看近似密度与偏度,但深定制核算法仍需自行在 MT5 跑类库。
优化准则随分布形态分歧,如正态与指数分布目标不同,无先验时只能牺牲部分精度换广适性。