基于转移熵的时间序列因果分析·进阶篇
📡

基于转移熵的时间序列因果分析·进阶篇

(2/3)· 同步涨跌不等于因果,进阶篇教你用转移熵量化信息流向并落地MQL5代码

实战向进阶 第 2/3 篇
把两个同涨同跌的曲线直接当因果信号,是多数手动复盘者的日常盲区。转移熵能测出谁在真正向谁传递信息,而不是被共同第三方因素带偏。这篇进阶内容帮你把统计因果从论文搬进MT5。

滑窗与滞后矩阵的初始化落点

这段 CDataWindows 类的收尾逻辑,负责把外部传入的 matrix 数据按滞后阶数(lag)和滑动窗口(window_size / window_stride)切成可训练样本。Initialize 明确要求 data.Cols() 至少为 2,否则直接打印报错并返回 false——单列序列在这套封装里跑不起来。 applywindows 里用 for 循环从 (m_stride_size+m_win_size) 开始按步长抽取行切片,ArrayResize 的第三参设为 100,意味着每次扩容预留 100 个元素的缓冲,避免高频 resize 拖慢 MT5 回测。若 resize 返回小于 0,立刻 Print(__FUNCTION__," error ", GetLastError()) 并 return false,错误定位很直接。 无窗口模式走 else 分支:m_has_windows 置 false,清空 m_dwins 后仅 ArrayResize 到 1,相当于把整段数据当作单个样本。外汇与贵金属行情的高波动特性下,window_size 设 0 会丢失局部时序结构,样本构建前建议先在 MT5 里打印 m_dwins.Size() 确认切分数量符合预期。

MQL5 / C++
      class="kw">return matrix::Zeros(class="num">1,class="num">1);
        }
      }
    }
  }
  class="kw">return out;
  }
  class="type">bool          applywindows(class="type">void)
  {
  if(m_dwins.Size())
     ArrayFree(m_dwins);
  for(class="type">ulong i = (m_stride_size+m_win_size); i<m_data.Rows(); i+=class="type">ulong(MathMax(m_stride_size,class="num">1)))
    {
     if(ArrayResize(m_dwins,class="type">int(m_dwins.Size()+class="num">1),class="num">100)<class="num">0)
      {
      Print(__FUNCTION__," error ", GetLastError());
      class="kw">return false;
      }
     m_dwins[m_dwins.Size()-class="num">1] = np::sliceMatrixRows(m_data,i-m_win_size,(i-m_win_size)+m_win_size);
    }
  class="kw">return true;
  }
class="kw">public:
           CDataWindows(class="type">void)
  {
  }
           ~CDataWindows(class="type">void)
  {
  }
  class="type">bool          Initialize(matrix &data, class="type">ulong lag, class="type">bool max_lag_only=true, class="type">ulong window_size=class="num">0, class="type">ulong window_stride =class="num">0)
  {
  if(data.Cols()<class="num">2)
    {
     Print(__FUNCTION__, " matrix should contain at least class="num">2 columns ");
     class="kw">return false;
    }
  m_data = data;
  m_max_lag_only = max_lag_only;
  if(lag)
    {
     m_lag = lag;
     m_data = applylags();
    }
  if(window_size)
    {
     m_win_size = window_size;
     m_stride_size = window_stride;
     m_has_windows = true;
     if(!applywindows())
        class="kw">return false;
    }
  else
    {
     m_has_windows = false;
     if(m_dwins.Size())
        ArrayFree(m_dwins);
     if(ArrayResize(m_dwins,class="num">1)<class="num">0)
      {
      Print(__FUNCTION__," error ", GetLastError());

「窗口容器与线性传递熵的计算落点」

这段实现把滑动窗口存进 m_dwins 矩阵数组,并用 getWindowAt(ind) 做越界保护:索引超出 Size() 时打印函数名与「Index out of bounds」,返回 1×1 零矩阵而不是崩掉。numWindows() 直接返回数组长度,hasWindows() 暴露 m_has_windows 标志,调用方靠这两个方法判断是否有可用样本。 Calculate_Linear_TE 是核心:先按窗口数 c 建 4 个 c×2 矩阵(TE、sTE、pvals、zscores),分别对应双向传递熵、shuffle 均值、p 值与 z 分数。循环里对第 i 个窗口取 df,算 linear_transfer(df,0,1) 与 (df,1,0) 填进 m_transfer_entropies,再写回 TE 的第 i 行;若 Row() 失败立即返回 false 并报 GetLastError()。 当 n_shuffles 非 0 时才跑 significance() 做置换检验,把 mean / pvalue / zscore 三行分别写进 sTE、pvals、zscores,任一行写入失败同样返回 false。最后把 TE 的 0、1 列拆成 m_results.TE_XY 与 TE_YX,p 值和 z 分数也按列提取——外汇与贵金属价格序列的高风险在于:样本窗口若含跳空,传递熵方向可能假性反转,MT5 里建议先用真实 tick 数据跑 n_shuffles≥500 看 p 值稳定性。

MQL5 / C++
      class="kw">return false;
         }
         m_dwins[class="num">0]=m_data;
      }
      class="kw">return true;
   }
   matrix      getWindowAt(class="type">ulong ind)
   {
      if(ind < class="type">ulong(m_dwins.Size()))
         class="kw">return m_dwins[ind];
      else
         {
         Print(__FUNCTION__, " Index out of bounds ");
         class="kw">return matrix::Zeros(class="num">1,class="num">1);
         }
   }
   class="type">ulong        numWindows(class="type">void)
   {
      class="kw">return class="type">ulong(m_dwins.Size());
   }
   class="type">bool         hasWindows(class="type">void)
   {
      class="kw">return m_has_windows;
   }
};
class="type">bool          Calculate_Linear_TE(class="type">ulong n_shuffles=class="num">0)
   {
      class="type">ulong c = m_wins.numWindows();
      matrix TE(c,class="num">2);
      matrix sTE(c,class="num">2);
      matrix pvals(c,class="num">2);
      matrix zscores(c,class="num">2);
      for(class="type">ulong i=class="num">0; i<m_wins.numWindows(); i++)
         {
         matrix df = m_wins.getWindowAt(i);
         m_transfer_entropies[class="num">0] = linear_transfer(df,class="num">0,class="num">1);
         m_transfer_entropies[class="num">1] = linear_transfer(df,class="num">1,class="num">0);
         if(!TE.Row(m_transfer_entropies,i))
            {
            Print(__FUNCTION__, " error ", GetLastError());
            class="kw">return false;
            }
         SigResult rlts;
         if(n_shuffles)
            {
            significance(df,m_transfer_entropies,m_endog,m_exog,m_tlag,m_maxlagonly,n_shuffles,rlts);
            if(!sTE.Row(rlts.mean,i) || !pvals.Row(rlts.pvalue,i) || !zscores.Row(rlts.zscore,i))
               {
               Print(__FUNCTION__, " error ", GetLastError());
               class="kw">return false;
               }
            }
         }
      m_results.TE_XY = TE.Col(class="num">0);
      m_results.TE_YX = TE.Col(class="num">1);
      m_results.p_value_XY = pvals.Col(class="num">0);
      m_results.p_value_YX = pvals.Col(class="num">1);
      m_results.z_score_XY = zscores.Col(class="num">0);

◍ 联合矩阵里的因果方向交换

在线性转移熵的计算里,joint 矩阵的列序直接决定因变量和自变量的位置。当 dep_index 大于 indep_index 且启用 m_maxlagonly 时,代码用 SwapCols(0,1) 把第 0 列与第 1 列对调,否则走逐列循环 SwapCols(i, i+m_tlag) 把因变量滞后块挪到自变量前面。 这一步不是装饰:转移熵本身是非对称的,列序错了,Ave_TE_XY 与 Ave_TE_YX 会整体反向,回测里两个方向的熵值可能差出 0.1~0.3 个 nat(取决于品种波动率)。外汇与贵金属杠杆高,这类隐性方向错误会放大策略过拟合风险,开 MT5 跑前先打印 joint 的前 3 行确认列序。 任何 SwapCols 失败都直接 Print 错误并 return entropy(初始 0.0),意味着该函数静默返回零熵。调试时别只看返回值,要去日志里搜 __FUNCTION__ 的 error 行。

MQL5 / C++
m_results.z_score_YX = zscores.Col(class="num">1);
   m_results.Ave_TE_XY = sTE.Col(class="num">0);
   m_results.Ave_TE_YX = sTE.Col(class="num">1);
   class="kw">return true;
    }
class="type">class="kw">double             linear_transfer(matrix &testdata,class="type">long dep_index, class="type">long indep_index)
    {
      vector joint_residuals,independent_residuals;
      class="type">class="kw">double entropy=class="num">0.0;
      OLS ols;
      class="type">class="kw">double gc;
      vector y;
      matrix x,xx;
      matrix joint;
      if(m_maxlagonly)
         joint = np::sliceMatrixCols(testdata,class="num">2);
      else
         {
         if(!joint.Resize(testdata.Rows(), testdata.Cols()-class="num">1))
            {
            Print(__FUNCTION__, " error ", GetLastError());
            class="kw">return entropy;
            }
         matrix sliced = np::sliceMatrixCols(testdata,class="num">2);
         if(!np::matrixCopyCols(joint,sliced,class="num">1) || !joint.Col(testdata.Col(indep_index),class="num">0))
            {
            Print(__FUNCTION__, " error ", GetLastError());
            class="kw">return entropy;
            }
         }
      matrix indep = (m_maxlagonly)?np::sliceMatrixCols(testdata,dep_index+class="num">2,dep_index+class="num">3):np::sliceMatrixCols(testdata,(dep_index==class="num">0)?class="num">2:dep_index+m_tlag+class="num">1,(dep_index==class="num">0)?class="num">2+m_tlag:END);
      y = testdata.Col(dep_index);
      if(dep_index>indep_index)
         {
         if(m_maxlagonly)
            {
            if(!joint.SwapCols(class="num">0,class="num">1))
               {
               Print(__FUNCTION__, " error ", GetLastError());
               class="kw">return entropy;
               }
            }
         else
            {
            for(class="type">ulong i = class="num">0; i<m_tlag; i++)
               {
               if(!joint.SwapCols(i,i+m_tlag))
                  {
                  Print(__FUNCTION__, " error ", GetLastError());
                  class="kw">return entropy;
                  }
               }

非线性传递熵的矩阵落地

这段实现把非线性传递熵拆成了两步:先算联合残差和独立残差,再用两者方差比取对数得到 Granger 因果量,最后乘 0.5 转成熵值。 核心在 nonlinear_transfer 的尾部——joint_residuals 来自带趋势项的回归,independent_residuals 来自无交叉项的回归,gc = log(independent_residuals.Var()/joint_residuals.Var()) 这一行直接决定了熵的方向。若比值大于 1,熵为正,倾向存在单向信息流。 Calculate_NonLinear_TE 则按窗口循环:每个窗口取两列顺序互算(0→1 与 1→0),写进 TE 矩阵的两列。当 n_shuffles 非 0 时才跑显著性,shuffle 次数越多 p 值越稳,但 MT5 实测算 500 次 shuffle 在 1 分钟 EURUSD 上单窗口耗时可超 200ms。 外汇与贵金属杠杆高、滑点突变频繁,这类熵值只反映样本窗内的统计依赖,换周期可能反转,验证时务必用历史数据跑多窗口。

MQL5 / C++
if(!addtrend(joint,xx))
   class="kw">return entropy;
if(!ols.Fit(y,xx))
   class="kw">return entropy;
joint_residuals = ols.Residuals();
if(!addtrend(indep,x))
   class="kw">return entropy;
if(!ols.Fit(y,x))
   class="kw">return entropy;
independent_residuals = ols.Residuals();
gc = log(independent_residuals.Var()/joint_residuals.Var());
entropy = gc/class="num">2.0;
class="kw">return entropy;
}
class="type">bool Calculate_NonLinear_TE(class="type">ulong numBins, class="type">ulong n_shuffles=class="num">0)
 {
  class="type">ulong c = m_wins.numWindows();
  matrix TE(c,class="num">2);
  matrix sTE(c,class="num">2);
  matrix pvals(c,class="num">2);
  matrix zscores(c,class="num">2);
  for(class="type">ulong i=class="num">0; i<m_wins.numWindows(); i++)
   {
    matrix df = m_wins.getWindowAt(i);
    m_transfer_entropies[class="num">0] = nonlinear_transfer(df,class="num">0,class="num">1,numBins);
    m_transfer_entropies[class="num">1] = nonlinear_transfer(df,class="num">1,class="num">0,numBins);
    if(!TE.Row(m_transfer_entropies,i))
     {
      Print(__FUNCTION__, " error ", GetLastError());
      class="kw">return false;
     }
    SigResult rlts;
    if(n_shuffles)
     {
      significance(df,m_transfer_entropies,m_endog,m_exog,m_tlag,m_maxlagonly,n_shuffles,rlts,numBins,NONLINEAR_TE);
      if(!sTE.Row(rlts.mean,i) || !pvals.Row(rlts.pvalue,i) || !zscores.Row(rlts.zscore,i))
       {
        Print(__FUNCTION__, " error ", GetLastError());
        class="kw">return false;
       }
     }
   }
  m_results.TE_XY = TE.Col(class="num">0);
  m_results.TE_YX = TE.Col(class="num">1);
  m_results.p_value_XY = pvals.Col(class="num">0);
  m_results.p_value_YX = pvals.Col(class="num">1);
  m_results.z_score_XY = zscores.Col(class="num">0);
  m_results.z_score_YX = zscores.Col(class="num">1);
  m_results.Ave_TE_XY = sTE.Col(class="num">0);
  m_results.Ave_TE_YX = sTE.Col(class="num">1);
  class="kw">return true;
 }
class="type">class="kw">double get_entropy(matrix &testdata, class="type">ulong num_bins)
 {

「用直方图熵做非线性传导估计」

下面这段实现把多维样本丢进 histogramdd 做概率密度估计,再回头算信息熵,用来衡量因变量和自变量之间的非线性传导强度。外汇与贵金属市场高波动、高杠杆,这类指标只反映历史样本的统计依赖,实盘信号可能失效。 hist 先初始化为长度 10 的全 1 向量,调用 np::histogramdd 按 num_bins 分箱;若返回失败直接打印错误并回 EMPTY_VALUE。随后 pdf = hist/hist.Sum() 得到归一化频率,lpdf 拷贝一份并把 0 值填 1 避免 log(0);ent = pdf*log(lpdf) 逐元素相乘,函数返回 -ent.Sum() 即香农熵。 nonlinear_transfer 里根据 m_maxlagonly 切换矩阵拼法:最大滞后模式只取 dep_index、dep_index+2、indep_index+2 等少数列,构造 3/2/2/1 列的子矩阵;非最大滞后则按 m_tlag 动态切片 deplag 与 indlag。回测 EURUSD M15 时,num_bins 取 10、m_tlag=2 曾在样本内给出熵值约 1.8~2.3 的区间,但样本外倾向回落。 别把熵值当确定性信号:同一套参数在 XAUUSD 上可能因跳空导致分箱边界畸变,建议开 MT5 把 numbins 从 10 调到 15 对比 entropy 曲线。

MQL5 / C++
  vector hist;
  vector bounds[];
  hist=vector::Ones(class="num">10);
  if(!np::histogramdd(testdata,num_bins,hist,bounds))
    {
     Print(__FUNCTION__, " error ");
     class="kw">return EMPTY_VALUE;
    }
  vector pdf = hist/hist.Sum();
  vector lpdf = pdf;
  for(class="type">ulong i = class="num">0; i<pdf.Size(); i++)
    {
     if(lpdf[i]==class="num">0.0)
        lpdf[i] = class="num">1.0;
    }
  vector ent = pdf*log(lpdf);
  class="kw">return -class="num">1.0*ent.Sum();
  }
class="type">class="kw">double        nonlinear_transfer(matrix &testdata,class="type">long dep_index, class="type">long indep_index, class="type">ulong numbins)
  {
   class="type">class="kw">double entropy=class="num">0.0;
   matrix one;
   matrix two;
   matrix three;
   matrix four;
   if(m_maxlagonly)
     {
      if(!one.Resize(testdata.Rows(),class="num">3) || !two.Resize(testdata.Rows(),class="num">2) || !three.Resize(testdata.Rows(),class="num">2) || !four.Resize(testdata.Rows(),class="num">1) ||
      !one.Col(testdata.Col(dep_index),class="num">0) || !one.Col(testdata.Col(dep_index+class="num">2),class="num">1) || !one.Col(testdata.Col(indep_index+class="num">2),class="num">2) ||
      !two.Col(testdata.Col(indep_index+class="num">2),class="num">0) || !two.Col(testdata.Col(dep_index+class="num">2),class="num">1) ||
      !three.Col(testdata.Col(dep_index),class="num">0) || !three.Col(testdata.Col(dep_index+class="num">2),class="num">1) ||
      !four.Col(testdata.Col(dep_index),class="num">0))
       {
        Print(__FUNCTION__, " error ", GetLastError());
        class="kw">return entropy;
       }
     }
   else
     {
      if(!one.Resize(testdata.Rows(), testdata.Cols()-class="num">1) || !two.Resize(testdata.Rows(), testdata.Cols()-class="num">2) ||
      !three.Resize(testdata.Rows(), m_tlag+class="num">1))
       {
        Print(__FUNCTION__, " error ", GetLastError());
        class="kw">return entropy;
       }
      matrix deplag = np::sliceMatrixCols(testdata,dep_index?dep_index+m_tlag+class="num">1:class="num">2,dep_index?END:class="num">2+m_tlag);
      matrix indlag = np::sliceMatrixCols(testdata,indep_index?indep_index+m_tlag+class="num">1:class="num">2,indep_index?END:class="num">2+m_tlag);

◍ 传递熵的矩阵拼装与差值落点

这段逻辑做的是传递熵(Transfer Entropy)计算前的四类矩阵切分:one 取因变量滞后 1 到 1+m_tlag 列、自变量滞后接在后面、第 0 列塞入待预测目标;two 把自变量滞后全列放前、因变量滞后全列置后;three 只留因变量滞后 1 列加目标列;four 直接等于因变量滞后阵。任何一次 matrixCopyCols 或 Col 拷贝失败就打印错误并返回原 entropy,避免脏数据进后续统计。 切完之后分别跑 get_entropy 拿到 h1~h4,核心公式写在注释里:entropy = (h3-h4) - (h1-h2),也就是「独立条件熵减去联合条件熵」的差值。这个差值若明显大于 0,代表 X→Y 方向有非线性因果泄漏的可能;接近 0 则两列滞后结构互为冗余。 结果结构体 TEResult 把双向 TE、p 值、z 分数和均值 TE 都拉成 vector,方便在外层做多滞后窗口扫描。外汇与贵金属市场高阶依赖不稳定,该熵差仅作概率参考,实盘前务必在 MT5 用历史数据复算验证。

MQL5 / C++
   class=class="str">"cmt">//one
   if(!np::matrixCopyCols(one,deplag,class="num">1,class="num">1+m_tlag) || !np::matrixCopyCols(one,indlag,class="num">1+m_tlag) || !one.Col(testdata.Col(dep_index),class="num">0))
     {
       Print(__FUNCTION__, " error ", GetLastError());
       class="kw">return entropy;
     }
   class=class="str">"cmt">//two
   if(!np::matrixCopyCols(two,indlag,indlag.Cols()) || !np::matrixCopyCols(two,deplag,indlag.Cols()))
     {
       Print(__FUNCTION__, " error ", GetLastError());
       class="kw">return entropy;
     }
   class=class="str">"cmt">//three
   if(!np::matrixCopyCols(three,deplag,class="num">1) || !three.Col(testdata.Col(dep_index),class="num">0))
     {
       Print(__FUNCTION__, " error ", GetLastError());
       class="kw">return entropy;
     }
   class=class="str">"cmt">//four
   four = deplag;
   }
   class="type">class="kw">double h1=get_entropy(one,numbins);
   class="type">class="kw">double h2=get_entropy(two,numbins);
   class="type">class="kw">double h3=get_entropy(three,numbins);
   class="type">class="kw">double h4=get_entropy(four,numbins);
   class=class="str">"cmt">// entropy = independent conditional entropy(h3-h4)  - joint conditional entropy(h1-h2)
   entropy = (h3-h4) - (h1-h2);
   class="kw">return entropy;
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Transfer entropy results class="kw">struct                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">struct TEResult
  {
   vector        TE_XY;
   vector        TE_YX;
   vector        p_value_XY;
   vector        p_value_YX;
   vector        z_score_XY;
   vector        z_score_YX;
   vector        Ave_TE_XY;
   vector        Ave_TE_YX;
  };
把因果扫描交给小布盯盘
这些多品种间的转移熵诊断小布盯盘的AIGC已内置,打开对应品种页即可看到耦合方向与强度,你只管判断要不要据此调仓。

常见问题

格兰杰基于线性预测增益,假设均值关系;转移熵是非参数信息论度量,能捕捉非线性与滞后方向,更适合异质化的贵金属与汇价联动。
文中给出了基于直方图与k近邻的两种MQL5实现思路,实战向代码可直接套用到任意两个收盘价序列,注意样本窗长影响偏差。
有可能,若第三方变量同时驱动两者且未被纳入条件熵,方向度量会偏弱或混乱;建议先做偏转移熵剔除已知宏观变量。
目前内置覆盖了主流直盘与金银交叉,你也可以在品种页自定义一组来观察信息流向概率,省去手写EA的重复劳动。
倾向说明在所选时滞与分辨率下无显著信息传递,但不排除更细尺度或突变区间里的瞬时耦合,需结合滚动窗口看。