统计估计·综合运用
📊

统计估计·综合运用

(3/3)·从分布中心五估计到 Gnuplot 可视化,一篇跑通 MQL5 统计估计全流程

案例拆解 第 3/3 篇
很多交易者直接拿均值和标准差套价格序列,却忽略异常值对少量样本的撕裂效应。一旦采样里混进罕见跳空,估计出的分布中心可能完全失真。先识别再估计,是这套工具存在的理由。

「用四图一眼看穿序列脾气」

离差、标准方差、偏度、峰度这四个统计量能把一个随机序列说清楚,但干看数字不如直接画图。把直方图、正态概率图、原始序列线和滞后图摆在同一张纸上,分布形态和相关性基本一眼可辨。 滞后图用 Y 轴放当前值、X 轴放前一时刻值:若存在强相关,点会拉成直线;若是像 MathRand() 生成的纯随机序列,点就散满全图。这一招在统计界叫 4-Plot,不是新发明,但放在 MT5 里自己跑一遍最实在。 文末的 s4plot.mq5 就是干这事:OnStart() 里先塞满 128 个随机数,依次算统计量与绘图数据,再丢给 vis4plot() 调 Gnuplot 弹窗。下面这段是核心启动逻辑,注意 vis4plot() 最后一个参数是小数位,本例写 6 即保留六位。 若你的序列数量级太小或太大,把该参数设成 -5 可切科学计数法,这恰是函数默认值。外汇与贵金属价格序列波动剧烈、杠杆风险高,用四图自检分布假设前,先确认采样窗口是否够长,否则小样本偏度峰度会严重误导。

MQL5 / C++
<span class="comment">class=class="str">"cmt">//----------------------------------------------------------------------------</span>
<span class="comment">class=class="str">"cmt">// Script program start function</span>
<span class="comment">class=class="str">"cmt">//----------------------------------------------------------------------------</span>
<span class="keyword">class="type">void</span> <span class="functions">OnStart</span>()
&nbsp;&nbsp;{
&nbsp;&nbsp;<span class="keyword">class="type">int</span> i;
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> dat[<span class="number">class="num">128</span>],histo[],rankit[],xrankit[];
&nbsp;&nbsp;statParam sp;

&nbsp;&nbsp;<span class="functions">MathSrand</span>(<span class="number">class="num">1</span>);
&nbsp;&nbsp;<span class="keyword">for</span>(i=<span class="number">class="num">0</span>;i&lt;<span class="functions">ArraySize</span>(dat);i++) dat[i]=<span class="functions">MathRand</span>();
&nbsp;&nbsp;
&nbsp;&nbsp;<span class="keyword">if</span>(dStat(dat,sp)==-<span class="number">class="num">1</span>)<span class="keyword">class="kw">return</span>;
&nbsp;&nbsp;<span class="keyword">if</span>(dHist(dat,histo,sp)==-<span class="number">class="num">1</span>)<span class="keyword">class="kw">return</span>;
&nbsp;&nbsp;<span class="keyword">if</span>(dRankit(dat,rankit,xrankit,sp)==-<span class="number">class="num">1</span>)<span class="keyword">class="kw">return</span>;
&nbsp;&nbsp;
&nbsp;&nbsp;vis4plot(dat,histo,rankit,xrankit,sp,<span class="number">class="num">6</span>);
&nbsp;&nbsp;}

◍ 用 Xorshift 造可控伪随机流

RNDXor128 这个类把 George Marsaglia 的 xorshift 算法封装进 MT5,初始周期长达 2 的 128 次方,足以支撑多数蒙特卡洛类的行情模拟。实例化时四个种子被写死成 x=123456789、y=362436069、z=521288629、w=88675123,所以每次新建对象得到的序列完全一样,不像 MathRand() 那样每次脚本重启都换一套。 它内置了几种分布形态:Rand() 吐出 [0, 4294967295] 的均匀整数,Rand_01() 压到 [0,1],Rand_Norm() 给零均值单位方差的正态,Rand_Exp() 和 Rand_Laplace() 分别对应参数 1.0 的指数与拉普拉斯。做外汇或贵金属回测时,这种可复现的随机流比系统函数更适合对照不同参数下的样本路径,但请记住模拟结果只反映历史假设,实盘杠杆品种高风险,分布再漂亮也不代表未来概率。 Reset() 能把种子拨回初始态,SRand() 则可手动换基值。下面这段头文件核心逻辑,建议直接丢进 MT5 的 include 目录验证编译。

MQL5 / C++
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class=class="str">"cmt">//                                                                         RNDXor128.mqh
class=class="str">"cmt">//                                                                         class="num">2011, victorg
class=class="str">"cmt">//                                                                         [MQL5官方文档]
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class="macro">#class="kw">property copyright "class="num">2011, victorg"
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#include <Object.mqh>
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class=class="str">"cmt">// Generation of pseudo-random sequences. The Xorshift RNG algorithm 
class=class="str">"cmt">// (George Marsaglia) with the class="num">2**class="num">128 period of initial sequence is used.
class=class="str">"cmt">//      class="type">uint rand_xor128()
class=class="str">"cmt">//        {
class=class="str">"cmt">//        class="kw">static class="type">uint x=class="num">123456789,y=class="num">362436069,z=class="num">521288629,w=class="num">88675123;
class=class="str">"cmt">//        class="type">uint t=(x^(x<<class="num">11));x=y;y=z;z=w;
class=class="str">"cmt">//        class="kw">return(w=(w^(w>>class="num">19))^(t^(t>>class="num">8)));
class=class="str">"cmt">//        }
class=class="str">"cmt">// Methods:
class=class="str">"cmt">//  Rand()       - even distribution withing the range [class="num">0,UINT_MAX=class="num">4294967295].
class=class="str">"cmt">//  Rand_01()    - even distribution within the range [class="num">0,class="num">1].
class=class="str">"cmt">//  Rand_Norm()  - normal distribution with zero mean and dispersion one.
class=class="str">"cmt">//  Rand_Exp()   - exponential distribution with the parameter class="num">1.0.
class=class="str">"cmt">//  Rand_Laplace() - Laplace distribution with the parameter class="num">1.0
class=class="str">"cmt">//  Reset()      - resetting of all basic values to initial state.
class=class="str">"cmt">//  SRand()      - setting new basic values of the generator.
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class="macro">#define xor32   xx=xx^(xx<<class="num">13);xx=xx^(xx>>class="num">17);xx=xx^(xx<<class="num">5)
class="macro">#define xor128 t=(x^(x<<class="num">11));x=y;y=z;z=w;w=(w^(w>>class="num">19))^(t^(t>>class="num">8))
class="macro">#define inidat x=class="num">123456789;y=class="num">362436069;z=class="num">521288629;w=class="num">88675123;xx=class="num">2463534242
class RNDXor128:class="kw">public CObject
  {
class="kw">protected:
  class="type">uint      x,y,z,w,xx,t;
  class="type">uint      UINT_half;
class="kw">public:
            RNDXor128()       {UINT_half=UINT_MAX>>class="num">1;inidat;};
  class="type">class="kw">double    Rand()            {xor128;class="kw">return((class="type">class="kw">double)w);};
  class="type">int       Rand(class="type">class="kw">double& a[],class="type">int n)

用 xor128 给数组灌入随机序列

把伪随机发生器接到 MT5 的数组上,核心就是两个重载:单值返回和批量填充。单值版 Rand_01() 直接拿 xor128 生成的 w 除以 UINT_MAX,得到 [0,1) 区间的浮点;批量版先卡两道防线——n<1 返回 -1,数组容量不够返回 -2,避免越界崩 EA。 正态版 Rand_Norm() 走的是 Box-Muller 变换:连续抽两组均匀随机,落单位圆内才用,sln 算的是 sqrt(-2*ln(s)/s)。它用 static 变量 ra 和标志位 b 缓存了配对值,第二次调用直接吐另一个正态样本,等于一次变换出两个不重复值。 实盘前建议先开脚本跑 Rand_01(a,1000) 再打印直方图,若分布明显偏左或重复率异常,多半是 xor128 宏的 w 更新逻辑没接对。外汇与贵金属杠杆高,这类随机序列只该用于回测扰动或蒙特卡洛压力测试,别拿去直接发单。

MQL5 / C++
class="type">int Rand_01(class="type">class="kw">double& a[],class="type">int n)
{class="type">int i;if(n<class="num">1)class="kw">return(-class="num">1);
 if(ArraySize(a)<n)class="kw">return(-class="num">2);
 for(i=class="num">0;i<n;i++){xor128;a[i]=(class="type">class="kw">double)w;}
 class="kw">return(class="num">0);};

class="type">class="kw">double Rand_01() {xor128;class="kw">return((class="type">class="kw">double)w/UINT_MAX);};

class="type">int Rand_01(class="type">class="kw">double& a[],class="type">int n)
{if(n<class="num">1)class="kw">return(-class="num">1);
 if(ArraySize(a)<n)class="kw">return(-class="num">2);
 for(i=class="num">0;i<n;i++){xor128;a[i]=(class="type">class="kw">double)w/UINT_MAX;}
 class="kw">return(class="num">0);};

class="type">class="kw">double Rand_Norm() {class="type">class="kw">double v1,v2,s,sln;class="kw">static class="type">class="kw">double ra;class="kw">static class="type">uint b=class="num">0;
 if(b==w){b=class="num">0;class="kw">return(ra);}
 do{
  xor128;v1=(class="type">class="kw">double)w/UINT_half-class="num">1.0;
  xor128;v2=(class="type">class="kw">double)w/UINT_half-class="num">1.0;
  s=v1*v1+v2*v2;
  }
 while(s>=class="num">1.0||s==class="num">0.0);
 sln=MathLog(s);sln=MathSqrt((-sln-sln)/s);
 ra=v2*sln;b=w;
 class="kw">return(v1*sln);};

class="type">int Rand_Norm(class="type">class="kw">double& a[],class="type">int n)

「指数与拉普拉斯分布的采样实现」

在价格行为模拟里,单纯正态往往不够——跳空和极端波动更贴近指数或拉普拉斯形态。下面这段 MT5 代码给出了三种分布的批量填充函数,可直接贴进 EA 的自定义库里跑。 Rand_Exp 用 -log(w/UINT_MAX) 把均匀随机量变成指数分布;当底层 xor128 返回 w==0 时直接给 DBL_MAX,避免对数域出现负无穷。Rand_Laplace 则靠 w 与 UINT_half 的大小分流,左右两支分别走 -log(a) 与 log(a),双尾对称。 两个带数组入参的版本(Rand_Exp(double &a[],int n) 等)都先做防御:n<1 回 -1,数组容量不足回 -2,再循环填 n 个样本。你在 MT5 里建个 1000 长度 double 数组,调 Rand_Laplace(a,1000),大概率能看到比正态更尖的峰和更肥的尾——这对回测滑点模型有参考价值。外汇与贵金属杠杆高,这类合成样本只用于方法验证,实盘须自担风险。

MQL5 / C++
class="type">int Rand_Norm(class="type">class="kw">double &a[],class="type">int n)
  {class="type">int i;if(n<class="num">1)class="kw">return(-class="num">1);
   if(ArraySize(a)<n)class="kw">return(-class="num">2);
   for(i=class="num">0;i<n;i++)a[i]=Rand_Norm();
   class="kw">return(class="num">0);};
 class="type">class="kw">double  Rand_Exp()       {xor128;if(w==class="num">0)class="kw">return(DBL_MAX);
   class="kw">return(-MathLog((class="type">class="kw">double)w/UINT_MAX));};
class="type">int     Rand_Exp(class="type">class="kw">double & a[],class="type">int n)
  {class="type">int i;if(n<class="num">1)class="kw">return(-class="num">1);
   if(ArraySize(a)<n)class="kw">return(-class="num">2);
   for(i=class="num">0;i<n;i++)a[i]=Rand_Exp();
   class="kw">return(class="num">0);};
 class="type">class="kw">double  Rand_Laplace()   {class="type">class="kw">double a;xor128;
  a=(class="type">class="kw">double)w/UINT_half;
  if(w>UINT_half)
    {a=class="num">2.0-a;
     if(a==class="num">0.0)class="kw">return(-DBL_MAX);
     class="kw">return(MathLog(a));}
  else
    {if(a==class="num">0.0)class="kw">return(DBL_MAX);
     class="kw">return(-MathLog(a));}};
class="type">int     Rand_Laplace(class="type">class="kw">double &a[],class="type">int n)
  {class="type">int i;if(n<class="num">1)class="kw">return(-class="num">1);

◍ 随机数发生器里的种子与状态复位

这段代码片段把 PRNG 的初始化和重置逻辑收口了。先判断数组容量是否够装 n 个样本,不够直接返回 -2,避免越界;够的话用 Laplace 分布填充数组,返回 0 表示成功。 Reset() 只是简单地触发 inidat 宏,把发生器内部状态清回初始值,相当于一次冷启动。 SRand(uint seed) 重载版接受单个种子:非零时写入 xx,随后连续跑 16 轮 xor32 打乱,再把 xx 分别赋给 x/y/z/w 各跑两轮 xor32,最后再来 16 轮 xor128 混合。这种多轮搅拌就是为了让单一种子也能扩散到全部 128 位状态。 四参数版 SRand(xs,ys,zs,ws) 直接接管四个 32 位状态字;四个全为 0 时返回 -1 拒绝,否则赋值后做 16 轮 xor128 即就绪。在 MT5 里你可以复制这套结构,用不同种子跑 Monte Carlo 模拟,观察欧元兑美元报价的尾部风险分布——外汇与贵金属杠杆高,模拟结果仅代表概率倾向,实盘仍可能超预期波动。

MQL5 / C++
if(ArraySize(a)<n)class="kw">return(-class="num">2);
for(i=class="num">0;i<n;i++)a[i]=Rand_Laplace();
class="kw">return(class="num">0);};
class="type">void      Reset()        {inidat;};
class="type">void      SRand(class="type">uint seed)  {class="type">int i;if(seed!=class="num">0)xx=seed;
for(i=class="num">0;i<class="num">16;i++){xor32;}
 xor32;x=xx;xor32;y=xx;
 xor32;z=xx;xor32;w=xx;
 for(i=class="num">0;i<class="num">16;i++){xor128;}};
class="type">int       SRand(class="type">uint xs,class="type">uint ys,class="type">uint zs,class="type">uint ws)
       {class="type">int i;if(xs==class="num">0&&ys==class="num">0&&zs==class="num">0&&ws==class="num">0)class="kw">return(-class="num">1);
 x=xs;y=ys;z=zs;w=ws;
 for(i=class="num">0;i<class="num">16;i++){xor128;}
 class="kw">return(class="num">0);};
};

六类序列的生成与对比验证

把不同统计属性的序列摆到一起,才能看清价格序列到底像不像随机噪声。下面这套例子覆盖均匀、正态、指数、拉普拉斯、正弦,以及带相邻相关的累积序列,每个都用 512 点长度跑出来看分布形态。 均匀序列在 [0,1) 上等概率撒点,正态序列围绕均值集中,指数序列右偏且拖尾,拉普拉斯则比正态更尖峰厚尾。正弦序列是确定性周期,累积拉普拉斯则制造出前后样本强相关——这和真实行情里常见的惯性段有点神似。 代码里统一用 RNDXor128 这个伪随机源,换不同 Rand_ 方法就得到不同分布;正弦那段直接调 MathSin,不依赖随机种子。把数组长度从 512 改成 4096,MT5 里跑一遍,直方图的形状差异会更刺眼。 外汇和贵金属价格序列往往介于随机与强相关之间,用这类基准序列做参照,能帮你判断某段行情是噪声主导还是结构主导,但市场高风险,结论只作概率参考。

MQL5 / C++
class="macro">#include "RNDXor128.mqh"
RNDXor128 Rnd;
class=class="str">"cmt">//----------------------------------------------------------------------------
class="type">void OnStart()
  {
  class="type">int i;
  class="type">class="kw">double dat[class="num">512];
  for(i=class="num">0;i<ArraySize(dat);i++) dat[i]=Rnd.Rand_01();
  ...  
  }
class="macro">#include "RNDXor128.mqh"
RNDXor128 Rnd;
class=class="str">"cmt">//----------------------------------------------------------------------------
class="type">void OnStart()
  {
  class="type">int i;
  class="type">class="kw">double dat[class="num">512];
  for(i=class="num">0;i<ArraySize(dat);i++) dat[i]=Rnd.Rand_Norm();
  ...  
  }
class="macro">#include "RNDXor128.mqh"
RNDXor128 Rnd;
class=class="str">"cmt">//----------------------------------------------------------------------------
class="type">void OnStart()
  {
  class="type">int i;
  class="type">class="kw">double dat[class="num">512];
  for(i=class="num">0;i<ArraySize(dat);i++) dat[i]=Rnd.Rand_Exp();
  ...  
  }
class="macro">#include "RNDXor128.mqh"
RNDXor128 Rnd;
class=class="str">"cmt">//----------------------------------------------------------------------------
class="type">void OnStart()
  {
  class="type">int i;
  class="type">class="kw">double dat[class="num">512];
  for(i=class="num">0;i<ArraySize(dat);i++) dat[i]=Rnd.Rand_Laplace();
  ...  
  }
class=class="str">"cmt">//----------------------------------------------------------------------------
class="type">void OnStart()
  {
  class="type">int i;
  class="type">class="kw">double dat[class="num">512];
  for(i=class="num">0;i<ArraySize(dat);i++) dat[i]=MathSin(class="num">2*M_PI/class="num">4.37*i);
  ...  
  }
class="macro">#include "RNDXor128.mqh"
RNDXor128 Rnd;
class=class="str">"cmt">//----------------------------------------------------------------------------
class="type">void OnStart()
  {
  class="type">int i;
  class="type">class="kw">double dat[class="num">512],a;
  for(i=class="num">0;i<ArraySize(dat);i++) {a+=Rnd.Rand_Laplace();dat[i]=a;}
  ...  
  }

「直接实现算法前的稳定性检查」

把数学公式原样搬进 MQL5 函数,开发速度最快,但隐藏的精度坑也最多。变量在四舍五入、截断和溢出时的微小偏差,会在循环计算里被放大,这一点在外汇与贵金属的高杠杆环境下尤其危险,可能让信号偏移几个点就触发错单。 本文示例里的算法都是“直译”数学定义,没有做过误差传播分析。若你要把这些函数塞进实盘 EA,先跑一遍边界值测试:极小价格变动、超大成交量累加、跨品种精度不一致,都要单独验证。 Gnuplot 本身没在本文展开,但它确实能被改造成 MT5 的配套绘图后端——改源码重新编译后,不走文件传命令,而是用编程接口直接交互,延迟更低也更少出错。

◍ 随文开源的脚本与类清单

这套正态分布检验的文章把可复用的 MQL5 代码全打成了附件,想自己跑一遍只需把下面几个文件拖进 MT5 的 MQL5 目录。 erremove.mq5(4.53 KB)负责从采样里剔除异常错误点;function_dstat.mq5(1.35 KB)封装了统计参数计算;function_dhist.mq5(1.21 KB)算直方图数值;function_drankit.mq5(2.46 KB)专门输出画正态刻度图要用的分位值。 s4plot.mq5(10.52 KB)是把四张诊断图绘在同一画布上的示例脚本,接上前面几个函数就能直接看采样偏离度。RNDXor128.mqh(12.01 KB)是 Xorshift128 随机数类,xorshift.zip(38.35 KB)附带了 Marsaglia 的原版论文实现,做蒙特卡洛对照时有用。 外汇与贵金属行情序列非正态特征明显,用这些脚本先做分布诊断再谈策略,属于高杠杆环境下的必要前置;参数别照抄,建议拿自己品种的 M1 数据先验一遍。

记住这一条就够了

样本里混进异常值会直接拉低参数估计精度,小样本下尤其狠——比如 5 个点的序列 4,7,13,16 算出的无偏峰度就能掉到 1 以下,这跟多数人以为峰度不可能小于 1 的直觉相反。 真要处理,最干净的做法是把离群点直接从样本剔掉,但时间序列里这么干要慎之又慎:砍掉原始数据等于篡改过程本身。Denis 和 Victor 在 2011 年那轮讨论里也确认,无偏估计下峰度可小于 1,相关算法已做修正。 外汇和贵金属这类高波动品种,以「低概率」为由手动排除大跳空,往往是实盘悲剧的起点。宁可怀疑数据源、换数据源,也别动手改原始 tick。 下面这行是原文里峰度相关的经验公式,开 MT5 把 n 和 kurt 填进去就能复算样本离散度:

MQL5 / C++
gs=(class="num">1.55+class="num">0.8*MathLog10((class="type">class="kw">double)n/class="num">10.0)*MathSqrt(kurt-class="num">1))*MathSqrt(sum2/(n-class="num">1));
让小布替你跑这套
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到分布中心与异常标记,把重复劳动交给小布,你专注决策。

常见问题

标准差对离群点敏感,而基于 50% 四分位间距的估计受极端值扰动更小,适合小样本时间序列的初步清洗。
文中将计算出的参数写成纯文本数据文件,再用 Gnuplot 脚本读取渲染,避免终端图表功能受限带来的可视短板。
它用来制造已知分布的对照样本,验证估计算法在可控条件下的偏差,再拿去套真实行情才心里有数。
可以,小布在对应品种页内置了分布中心与异常检测,省去自己写 MQL5 脚本跑全流程的步骤,但外汇贵金属波动高风险,结果仅作概率参考。
N 过小时异常值权重被放大,经验公式给出的方差与超量系数不稳定,建议结合多周期采样交叉看。