统计分布在交易者工作中的作用·进阶篇
📘

统计分布在交易者工作中的作用·进阶篇

第 2/2 篇

从样本清洗到正态性检验的实操链路

做价格行为统计前,先把样本里的异常值剔掉。异常值就是明显偏离主体分布的那些观测(上下都算),没有万能删法,可借由 Bulashev 提出的思路配合 OOP 封装成 CExpStatistics 类,它返回剔除异常值后的 Parr[],同时给出现实样本的描述性统计。 清洗完用 Sturges 公式算直方图分组数:k = 1 + log2(n),n 是样本量。MQL5 里算好 k 后调 Allocate 把观测塞进 f[i] 类、b[i] 存类中点,再用 histogramSave 导成 HTML 看图。我跑 volatilityTest.mq5 取 EURUSD H4 最近 500 根柱的「最高-最低」绝对差,第一类 146 个、第二类 176 个观测;returnsTest.mq5 取同周期标准化对数收益,第四类 244 个、第五类 124 个。 图上看这两种分布都不像正态,但别只信眼睛。惯例先测主假设:用 Jarque-Bera 检验。jarqueberatest 吃初始样本 x、吐出 p 值(零假设成立却被拒的概率)。returnsTest 跑 EURUSD H4 标准化收益,p = 0.0000,拒正态;volatilityTest 跑绝对波动,结论类似。外汇与贵金属波动分布异于正态是高频现象,杠杆交易高风险,参数估计别直接套正态模型。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expected Statistics class definition |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CExpStatistics
  {
class="kw">private:
   class="type">class="kw">double          arr[];       class=class="str">"cmt">//initial array
   class="type">int             N;           class=class="str">"cmt">//initial array size
   class="type">class="kw">double          Parr[];      class=class="str">"cmt">//processed array
   class="type">int             pN;          class=class="str">"cmt">//processed array size
   class="type">void            stdz(class="type">class="kw">double &outArr_st[],class="type">bool A); class=class="str">"cmt">//standardization
class="kw">public:
   class="type">void            setArrays(class="type">bool A,class="type">class="kw">double &Arr[],class="type">int &n); class=class="str">"cmt">//set array for processing
   class="type">bool            isProcessed;  class=class="str">"cmt">//array processed?
   class="type">void            CExpStatistics(){};  class=class="str">"cmt">//constructor
   class="type">void            setCExpStatistics(class="type">class="kw">double &Arr[]); class=class="str">"cmt">//set the initial array for the class
   class="type">void            ZeroCheckArray(class="type">bool A); class=class="str">"cmt">//check the input array for zero elements
   class="type">int             get_arr_N();           class=class="str">"cmt">//get the initial array length
   class="type">class="kw">double          median(class="type">bool A);        class=class="str">"cmt">//median
   class="type">class="kw">double          median50(class="type">bool A); class=class="str">"cmt">//median of class="num">50% interquantile range(midquartile range)

「统计类接口与分组函数的实作细节」

在 MT5 里做样本分布分析,先得把 CExpStatistics 这类封装看明白。它暴露的接口覆盖了从全样本均值到截尾处理的完整链路:mean 取初始样本全体均值,mean50 只算 50% 分位距内的均值,interqtlRange 给四分位距,expKurtosis 顺带通过引用参数把偏度一起算出来。 outlierDelete 负责删离群值,pArrOutput 把处理后数组吐出来,censorR 返回截尾系数。这些函数都带 bool A 参数,一般用来切换「全部样本 / 过滤后样本」两套状态,写 EA 时别写死。 Sturges 函数按斯特奇斯法则定分组数:s = 1 + log2(n),且硬性封顶 15 组。你拿 1000 根 H1 收盘价跑,log2(1000)≈9.97,得出约 11 组;样本过万也只会给 15 组,这是经验上限不是 bug。 Allocate 做观测落组:先取数组极小 t、极大 c,再按正负分别乘 0.99 / 1.01 外扩边界,避免极值卡在组缘。外汇与贵金属波动跳变多,这种外扩能降低边界样本误归,但高频品种仍建议先 outlierDelete 再 Allocate。

MQL5 / C++
class="type">class="kw">double mean(class="type">bool A); class=class="str">"cmt">//mean of the entire initial sample
class="type">class="kw">double mean50(class="type">bool A); class=class="str">"cmt">//mean of class="num">50% interquantile range
class="type">class="kw">double interqtlRange(class="type">bool A); class=class="str">"cmt">//interquartile range
class="type">class="kw">double RangeCenter(class="type">bool A); class=class="str">"cmt">//range center
class="type">class="kw">double meanCenter(class="type">bool A); class=class="str">"cmt">//mean of the top five estimates
class="type">class="kw">double expVariance(class="type">bool A); class=class="str">"cmt">//estimated variance
class="type">class="kw">double expSampleVariance(class="type">bool A); class=class="str">"cmt">//shifted estimate of sample variance
class="type">class="kw">double expStddev(class="type">bool A); class=class="str">"cmt">//estimated standard deviation
class="type">class="kw">double Moment(class="type">int index,class="type">bool A,class="type">int sw,class="type">class="kw">double xm); class=class="str">"cmt">//moment of distribution
class="type">class="kw">double expKurtosis(class="type">bool A,class="type">class="kw">double &Skewness); class=class="str">"cmt">////estimated kurtosis and skewness
class="type">class="kw">double censorR(class="type">bool A); class=class="str">"cmt">//censoring coefficient
class="type">int outlierDelete(); class=class="str">"cmt">//deletion of outliers from the sample
class="type">int pArrOutput(class="type">class="kw">double &outArr[],class="type">bool St); class=class="str">"cmt">//processed array output
class="type">void ~CExpStatistics(){};class=class="str">"cmt">//destructor
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int Sturges(class="type">int n)
class=class="str">"cmt">/*
 Function for determining the number of class intervals using Sturges&class="macro">#x27; rule.
 Variables:
 y is the number of sampling observations.
 Returned value:
 number of class intervals.
*/
{
 class="type">class="kw">double s; class=class="str">"cmt">// Returned value
 s=class="num">1.+log2(y);
 if(s>class="num">15) class=class="str">"cmt">// Empirical rule
 s=class="num">15;
 class="kw">return(class="type">int) floor(s);
}
class="type">void Allocate(class="type">class="kw">double &data[],class="type">int n,class="type">class="kw">double &f[],class="type">class="kw">double &b[],class="type">int k)
class=class="str">"cmt">/*
Function for allocating observations to classes.
Variables:
 class="num">1) data — initial sample(array)
 class="num">2) n — sample size
 class="num">3) f — calculated array of observations allocated to classes
 class="num">4) b — array of class midpoints
 class="num">5) k — number of classes
*/
 {
 class="type">int i,j; class=class="str">"cmt">// Loop counter
 class="type">class="kw">double t,c; class=class="str">"cmt">// Auxiliary variable
 t=data[ArrayMinimum(data)]; class=class="str">"cmt">// Sample minimum
 t=t>class="num">0 ? t*class="num">0.99 : t*class="num">1.01;
 c=data[ArrayMaximum(data)]; class=class="str">"cmt">// Sample maximum
 c=c>class="num">0 ? c*class="num">1.01 : c*class="num">0.99;

◍ 分组区间与雅克-贝拉正态检验的实现细节

这段代码先把连续样本切进等宽分组:组距的一半 c 由 (c-t)/k/2 算出,b[0] 设为左端点 t 加 c 作为第一组中点,随后用循环以 2c 步长递推其余中点,f 数组清零准备计数。 双层循环做归组:对每一个样本 data[i],找到满足 b[j]-c < data[i] <= b[j]+c 的组就 f[j]++ 并 break,复杂度为 O(n·k),在 MT5 脚本里跑几千样本、k=20 组时基本无感延迟。 jarqueberatest 封装了 Jarque-Bera 检验:样本量 n<5 直接返回 p=1.0 表示无法拒绝正态原假设;n 够大则先算统计量 s,再用近似函数得 p 值。 别把正态当圣经:外汇与贵金属收益率在极端行情常呈厚尾,p 值偏高只代表「未检出非正态」,不代表下一根 K 线分布就乖巧,实盘前务必用历史数据自测。

MQL5 / C++
  c=(c-t)/k/class="num">2;                      class=class="str">"cmt">// Half of the class interval
  b[class="num">0]=t+c;                         class=class="str">"cmt">// Array of class interval midpoints
  f[class="num">0]= class="num">0;
  for(i=class="num">1; i<k; i++)
    {
      b[i] = b[i - class="num">1] + c + c;
      f[i] = class="num">0;
    }
class=class="str">"cmt">// Grouping
  for(i=class="num">0; i<n; i++)
      for(j=class="num">0; j<k; j++)
        if(data[i]>b[j]-c && data[i]<=b[j]+c)
          {
            f[j]++;
            break;
          }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//                                                                  the Jarque-Bera Test                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void jarqueberatest(class="type">class="kw">double &x[],class="type">class="kw">double &p)
class=class="str">"cmt">/*
  The Jarque-Bera test is used to check hypothesis about the fact that
   a given sample xS  is a sample of normal random variable with unknown
   mean and variance.
  Variables:
    x - sample Xs;
    p - p-value;
*/
  {
   class="type">int n=ArraySize(x);
   class="type">class="kw">double s;
   p=class="num">0.;
   if(n<class="num">5)class=class="str">"cmt">//N is too small
     {
      p=class="num">1.0;
      class="kw">return;
     }
class=class="str">"cmt">//N is large enough
   jarquebera_jarqueberastatistic(x,n,s);
   p=jarquebera_jarqueberaapprox(n,s);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

用卡方检验给行情样本找理论分布

把经验分布和正态分布硬套往往不成立,因为样本自身的正态性本就存疑,且分布参数未知。实际做法是用非参数检验,比如 χ2 拟合优度测试(基于 Pearson 度量),用经验分布估出的参数去填理论公式的坑。 MT5 里 chsone 函数就是干这个的:它拿观察频数数组 f[] 和预期频数数组 ebins[] 算卡方值和接受零假设的概率。下面这段是核心调用逻辑,逐行拆一下: void chsone(double &f[],double &ebins[],double &df, double &chsq,double &prob,const int knstrn=1) /* 1) f — 观察频数分组数组 2) ebins - 预期频数数组 3) df - 自由度 4) chsq — 卡方统计量 5) prob - 接受真零假设的概率 6) knstrn — 约束数 */ { CGamma gam; // 用 CGamma 类实例算不完全 gamma 函数 int j,nbins=ArraySize(bins),q,g; // nbins 取 bins 数组长度 double temp; df=nbins-knstrn; // 自由度 = 分组数减约束 chsq=0.0; // 卡方初值清 0 q=nbins/2; g=nbins-1; for(j=0;j<nbins/2;j++) // 先扫分布左半边 {

if(ebins[j]<0.0(ebins[j]==0. && bins[j]>0.))

Alert("Bad expected number in chsone!"); // 预期频数非法就报警 if(ebins[j]<=5.0) { --df; // 预期频数过小,自由度减 1 ebins[j+1]+=ebins[j]; // 合并到下一分组 拿 EURUSD H4 的标准化收益跑 returnsTest.mq5,第一感觉正态不合适(峰度太大),换双曲线正割分布 HS(-0.00, 1.00),输出 Chi-square statistic:1.89; probability:0.8648——卡方极小,拟合良好。波动数据换对数正态 Logn(6.09,0.53),卡方 6.17、p=0.4040,p=0.05 水平下不能拒零假设。 randomTest.mq5 能按设定参数造随机样本:X~Nor(3.50,2.77) 生成后再读回检验,Jarque-Bera p=0.9381、χ2 p=0.9843,参数被估成 Nor(3.58,2.94),偏差很小。fitAll.mq5 则一次性尝试所有可用分布并吐 HTML 报告,正态 p=0.9926 最佳,但贝塔分布常因参数不匹配报 cannot be estimated。外汇贵金属波动拟合只是概率描述,实盘高杠杆下分布尾部风险仍可能击穿模型。

MQL5 / C++
class="type">void chsone(class="type">class="kw">double &f[],class="type">class="kw">double &ebins[],class="type">class="kw">double &df,
              class="type">class="kw">double &chsq,class="type">class="kw">double &prob,const class="type">int knstrn=class="num">1)
class=class="str">"cmt">/*
  class="num">1) f — array of observations allocated to classes
  class="num">2) ebins - array of expected frequencies
  class="num">3) df - number of degrees of freedom
  class="num">3) chsq — chi-square statistics
  class="num">4) prob - probability of accepting a true null hypothesis
  class="num">5) knstrn — constraint
*/
  {
  CGamma gam;
  class="type">int j,nbins=ArraySize(bins),q,g;
  class="type">class="kw">double temp;
  df=nbins-knstrn;
  chsq=class="num">0.0;
  q=nbins/class="num">2;
  g=nbins-class="num">1;
  for(j=class="num">0;j<nbins/class="num">2;j++) class=class="str">"cmt">//passing through the left side of the distribution
    {
    if(ebins[j]<class="num">0.0 || (ebins[j]==class="num">0. && bins[j]>class="num">0.))
      Alert("Bad expected number in chsone!");
    if(ebins[j]<=class="num">5.0)
      {
      --df;
      ebins[j+class="num">1]+=ebins[j];

「卡方拟合的右尾合并与自由度补偿」

上面的代码片段只做一件事:从分布右半边往中心收口,把期望频数过小的格子并到相邻格,避免卡方项被零或近零分母放大。 循环从最后一个 bin 倒推到 nbins/2-1,先判 ebins[j] 是否为负,或在期望为 0 却观测大于 0 时弹 Alert,这属于数值前置防护。若 ebins[j]<=5.0,就把该格期望与实测都加到 j-1 格,同时 --df 扣掉一个自由度;否则照常算 (bins[j]-ebins[j])^2/ebins[j] 累加进 chsq。 右半处理完,若 df 被减到小于 1,则强制置 1 做补偿。最后用 gammq(0.5*df, 0.5*chsq) 取卡方上尾概率 prob——这个值越接近 1,样本越倾向符合原假设分布。外汇与贵金属 tick 分布检验属高风险场景,prob 仅作统计参考,不能直接推导行情方向。

MQL5 / C++
      bins[j+class="num">1]+=bins[j];
      }
   else
      {
      temp=bins[j]-ebins[j];
      chsq+=pow(temp,class="num">2)/ebins[j];
      }
   }
 for(j=nbins-class="num">1;j>nbins/class="num">2-class="num">1;j--) class=class="str">"cmt">//passing through the right side of the distribution
   {
   if(ebins[j]<class="num">0.0 || (ebins[j]==class="num">0. && bins[j]>class="num">0.))
      Alert("Bad expected number in chsone!");
   if(ebins[j]<=class="num">5.0)
      {
      --df;
      ebins[j-class="num">1]+=ebins[j];   class=class="str">"cmt">//starting with the last class
      bins[j-class="num">1]+=bins[j];
      }
   else
      {
      temp=bins[j]-ebins[j];
      chsq+=pow(temp,class="num">2)/ebins[j];
      }
   }
 if(df<class="num">1)df=class="num">1; class=class="str">"cmt">//compensate
 prob=gam.gammq(class="num">0.5*df,class="num">0.5*chsq); class=class="str">"cmt">//Chi-square probability function
}

◍ 把概率视角带进EA和品种评估

这套推导本身并不算完全成功,序列始终在变,尤其对数收益序列的拟合并不干净。作者也明确没把「评估方法优劣」当成本文任务,只是抛出一个从概率角度看市场、工具和EA的思路,留给有兴趣的人继续啃。 真要动手验证,把 volatilityTest.mq5、returnsTest.mq5、randomTest.mq5 三个脚本丢进 %MetaTrader%\MQL5\Scripts,配合 Include 下的 Distribution_class.mqh 等四个库,跑完会生成 Volat.csv、Returns_std.csv 和 Randoms.csv,再让 fitAll.mq5 出 chi_test.htm 报告。EURUSD H4 的样本跑下来,分布拟合偏差大概率集中在尾部。 外汇和贵金属是高杠杆品种,这类统计结论只描述「样本长什么样」,不预示下一根K线方向;拿去写EA前,先在策略测试器用不同年份重采样一遍再说。

常见问题

先用分位数或标准差阈值框定正常区间,超出 3 倍标准差的孤立点大概率是非流动性误报,可剔除;保留连续行情中的正常跳空。
p 值小于 0.05 时拒绝正态假设,此时别硬套均值方差模型,改用非参数或重采样方法评估品种波动更稳妥。
可以,小布能直接对指定品种的历史样本做清洗、分组并输出正态性与卡方检验结论,省去手写脚本的重复劳动。
把右尾相邻区间合并到期望频数大于 5,自由度 = 合并后区间数 − 1 − 被估参数个数,避免自由度虚高导致误判。
先算样本偏度和峰度再选检验,若拒绝正态就用卡方找理论分布,据此设止损间距和仓位,比固定点数更贴合品种风险。