Box-Cox 变换·综合运用
(3/3)·从理论到落地,用七步流程把不规则报价转成正态可分析序列
Box-Cox 变换里的 Delta 边界与分位映射
在 MT5 里做价格序列的正态化预处理,CFullBoxCox 类先把输入数组 dat[] 的长度记进 Dlen,并给 Dat、Shift、BCDat、Cdf 四个内部数组按同一长度开空间。Cdf 存的是正态分位逆函数 ndtri 的输出,不是价格本身。 CalcPar 里分位点的算法值得盯一眼:首尾两个分位用 a=MathPow(0.5,1.0/Dlen) 算,中间第 i 个用 (i+0.6825)/(Dlen+0.365) 喂给 ndtri。全部 Cdf 元素平方和再开根得到 Scdf,这一步直接决定后续优化目标函数的尺度。 Delta 的上下界是硬算出来的:DeltaMin 固定为 1e-5 减序列最小值,DeltaMax 则是 (最大值-最小值)*200 减最小值。Lambda 初值给 1.0,Delta 初值取 (max-min)/2-min,随后丢进 Powell 法跑 Optimize(Par)。外汇与贵金属价格跳变频繁,这套边界在极端行情下可能把搜索空间拉得过大,实盘前建议用历史 tick 数据打印 Par[] 收敛轨迹。
class="type">class="kw">double DeltaMin; class=class="str">"cmt">// Delta minimum value class="type">class="kw">double DeltaMax; class=class="str">"cmt">// Delta maximum value class="type">class="kw">double Par[class="num">2]; class=class="str">"cmt">// parameters array class="kw">public: class="type">void CFullBoxCox(class="type">void) { } class="type">void CalcPar(class="type">class="kw">double &dat[]); class="type">class="kw">double GetPar(class="type">int n) { class="kw">return(Par[n]); } class="kw">private: class="type">class="kw">double ndtri(class="type">class="kw">double y0); class=class="str">"cmt">// the function opposite to the normal distribution function class="kw">virtual class="type">class="kw">double func(const class="type">class="kw">double &p[]); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| CalcPar | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CFullBoxCox::CalcPar(class="type">class="kw">double &dat[]) { class="type">int i; class="type">class="kw">double a,max,min; Dlen=ArraySize(dat); ArrayResize(Dat,Dlen); ArrayResize(Shift,Dlen); ArrayResize(BCDat,Dlen); ArrayResize(Cdf,Dlen); class=class="str">"cmt">//--- copy the input data array ArrayCopy(Dat,dat); Scdf=class="num">0; a=MathPow(class="num">0.5,class="num">1.0/Dlen); Cdf[Dlen-class="num">1]=ndtri(a); Scdf+=Cdf[Dlen-class="num">1]*Cdf[Dlen-class="num">1]; Cdf[class="num">0]=ndtri(class="num">1.0-a); Scdf+=Cdf[class="num">0]*Cdf[class="num">0]; a=Dlen+class="num">0.365; for(i=class="num">1;i<(Dlen-class="num">1);i++) { class=class="str">"cmt">//--- calculation of the distribution cumulative function Quantile Cdf[i]=ndtri((i+class="num">0.6825)/a); class=class="str">"cmt">//--- calculation of the sum of Quantile^class="num">2 Scdf+=Cdf[i]*Cdf[i]; } class=class="str">"cmt">//--- square root of the sum of Quantile^class="num">2 Scdf=MathSqrt(Scdf); min=dat[class="num">0]; max=min; for(i=class="num">0;i<Dlen;i++) { class=class="str">"cmt">//--- copy the input data a=dat[i]; Dat[i]=a; if(min>a)min=a; if(max<a)max=a; } class=class="str">"cmt">//--- Delta minimum value DeltaMin=class="num">1e-5-min; class=class="str">"cmt">//--- Delta maximum value DeltaMax=(max-min)*class="num">200-min; class=class="str">"cmt">//--- Lambda initial value Par[class="num">0]=class="num">1.0; class=class="str">"cmt">//--- Delta initial value Par[class="num">1]=(max-min)/class="num">2-min; class=class="str">"cmt">//--- optimization using Powell method Optimize(Par); }
「Box-Cox 变换里的参数边界与正态贴合度计算」
在 MT5 里做价格序列的 Box-Cox 变换时,Lambda 和 Delta 不能无限制搜索。上面这段 CFullBoxCox::func 给 Lambda 加了 ±5.0 的硬边界,超出后按 (lam-5.0)*400 这类惩罚项累加进返回值,相当于把越界的解往回拉。Delta 则依赖外部定义的 DeltaMax / DeltaMin 截断,越界同样乘 400 做惩罚。 几何均值 gm 先由 Dat[i]+del 取对数求和再除以 Dlen 做 MathExp 还原,gmpow 用 lam*MathPow(gm,lam-1) 作尺度因子。Lambda 非零时走幂变换分支,Lambda 为零走对数分支,两套都写出 BCDat[i] 并累加 mean。 变换完的 BCDat 直接 ArraySort,再和预设的 Cdf 数组做相关:ret = a/(Scdf*MathSqrt(b))。最终返回值是 k1+k2-ret,也就是惩罚减去正态相关系数。开 MT5 把 Dlen 设成你盯的 K 线数,调 Lambda 初值从 -5 到 5 扫一遍,能直观看到 ret 在哪一截最高。 外汇与贵金属波动有跳空和异方差,Box-Cox 只是把分布往正态拉,不保证后续信号胜率,实盘前请用历史数据验证。
class="type">class="kw">double CFullBoxCox::func(const class="type">class="kw">double &p[]) { class="type">int i; class="type">class="kw">double a,b,c,lam,del,k1,k2,gm,gmpow,mean,ret; lam=p[class="num">0]; del=p[class="num">1]; k1=class="num">0; k2=class="num">0; if (lam>class="num">5.0){k1=(lam-class="num">5.0)*class="num">400; lam=class="num">5.0;} class=class="str">"cmt">// Lambda > class="num">5.0 else if(lam<-class="num">5.0){k1=-(lam+class="num">5.0)*class="num">400; lam=-class="num">5.0;} class=class="str">"cmt">// Lambda < -class="num">5.0 if (del>DeltaMax){k2=(del-DeltaMax)*class="num">400; del=DeltaMax;} class=class="str">"cmt">// Delta > DeltaMax else if(del<DeltaMin){k2=(DeltaMin-del)*class="num">400; del=DeltaMin; class=class="str">"cmt">// Delta < DeltaMin gm=class="num">0; for(i=class="num">0;i<Dlen;i++) { Shift[i]=Dat[i]+del; gm+=MathLog(Shift[i]); } class=class="str">"cmt">//--- geometric mean gm=MathExp(gm/Dlen); gmpow=lam*MathPow(gm,lam-class="num">1); mean=class="num">0; class=class="str">"cmt">//--- Lambda != class="num">0.0 if(lam!=class="num">0) { for(i=class="num">0;i<Dlen;i++) { a=(MathPow(Shift[i],lam)-class="num">1.0)/gmpow; class=class="str">"cmt">//--- transformed data(Box-Cox) BCDat[i]=a; class=class="str">"cmt">//--- average value mean+=a; } } class=class="str">"cmt">//--- Lambda == class="num">0.0 else { for(i=class="num">0;i<Dlen;i++) { a=gm*MathLog(Shift[i]); class=class="str">"cmt">//--- transformed data(Box-Cox) BCDat[i]=a; class=class="str">"cmt">//--- average value mean+=a; } } mean=mean/Dlen; class=class="str">"cmt">//--- sorting of the transformed data array ArraySort(BCDat); a=class="num">0; b=class="num">0; for(i=class="num">0;i<Dlen;i++) { c=(BCDat[i]-mean); a+=Cdf[i]*c; b+=c*c; } class=class="str">"cmt">//--- correlation coefficient ret=a/(Scdf*MathSqrt(b)); class="kw">return(k1+k2-ret); }
◍ 误差函数底层的常量与多项式系数
在 MT5 里手写误差函数(erf)或相关概率计算时,核心是一组静态常量与分段有理逼近系数。下面这段声明直接给出了 sqrt(2π) 的近似值 2.50662827463100050242,以及 P0/Q0、P1/Q1、P2/Q2 三组多项式系数,分别对应不同自变量区间的逼近公式。 这些系数不是拍脑袋来的:P0 有 5 项、Q0 有 8 项,P1/P2 各 9 项、Q1/Q2 各 8 项,精度跑到双精度尾段。比如 P0[0] 是 -59.9633501014107895267,Q0[3] 是 -225.462687854119370527,数量级跨度很大,复制时漏一位就会让尾部概率算偏。
| [CODE] 里的声明可以直接塞进你的 .mq5 脚本头部,配合分段判断 | x | 大小来选 P0Q0 / P1Q1 / P2Q2 做除法,就能在 EA 里脱离标准库自己算正态累积概率。外汇与贵金属波动大,这类自算模块仅用于策略研究,实盘高杠杆风险自负。 |
|---|
class="kw">static class="type">class="kw">double s2pi =class="num">2.50662827463100050242E0; class=class="str">"cmt">// sqrt(2pi) class="kw">static class="type">class="kw">double P0[class="num">5]={-class="num">5.99633501014107895267E1, class="num">9.80010754185999661536E1, -class="num">5.66762857469070293439E1, class="num">1.39312609387279679503E1, -class="num">1.23916583867381258016E0}; class="kw">static class="type">class="kw">double Q0[class="num">8]={ class="num">1.95448858338141759834E0, class="num">4.67627912898881538453E0, class="num">8.63602421390890590575E1, -class="num">2.25462687854119370527E2, class="num">2.00260212380060660359E2, -class="num">8.20372256168333339912E1, class="num">1.59056225126211695515E1, -class="num">1.18331621121330003142E0}; class="kw">static class="type">class="kw">double P1[class="num">9]={ class="num">4.05544892305962419923E0, class="num">3.15251094599893866154E1, class="num">5.71628192246421288162E1, class="num">4.40805073893200834700E1, class="num">1.46849561928858024014E1, class="num">2.18663306850790267539E0, -class="num">1.40256079171354495875E-1,-class="num">3.50424626827848203418E-2, -class="num">8.57456785154685413611E-4}; class="kw">static class="type">class="kw">double Q1[class="num">8]={ class="num">1.57799883256466749731E1, class="num">4.53907635128879210584E1, class="num">4.13172038254672030440E1, class="num">1.50425385692907503408E1, class="num">2.50464946208309415979E0, -class="num">1.42182922854787788574E-1, -class="num">3.80806407691578277194E-2,-class="num">9.33259480895457427372E-4}; class="kw">static class="type">class="kw">double P2[class="num">9]={ class="num">3.23774891776946035970E0, class="num">6.91522889068984211695E0, class="num">3.93881025292474443415E0, class="num">1.33303460815807542389E0, class="num">2.01485389549179081538E-1, class="num">1.23716634817820021358E-2, class="num">3.01581553508235416007E-4, class="num">2.65806974686737550832E-6, class="num">6.23974539184983293730E-9}; class="kw">static class="type">class="kw">double Q2[class="num">8]={ class="num">6.02427039364742014255E0, class="num">3.67983563856160859403E0,
逆正态累积分布的分段逼近实现
这段 ndtr 反函数(分位函数)的核心,是把输入概率 y0 按阈值 0.13533528323661269189(即 exp(-2))一分为二:大于该值走中心多项式逼近,小于则走尾部分支的对数变换。 当 y 落在 (exp(-2), 1-exp(-2)) 区间时,代码先做 y-=0.5 的对称偏移,再用 P0/Q0 两组系数(最高 7 阶)做有理逼近,最后乘 s2pi 返回。该区间覆盖了约 73% 的常用置信带,日常 1σ~2σ 转换直接走这里。 尾部分支先算 x=sqrt(-2*log(y)),再按 x<8.0 切到 P1/Q1 或 P2/Q2 两组 8 阶系数做修正;x<8 对应 y>1.266e-14(exp(-32)),再往下概率 MT5 双精度已难稳定表达。 下面这段是原文尾部分支与返回逻辑的直接抽取,逐行对应上述分支: // y0 越界直接报错返回极值 if(y0<=0.0){Print("Function ndtri() error!"); return(-DBL_MAX);} if(y0>=1.0){Print("Function ndtri() error!"); return(DBL_MAX);} // code=1 标记原分支,y>1-exp(-2) 时翻转并 code=0 code=1; y=y0; if(y>(1.0-0.13533528323661269189)){y=1.0-y; code=0;} // 中心区:偏移后有理逼近 if(y>0.13533528323661269189){ y=y-0.5; y2=y*y; a=P0[0]; for(i=1;i<5;i++)a=a*y2+P0[i]; b=y2+Q0[0]; for(i=1;i<8;i++)b=b*y2+Q0[i]; x=y+y*(y2*a/b); x=x*s2pi; return(x); } // 尾部:对数初值 + 分段修正 x=MathSqrt(-2.0*MathLog(y)); x0=x-MathLog(x)/x; z=1.0/x; if(x<8.0){ a=P1[0]; for(i=1;i<9;i++)a=a*z+P1[i]; b=z+Q1[0]; for(i=1;i<8;i++)b=b*z+Q1[i]; x1=z*a/b; } else { a=P2[0]; for(i=1;i<9;i++)a=a*z+P2[i]; b=z+Q2[0]; for(i=1;i<8;i++)b=b*z+Q2[i]; x1=z*a/b; } x=x0-x1; if(code!=0)x=-x; return(x); 在 MT5 里接 CFullBoxCox 类调用该分位函数,可把 Box-Cox 变换后的统计量反查回正态分位;外汇与贵金属价格序列做这类变换前须明确——杠杆市场高波动可能让尾部分支溢出,实盘验证务必用小样本先跑。
if(y0<=class="num">0.0){Print("Function ndtri() error!"); class="kw">return(-DBL_MAX);} if(y0>=class="num">1.0){Print("Function ndtri() error!"); class="kw">return(DBL_MAX);} code=class="num">1; y=y0; if(y>(class="num">1.0-class="num">0.13533528323661269189)){y=class="num">1.0-y; code=class="num">0;} if(y>class="num">0.13533528323661269189){ y=y-class="num">0.5; y2=y*y; a=P0[class="num">0]; for(i=class="num">1;i<class="num">5;i++)a=a*y2+P0[i]; b=y2+Q0[class="num">0]; for(i=class="num">1;i<class="num">8;i++)b=b*y2+Q0[i]; x=y+y*(y2*a/b); x=x*s2pi; class="kw">return(x); } x=MathSqrt(-class="num">2.0*MathLog(y)); x0=x-MathLog(x)/x; z=class="num">1.0/x; if(x<class="num">8.0){ a=P1[class="num">0]; for(i=class="num">1;i<class="num">9;i++)a=a*z+P1[i]; b=z+Q1[class="num">0]; for(i=class="num">1;i<class="num">8;i++)b=b*z+Q1[i]; x1=z*a/b; } else { a=P2[class="num">0]; for(i=class="num">1;i<class="num">9;i++)a=a*z+P2[i]; b=z+Q2[class="num">0]; for(i=class="num">1;i<class="num">8;i++)b=b*z+Q2[i]; x1=z*a/b; } x=x0-x1; if(code!=class="num">0)x=-x; class="kw">return(x);
「把 EURUSD 报价跑成 Box-Cox 变换数组」
这段脚本干的事很直接:从本地 CSV 读入 1200 根 EURUSD M1 收盘价,用 Box-Cox 把原始序列压成近似正态的 bcdat[],方便后续做均值回归或波动率建模。外汇与贵金属属高风险品种,变换只是预处理,不预示方向。 OnStart 里先调 Bc.CalcPar(dat) 自动搜 lambda 和 delta,再打印迭代次数与参数值;例如某次跑下来可能输出 lambda=0.3521、delta=0.0008。之后对每个点做 (shift[i]^lambda - 1)/gmpow,lambda 为 0 时退化为 gm*log(shift[i])。 readCSV 用 FILE_CSV|FILE_ANSI 打开,初始数组 512、每次快满就加 128,读完裁到实际长度 n-1。路径写死 Dataset2\\EURUSD_M1_1200.txt,开 MT5 前先把文件丢进 MQL5\\Files 对应目录,否则 FileOpen 返回 INVALID_HANDLE 直接报错退出。
class="type">void OnStart() { class="type">int i,n; class="type">class="kw">double dat[],shift[],bcdat[],lambda,delta,gm,gmpow; class="type">class="kw">string fname; class=class="str">"cmt">//--- input file name fname="Dataset2\\EURUSD_M1_1200.txt"; class=class="str">"cmt">//--- reading the data if(readCSV(fname,dat)<class="num">0){Print("Error."); class="kw">return;} class=class="str">"cmt">//--- data size n=ArraySize(dat); class=class="str">"cmt">//--- shifted input data array ArrayResize(shift,n); class=class="str">"cmt">//--- transformed data array ArrayResize(bcdat,n); class=class="str">"cmt">//--- lambda and delta parameters optimization Bc.CalcPar(dat); lambda=Bc.GetPar(class="num">0); delta=Bc.GetPar(class="num">1); PrintFormat("Iterations= %i, lambda= %.4f, delta= %.4f", Bc.GetIter(),lambda,delta); gm=class="num">0; for(i=class="num">0;i<n;i++) { shift[i]=dat[i]+delta; gm+=MathLog(shift[i]); } class=class="str">"cmt">//--- geometric mean gm=MathExp(gm/n); gmpow=lambda*MathPow(gm,lambda-class="num">1); if(lambda!=class="num">0){for(i=class="num">0;i<n;i++)bcdat[i]=(MathPow(shift[i],lambda)-class="num">1.0)/gmpow;} else {for(i=class="num">0;i<n;i++)bcdat[i]=gm*MathLog(shift[i]);} class=class="str">"cmt">//--- dat[] <-- input data class=class="str">"cmt">//--- shift[] <-- input data with the shift class=class="str">"cmt">//--- bcdat[] <-- transformed data } class="type">int readCSV(class="type">class="kw">string fnam,class="type">class="kw">double &dat[]) { class="type">int n,asize,fhand; fhand=FileOpen(fnam,FILE_READ|FILE_CSV|FILE_ANSI); if(fhand==INVALID_HANDLE) { Print("FileOpen Error!"); class="kw">return(-class="num">1); } asize=class="num">512; ArrayResize(dat,asize); n=class="num">0; while(FileIsEnding(fhand)!=true) { dat[n++]=FileReadNumber(fhand); if(n+class="num">128>asize) { asize+=class="num">128; ArrayResize(dat,asize); } } FileClose(fhand); ArrayResize(dat,n-class="num">1); class="kw">return(class="num">0); }
◍ 预测场景下的参数反搜逻辑
Box-Cox 变换在多数人认知里是「把序列弄成正态」的预处理手段,但在 MT5 做时间序列预测时,它可以反过来用:以预测误差最小化为目标去反搜变换参数与模型参数。 具体回路是:先给定变换参数初值与预测模型,对输入做 Box-Cox 正变换,按当前参数出预测,再做反向变换还原,用原始未变换序列算误差,然后改参数重跑,直到误差下不去为止。这一步里,变换目标已经不是正态性,而是「哪种分布规律配这套预测法误差最低」。 实测中该分布往往不必须是正态,取决于你选的是线性回归还是其他外推法。另一个硬约束是:Box-Cox 只吃正值非零序列,碰到含零或负的行情序列必须先整体移位再变换,这算它唯一的明显短板。 即便有移位麻烦,它在同类变换里依旧是最通用且省心的选项,外汇与贵金属回测中高频出现零值缺口时,移位量设 1e-6 通常够用,但这类品种杠杆高、跳空多,实盘验证前务必小资金试错。
把工具请下神坛
Box-Cox 变换附带的 91.48 KB ZIP 里是可直接在 MT5 跑的 MQL5 实现,但别把它当正态化的银弹。作者在原讨论里自己承认,重复套用变换效果会衰减,且可能把原始序列特征洗得面目全非,外汇与贵金属行情本身高波动、高杠杆,盲信「变换后就能用经典统计」容易过度拟合。 读者真正该做的,是下载那包代码,在 EURUSD 的 H1 上对比变换前后 Q-Q 图偏离度,只在首次变换显著提升近似正态时才纳入过滤逻辑。工具是箱子里的扳手,不是占卜的水晶球。