统计分布在交易者工作中的作用·进阶篇
从样本清洗到正态性检验的实操链路
做价格行为统计前,先把样本里的异常值剔掉。异常值就是明显偏离主体分布的那些观测(上下都算),没有万能删法,可借由 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 跑绝对波动,结论类似。外汇与贵金属波动分布异于正态是高频现象,杠杆交易高风险,参数估计别直接套正态模型。
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。
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 线分布就乖巧,实盘前务必用历史数据自测。
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。外汇贵金属波动拟合只是概率描述,实盘高杠杆下分布尾部风险仍可能击穿模型。
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 仅作统计参考,不能直接推导行情方向。
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前,先在策略测试器用不同年份重采样一遍再说。