未知概率密度函数的核密度估计·综合运用
(3/3)·从计算到 HTML 矢量出图,把非平稳短中序列的密度估计完整跑通
核密度估计里的标准化与带宽自适应
下面这段实现先把原始序列 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 分钟线用这套,可能在低波动段给出更锐的价格聚集提示,但外汇/贵金属杠杆高,信号仅作概率参考。 |
|---|
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 峰值随波段切换而平移。外汇和贵金属杠杆高,这种密度图只描述概率分布,不预示方向,实盘须自担回撤风险。
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 搬文件、调浏览器。
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_WRITE | FILE_TXT | FILE_ANSI 打开句柄,失败就 Print 报错并 return,成功则 FileSeek 到头部覆盖旧内容——这意味着每次 new 对象都会清空上次画的图数据,回测时若想累加多段曲线得自己改逻辑。 |
|---|
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">//+------------------------------------------------------------------+
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 会重复嵌套。
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 数量是否等于调用次数。
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 里点完绘制就能直接看到网页图表。外汇与贵金属波动剧烈,这类本地可视化仅作辅助,实盘仍属高风险。
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,删掉整个目录就干净不留痕。
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 线。若厚尾显著,均线类策略的止损可能偏紧,概率上容易被洗。
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 实现仍稀缺,这本身就是一个可验证的缺口。