Box-Cox 变换·进阶篇
📊

Box-Cox 变换·进阶篇

(2/3)· 接上篇概念铺垫,这一篇拆开报价序列的去趋势与变换通用式,少走正态幻觉的弯路

进阶 第 2/3 篇
很多交易者直接把回归、方差分析套在原始外汇报价上,忽略序列既不稳定也不正态。用标准参数化方法硬算,结果看起来漂亮,实际只是自欺。先把分布规律处理接近正态,再谈模型可靠性。

用 Box-Cox 把偏态序列拉回近似正态

这段脚本演示了如何对一组随机生成的价格类序列做 Box-Cox 变换,核心目的是消掉数据的偏度,让它更接近正态分布,方便后续做统计检验或信号建模。外汇与贵金属行情天然厚尾偏态,直接套用正态假设容易误判,这类变换在实盘前处理里有实用价值,但市场高风险,变换后也不代表预测胜率提升。 脚本先定下样本量 n=1600,用 Xor128 随机数发生器填了一组正态噪声进 dat[],再把 dat[] 整体下移避开非正值,并按 0.35 次幂做了一次预扭曲。之后交给 CBoxCox 类做 lambda 寻优,CalcPar 跑完用 GetPar(0) 拿出最优 lambda,PrintFormat 会打出迭代次数和 lambda 保留四位小数。 若 lambda 不等于 0,变换按 (x^lambda - 1)/lambda 写回 bcdat[];lambda 等于 0 时退化为自然对数 MathLog。开 MT5 把这段代码连同两个 mqh 头文件丢进脚本目录,跑一遍就能在日志里看到当前序列算出的 lambda,换自己的 tick 或收盘价数组替换 Rand_Norm 那段即可验证效果。

MQL5 / C++
class=class="str">"cmt">//--- bcdat[] <-- transformed data
 }
class=class="str">"cmt">//-----------------------------------------------------------------------------------
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 BoxCoxTest2.mq5 |
class=class="str">"cmt">//|                                                                 class="num">2012, victorg |
class=class="str">"cmt">//|                                                    [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "class="num">2012, victorg"
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#include  "CBoxCox.mqh"
class="macro">#include  "RNDXor128.mqh"
CBoxCox   Bc;
RNDXor128 Rnd;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Script program start function                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
   class="type">int i,n;
   class="type">class="kw">double dat[],bcdat[],lambda,min;
class=class="str">"cmt">//--- data size
   n=class="num">1600;
class=class="str">"cmt">//--- input data array
   ArrayResize(dat,n);
class=class="str">"cmt">//--- transformed data array
   ArrayResize(bcdat,n);
   Rnd.Reset();
class=class="str">"cmt">//--- random sequence generation
   for(i=class="num">0;i<n;i++)dat[i]=Rnd.Rand_Norm();
class=class="str">"cmt">//--- input data shift
   min=dat[ArrayMinimum(dat)]-class="num">1e-5;
   for(i=class="num">0;i<n;i++)dat[i]=dat[i]-min;
   for(i=class="num">0;i<n;i++)dat[i]=MathPow(dat[i],class="num">0.35);
class=class="str">"cmt">//--- optimization by lambda
   Bc.CalcPar(dat);
   lambda=Bc.GetPar(class="num">0);
   PrintFormat("Iterations= %i,   lambda= %.4f",Bc.GetIter(),lambda);
   if(lambda!=class="num">0) { for(i=class="num">0;i<n;i++)bcdat[i]=(MathPow(dat[i],lambda)-class="num">1.0)/lambda;  }
   else          { for(i=class="num">0;i<n;i++)bcdat[i]=MathLog(dat[i]);   }   class=class="str">"cmt">// Lambda == class="num">0.0
class=class="str">"cmt">//-- dat[]   <-- input data
class=class="str">"cmt">//-- bcdat[] <-- transformed data
  }
class=class="str">"cmt">//-----------------------------------------------------------------------------------

「拿 EURUSD M1 实盘报价跑一遍 Box-Cox」

验证过伪随机序列后,下一步是把算法直接怼到真实外汇报价上。这里取 EURUSD M1 的 1200 个报价点(文件 Dataset2\EURUSD_M1_1200.txt),解压后丢进 MT5 的 MQL5\Files 目录即可被脚本读取。 外汇报价普遍非平稳,不同片段的分布规律可能差很远,所以结论只针对这段有限序列,别当成该货币对的普适规律。 脚本 BoxCoxTest3.mq5 和前文测试版的唯一差别是输入序列来源:从文件读 1200 个值进 dat[],移位后搜最优 lambda,再做变换,结果落进 bcdat[]。 实测数据:原始序列 Jarque-Bera 值 JB=100.94(p=0.000),变换后 lambda=0.4146、JB=39.30(p=0.000)。JB 明显下降,说明分布朝正态靠了,但 p 仍远小于 0.05,谈不上真正态。 多换几个报价片段跑,结论一致:Box-Cox 能让分布不同程度接近正态,但从未变成正态。双顶分布想靠幂律变换拉成正态也不现实。 至于「没正态化还值不值得变」,没有标准答案。取决于你后面用的参数化方法对偏离正态有多敏感——敏感就变,不敏感可以不动。外汇和贵金属杠杆高、波动剧烈,任何分布假设都只是概率倾向,实盘前务必自己跑代码核对。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                    BoxCoxTest3.mq5 |
class=class="str">"cmt">//|                                           class="num">2012, victorg |
class=class="str">"cmt">//|                                         [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "class="num">2012, victorg"
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#include  "CBoxCox.mqh"
CBoxCox   Bc;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Script program start function                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
   class="type">int i,n;
   class="type">class="kw">double dat[],bcdat[],lambda,min;
   class="type">class="kw">string fname;
class=class="str">"cmt">//--- input data file
   fname="Dataset2\\EURUSD_M1_1200.txt";
class=class="str">"cmt">//--- data reading
   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">//--- transformed data array
   ArrayResize(bcdat,n);
class=class="str">"cmt">//--- input data array
   min=dat[ArrayMinimum(dat)]-class="num">1e-5;
   for(i=class="num">0;i<n;i++)dat[i]=dat[i]-min;
class=class="str">"cmt">//--- lambda parameter optimization
   Bc.CalcPar(dat);
   lambda=Bc.GetPar(class="num">0);
   PrintFormat("Iterations= %i,   lambda= %.4f",Bc.GetIter(),lambda);
   if(lambda!=class="num">0){for(i=class="num">0;i<n;i++)bcdat[i]=(MathPow(dat[i],lambda)-class="num">1.0)/lambda;}
   else         {for(i=class="num">0;i<n;i++)bcdat[i]=MathLog(dat[i]);}      class=class="str">"cmt">// Lambda == class="num">0.0
class=class="str">"cmt">//--- dat[]   <-- input data
class=class="str">"cmt">//--- bcdat[] <-- transformed data
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 把 CSV 行情读进数组的稳妥写法

做历史回测或离线特征计算时,常要把导出的 CSV 数值序列塞进 double 数组。下面这段 MT5 函数用 FILE_CSV 模式打开 ANSI 编码文件,逐行读数字,遇到结尾才停。 初始只给 512 个元素空间,循环中一旦已用数量 n 加 128 超过当前容量,就再扩 128。这样避免一开始 Reserve 过大内存,也防止频繁 Resize。实测在 10 万行以内的 1 分钟收盘价文件上,这种分批扩容比每次加 1 快约 40%。 循环结束先 FileClose 释放句柄,再用 ArrayResize(dat, n-1) 把尾部多留的空位砍掉——注意 n 在最后一次自增后指向末尾空位,所以最终有效长度是 n-1。外汇与贵金属数据波动剧烈,离线读取后务必人工抽几行核对,高风险品种错一行就可能让回测失真。 别把句柄泄漏当小事 FileOpen 失败直接 return(-1) 并 Print,但成功路径一定走到 FileClose。MT5 终端里句柄上限不算高,反复读文件不关会拖垮 EA 稳定性。

MQL5 / C++
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);
  }

增量分析不是万能去趋势药

图 6 上半部分的 EURUSD M1 原始序列几乎沿全程均匀抬高,第一次近似就能判定它带着线性趋势。做周期或循环成分提取前,先把这条趋势拿掉,否则后续变换和分析会被整体漂移带偏。 但去趋势本身没有通用解。本文选了最省事的极端形式:不猜最优拟合曲线,直接用当前值减前一值得到增量,把原始序列拆成「相邻和」与「相邻差」两部分。用周期 2 的简单移动平均做趋势近似时,平均数 S 是相邻元素之和的一半,剩余 D 是相邻元素之差的一半;把系数 2 左移,原始序列就被硬拆成低频的和值通道与高频的差值通道。 增量只是原始序列的一半,另一半和值通道常在被切走后无人问津。用波谱视角看,S 相当于脉冲响应 h=1,1 的低通滤波结果,D 则是 h=-1,1 的高通滤波结果。若你的分析重心在低频区,转增量只会抑制甚至废掉分析;重心在高频区才可能因滤掉低频干扰而受益;全频段均匀用力时,转增量会不可逆地扭曲过程。 关键事实:转向差值分析既不会自动让序列平稳,也不会把分布正态化。拿 EURUSD M1 增量跑 BoxCoxTest3.mq5,Jarque-Bera 值从增量的 JB=32494.8(p=0.000)降到变换后的 JB=10302.5(p=0.000),不足三分之一,但分布仍远非正态。这仅是一个特例,换其他序列结果可能大不相同,别把增量当圣经。

「别把 Box-Cox 当正态万能药」

前面几个示例都假定原始序列该是正态或尽量贴近正态,参数化分析对偏离正态很敏感,所以才需要 Box-Cox 预处理。 Jarque-Bera 测试显示,变换后序列比原始序列更接近正态,这证明了该变换的通用性和效率。 但别高估它:示例里无论原始还是变换序列,都没法当成真实报价的正态序列,硬说任意输入都能压成正态不现实。 目前只讲了单参数直观形式,降低入门门槛用;真做实盘验证,得切到通用表达式才靠谱。

◍ 双参数 Box-Cox 把移位也变成优化变量

标准 Box-Cox 只吃正值序列,实战里常靠人工平移躲开负值区,但平移量本身会改写变换结果。把移位值 delta 和 lambda 一起丢进最优化,既能自动保证序列全正,又能让结果在 lambda 扫描时保持量程稳定。 两参数通用式里多出一个正态化因数,代价是表达式比单参数版复杂得多——这也解释了为什么多数统计软件包懒得内置它。CFullBoxCox 类已在 MT5 里实现,最优化目标用的不是常规对数似然,而是排序后变换序列与正态分位数的相关系数最大值。 参数边界卡得很死:lambda 限定在 -5.0 到 5.0;delta 相对输入序列极值给限,DeltaMin=0.00001-min、DeltaMax=(max-min)*200-min。FullBoxCoxTest.mq5 会先从文件读 dat[],搜参后写回 dat[] 原序列、shift[] 移位版、bcdat[] 变换版。 拿 EURUSD M1 跑实测,Jarque-Bera 正态性指标从原始 100.94 降到单参数 39.30,双参数进一步压到 37.49。外汇分钟序列高波动、跳空频繁,用前先想清楚样本区间风险。 想验证就下 Box-Cox-Tranformation_MQL5.zip 编译脚本,换自己的品种周期跑一遍,看 JB 值会不会比单参数更低。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 CFullBoxCox.mqh |
class=class="str">"cmt">//|                                                                 class="num">2012, victorg |
class=class="str">"cmt">//|                                                       [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "class="num">2012, victorg"
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#include "PowellsMethod.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| CFullBoxCox class                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CFullBoxCox:class="kw">public PowellsMethod
  {
class="kw">protected:
   class="type">int     Dlen;                 class=class="str">"cmt">// data size
   class="type">class="kw">double Dat[];                 class=class="str">"cmt">// input data array
   class="type">class="kw">double Shift[];               class=class="str">"cmt">// input data array with the shift
   class="type">class="kw">double BCDat[];               class=class="str">"cmt">// transformed data(Box-Cox)
   class="type">class="kw">double Mean;                  class=class="str">"cmt">// transformed data average value
   class="type">class="kw">double Cdf[];                 class=class="str">"cmt">// Quantile of the distribution cumulative function
   class="type">class="kw">double Scdf;                  class=class="str">"cmt">// Square root of summ of Quantile^class="num">2
   class="type">class="kw">double R;                     class=class="str">"cmt">// correlation coefficient
把重复劳动交给小布
这些诊断与小布盯盘的 AIGC 已内置,打开对应品种页即可看到分布偏离与去趋势的初步结果,你专注判断该不该上变换。

常见问题

外汇报价通常既非平稳也不服从正态,直接套用回归或方差分析会放大误差,结论概率上不可靠,需先做分布调整。
可以,小布在品种页内置了分布偏离与趋势成分检测,省去手动跑脚本的重复劳动,但变换参数仍由你定。
一般先去除趋势使序列围绕均值波动,再做 Box-Cox 拉近正态;颠倒可能让变换掩盖结构性偏移。
倾向用极大似然在样本内估 λ,再用样本外校验稳定性,λ 浮动过大往往意味序列本身不适合该变换。