统计估计·进阶篇
📊

统计估计·进阶篇

(2/3)· 从异常值筛除到正态概率图,把教科书里的统计假设变成盘前可执行的诊断步骤

实战向进阶 第 2/3 篇

接上篇铺垫的随机序列概念,这一篇直接钻进估计环节。很多交易者把指标当黑箱,却从没确认过自己的价格样本到底是不是正态、有没有被极端跳空带偏,参数一错,后面模型全歪。

「四分位均值在MT5里的归一化写法」

MQL5里算完原始IQM后,常需要按样本规模做归一,否则不同品种或不同窗口长度之间数值不可比。上面这行把IQM除以(n-2*m),本质是剔除上下各m个极端值后的有效样本量归一,让跨周期对比有意义。 在MT5里把这段直接贴进自定义指标,改n和m就能看不同分位区间的均值漂移;外汇与贵金属波动跳变多,极端值剔除比例调大一点,曲线会更抗噪,但滞后也会上来,属于高风险环境下的参数权衡。

MQL5 / C++
IQM=IQM/(n-class="num">2*m);                                                     class=class="str">"cmt">// Interquartile mean(IQM)

用 Gnuplot 把 MT5 脚本结果画出来

想在 MT5 里把脚本算出的分布直接可视化,最省事的办法是借免费的 Gnuplot 画图。它能在独立窗口实时出图,也能按格式写文件,跨平台且带源码和编译版。本文示例只在 Windows XP SP3 下用 4.2.2 版跑过,安装包 gp442win32.zip 解压后会得到 \gnuplot 文件夹,必须整个丢进 MetaTrader 5 客户端根目录才能和脚本互动。 验证步骤很直接:进 \gnuplot\binary 跑 wgnuplot.exe,在 gnupplot> 提示符敲 plot sin(x),弹窗画出正弦曲线就说明环境通了。之后启动 erremove.mq5,就会在单独窗口绘出分布图。 程序侧的交互极简:先写 gplot.txt 放 Gnuplot 命令和数据,再用 shell32.dll 导出的 ShellExecuteW() 调 wgnuplot.exe 并把这个文件当参数传进去,所以客户端要开「允许导入外部 dll」。 终端选型有个坑。wxt 终端抗锯齿、画质好,但关图窗口时 wgnuplot.exe 进程不会自动退;反复调用会堆出一堆僵尸进程。本文示例刻意用 windows 终端,命令行写 wgnuplot.exe -p MQL5\Files\gplot.txt,关窗口即杀进程,不留垃圾。图窗支持鼠标和键盘,活跃时按 h 能在文本窗列出默认热键。外汇与贵金属行情高波动,这类可视化仅用于辅助概率判断,不构成方向承诺。

◍ 用 h 估计量算清分布的四项特征

拿到一段行情采样,总体参数未知时只能靠无偏估计。采样平均值(数学期望估计)是分布中心,其他离差、标准方差、偏度、峰度都绕着它转。 峰度这块容易踩坑:正态分布的理论峰度是 3,但很多工具减掉 3 后报“超出峰度”。Excel 用 k 估计算超出量,而交易者统计书里常用 h 估计做无偏峰度,两者第四动差算法不同,表达式不一样,别混用。 下面 dStat() 直接按 h 估计落地。数组少于 4 个元素会返回 -1,因为中心动差到三阶以上样本量太小估计失效。中位数顺手排个序取中值,偶数长度取中间两数平均。 方差用 sum2/(n-1) 而非 n,偏度 skew=n*sum3/(n-2)/sum2/stdev,峰度那行乘了一串 (n-1)/(n-2)/(n-3) 修正项——这就是 h 估计的无偏处理。外汇和贵金属波动肥尾常见,算出来峰度显著大于 3 时,正态假设可能不成立,杠杆品种高风险。

MQL5 / C++
class="kw">struct statParam
  {
  class="type">class="kw">double mean;
  class="type">class="kw">double median;
  class="type">class="kw">double var;
  class="type">class="kw">double stdev;
  class="type">class="kw">double skew;
  class="type">class="kw">double kurt;
  };
class=class="str">"cmt">//----------------------------------------------------------------------------
class="type">int dStat(const class="type">class="kw">double &x[],statParam &sP)
  {
  class="type">int i,m,n;
  class="type">class="kw">double a,b,sum2,sum3,sum4,y[];

  ZeroMemory(sP);                                        class=class="str">"cmt">// Reset sP
  n=ArraySize(x);
  if(n<class="num">4)                                                class=class="str">"cmt">// Error
    {
    Print("Function dStat() error!");
    class="kw">return(-class="num">1);
    }
  sP.kurt=class="num">1.0;
  ArrayResize(y,n);
  ArrayCopy(y,x);
  ArraySort(y);
  m=(n-class="num">1)/class="num">2;
  sP.median=y[m];                                        class=class="str">"cmt">// Median
  if((n&0x01)==class="num">0)sP.median=(sP.median+y[m+class="num">1])/class="num">2.0;
  sP.mean=class="num">0;
  for(i=class="num">0;i<n;i++)sP.mean+=x[i];
  sP.mean/=n;                                            class=class="str">"cmt">// Mean
  sum2=class="num">0;sum3=class="num">0;sum4=class="num">0;  
  for(i=class="num">0;i<n;i++)
    {
    a=x[i]-sP.mean;
    b=a*a;sum2+=b;
    b=b*a;sum3+=b;
    b=b*a;sum4+=b;
    }
  if(sum2<class="num">1.e-150)class="kw">return(class="num">1);
  sP.var=sum2/(n-class="num">1);                                     class=class="str">"cmt">// Variance
  sP.stdev=MathSqrt(sP.var);                             class=class="str">"cmt">// Standard deviation
  sP.skew=n*sum3/(n-class="num">2)/sum2/sP.stdev;                   class=class="str">"cmt">// Skewness
  sP.kurt=((n*n-class="num">2*n+class="num">3)*sum4/sum2/sum2-(class="num">6.0*n-class="num">9.0)/n)*
                                    (n-class="num">1.0)/(n-class="num">2.0)/(n-class="num">3.0); class=class="str">"cmt">// Kurtosis

  class="kw">return(class="num">1);  

「用峰度算直方图分段数」

看采样分布不能只盯均值和标准差,有限样本的长相往往藏在直方图里。把数值范围切成若干段、数每段落进去的个数,再按段宽标准化,就得到经验分布密度的近似——这一步能直观判断采样是否贴近正态。 分段数不是拍脑袋定的。按交易者统计文献里的经验式,段数 L = (峰度+1.5) × N^0.4 / 6,N 是样本量、峰度来自已算好的 statParam。峰度越高尾巴越肥,分段就会自动变多,避免把厚尾误画成单峰。 下面的 dHist() 就是照这个公式落地的:传进来的 x[] 只读不改,histo[] 是动态数组接收结果,元素数 = 段数+2,头尾各补一个零值元素当边界。若 histo[] 不是动态数组、或 x[] 少于 4 个元素,函数直接返回 -1 并打印错误,不污染原序列。 别把正态当圣经 直方图画出来若明显比正态钟形胖尾或偏一边,外汇和贵金属这种高波动品种里往往意味着极端行情概率被低估,实盘前最好再叠一张正态概率图交叉看。

MQL5 / C++
class="kw">struct statParam
  {
  class="type">class="kw">double mean;
  class="type">class="kw">double median;
  class="type">class="kw">double var;
  class="type">class="kw">double stdev;
  class="type">class="kw">double skew;
  class="type">class="kw">double kurt;
  };
class=class="str">"cmt">//----------------------------------------------------------------------------
class="type">int dHist(const class="type">class="kw">double &x[],class="type">class="kw">double &histo[],const statParam &sp)
  {
  class="type">int i,k,n,nbar;
  class="type">class="kw">double a[],max,s,xmin;

  if(!ArrayIsDynamic(histo))                            class=class="str">"cmt">// Error
    {
    Print("Function dHist() error!");
    class="kw">return(-class="num">1);
    }
  n=ArraySize(x);
  if(n<class="num">4)                                               class=class="str">"cmt">// Error
    {
    Print("Function dHist() error!");
    class="kw">return(-class="num">1);
    }
  nbar=(sp.kurt+class="num">1.5)*MathPow(n,class="num">0.4)/class="num">6.0;
  if((nbar&0x01)==class="num">0)nbar--; if(nbar<class="num">5)nbar=class="num">5;           class=class="str">"cmt">// Number of bars
  ArrayResize(a,n);
  ArrayCopy(a,x);
  max=class="num">0.0;
  for(i=class="num">0;i<n;i++)
    {
    a[i]=(a[i]-sp.mean)/sp.stdev;                       class=class="str">"cmt">// Normalization
    if(MathAbs(a[i])>max)max=MathAbs(a[i]);
    }
  xmin=-max;
  s=class="num">2.0*max*n/nbar;
  ArrayResize(histo,nbar+class="num">2);
  ArrayInitialize(histo,class="num">0.0);
  histo[class="num">0]=class="num">0.0;histo[nbar+class="num">1]=class="num">0.0;
  for(i=class="num">0;i<n;i++)
    {
    k=(a[i]-xmin)/max/class="num">2.0*nbar;
    if(k>(nbar-class="num">1))k=nbar-class="num">1;
    histo[k+class="num">1]++;
    }
  for(i=class="num">0;i<nbar;i++)histo[i+class="num">1]/=s;

  class="kw">return(class="num">1);
  }

用 Rankit 法把收益序列拉成一条直线看正态

正态概率图的核心目的,是把样本序列做拉伸变换后画到坐标里——若价格收益率真服从正态,点会近似落在一条直线上。肉眼扫偏离程度,比跑一个 p 值更适合盯盘时快速判断分布假设是否成立。 计算这类图的坐标,靠的是 dRankit() 函数。它吃进原始数组 x[],把 Y 轴用的标准化值和 X 轴用的理论分位值分别写进 resp[] 与 xscale[],同时要求传入已算好的 statParam 结构(均值、标准差等)。样本数少于 4 会直接返回 -1,实盘里小样本画这图没意义。 X 轴分位值由 ltqnorm() 算逆正态累积分布得到,算法源自一篇计算逆 CDF 的论文。下面这段代码里,xscale 首尾用 0.5^(1/n) 对称处理,中间走 (i+1-0.3175)/(n+0.365) 的 Rankit 近似——换不同系数(比如 Blom 用 3/8)会让尾部点轻微偏移,MT5 里改那两个常数就能比对。 外汇与贵金属杠杆高,分布厚尾是常态,正态假设被拒是概率事件而非异常,别拿直线近似当风控依据。

MQL5 / C++
class="kw">struct statParam
  {
  class="type">class="kw">double mean;
  class="type">class="kw">double median;
  class="type">class="kw">double var;
  class="type">class="kw">double stdev;
  class="type">class="kw">double skew;
  class="type">class="kw">double kurt;
  };
class=class="str">"cmt">//----------------------------------------------------------------------------
class="type">int dRankit(const class="type">class="kw">double &x[],class="type">class="kw">double &resp[],class="type">class="kw">double &xscale[],const statParam &sp)
  {
  class="type">int i,n;
  class="type">class="kw">double np;

  if(!ArrayIsDynamic(resp)||!ArrayIsDynamic(xscale))      class=class="str">"cmt">// Error
    {
    Print("Function dHist() error!");
    class="kw">return(-class="num">1);
    }
  n=ArraySize(x);
  if(n<class="num">4)                                            class=class="str">"cmt">// Error
    {
    Print("Function dHist() error!");
    class="kw">return(-class="num">1);
    }
  ArrayResize(resp,n);
  ArrayCopy(resp,x);
  ArraySort(resp);
  for(i=class="num">0;i<n;i++)resp[i]=(resp[i]-sp.mean)/sp.stdev;
  ArrayResize(xscale,n);
  xscale[n-class="num">1]=MathPow(class="num">0.5,class="num">1.0/n);
  xscale[class="num">0]=class="num">1-xscale[n-class="num">1];
  np=n+class="num">0.365;
  for(i=class="num">1;i<(n-class="num">1);i++)xscale[i]=(i+class="num">1-class="num">0.3175)/np;
  for(i=class="num">0;i<n;i++)xscale[i]=ltqnorm(xscale[i]);

  class="kw">return(class="num">1);
  }
class=class="str">"cmt">//----------------------------------------------------------------------------
class="type">class="kw">double A1 = -class="num">3.969683028665376e+01, A2 =  class="num">2.209460984245205e+02,
       A3 = -class="num">2.759285104469687e+02, A4 =  class="num">1.383577518672690e+02,
       A5 = -class="num">3.066479806614716e+01, A6 =  class="num">2.506628277459239e+00;
class="type">class="kw">double B1 = -class="num">5.447609879822406e+01, B2 =  class="num">1.615858368580409e+02,
       B3 = -class="num">1.556989798598866e+02, B4 =  class="num">6.680131188771972e+01,
       B5 = -class="num">1.328068155288572e+01;
class="type">class="kw">double C1 = -class="num">7.784894002430293e-03, C2 = -class="num">3.223964580411365e-01,
       C3 = -class="num">2.400758277161838e+00, C4 = -class="num">2.549732539343734e+00,
       C5 =  class="num">4.374664141464968e+00, C6 =  class="num">2.938163982698783e+00;
class="type">class="kw">double D1 =  class="num">7.784695709041462e-03, D2 =  class="num">3.224671290700398e-01,

◍ 分尾区用对数变换兜底

中心区之外(p<0.02425 或 p>0.97575)直接套有理式会飘,代码改用 sqrt(-2*log(p)) 做自变量变换再喂进另一组系数。 上下尾的区别只在一个符号:下尾 s=1、上尾 s=-1,其余共用 C1~C6 与 D1~D4 的分子分母结构。D3 写死为 2.445134137142996、D4 为 3.754408661907416,是这组近似的固定常数。 把 ltqnorm 贴进 MT5 脚本,传入 0.001 和 0.999 各跑一次,返回约 -3.09 与 +3.09;外汇与贵金属波动带常落在这类极端分位,用前先认清杠杆下的高风险。

MQL5 / C++
class="type">class="kw">double ltqnorm(class="type">class="kw">double p)
  {
  class="type">int s=class="num">1;
  class="type">class="kw">double r,x,q=class="num">0;
  if(p<=class="num">0||p>=class="num">1){Print("Function ltqnorm() error!");class="kw">return(class="num">0);}
  if((p>=class="num">0.02425)&&(p<=class="num">0.97575))    class=class="str">"cmt">// Rational approximation for central region
    {
    q=p-class="num">0.5; r=q*q;
    x=(((((A1*r+A2)*r+A3)*r+A4)*r+A5)*r+A6)*q/(((((B1*r+B2)*r+B3)*r+B4)*r+B5)*r+class="num">1);
    class="kw">return(x);
    }
  if(p<class="num">0.02425)                      class=class="str">"cmt">// Rational approximation for lower region
    {
    q=sqrt(-class="num">2*log(p));
    s=class="num">1;
    }
  else    class=class="str">"cmt">//if(p>class="num">0.97575)              // Rational approximation for upper region
    {
    q = sqrt(-class="num">2*log(class="num">1-p));
    s=-class="num">1;
    }
  x=s*(((((C1*q+C2)*q+C3)*q+C4)*q+C5)*q+C6)/((((D1*q+D2)*q+D3)*q+D4)*q+class="num">1);
  class="kw">return(x);
  }
让小布替你跑这套
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到分布中心与异常值提示,把重复劳动交给小布,你专注决策。

常见问题

采样量不足时,少数极端值对均值和方差的拉动非常明显,静止过程假设也可能不成立,因此估计值只能当作概率倾向而非确定结论。
文中采用多中心估计取中值的方法,用中值、四分位距均值等五个估计排序列出第三个作为分布中心,以此降低异常值干扰,外汇贵金属跳空行情尤需留意高风险。
点越贴近参考直线,序列越可能接近正态分布;明显弯折意味着厚尾或偏态,后续模型选择要倾向非正态假设。
可以,品种页内置了分布形态与异常值检测,不用自己写 Gnuplot 脚本也能快速判断样本质量。
桶过少会掩盖形态,过多会放大噪声;可先按采样量开方试探,再结合概率图交叉验证更稳妥。