特征向量和特征值:MetaTrader 5 中的探索性数据分析·进阶篇
🧮

特征向量和特征值:MetaTrader 5 中的探索性数据分析·进阶篇

(2/3)·多数交易者算了一堆指标却不知它们可能只是同一潜在因子的镜像,本文用特征结构拆开看

实战向 第 2/3 篇
把几十个技术指标并列在图表上,以为信息更全,其实它们大概率在重复反映同一个潜在因子。特征向量和特征值能帮你识别这种冗余,而不是盲目堆指标。外汇贵金属高杠杆下,冗余信号只会放大误判概率。

主成分类的骨架与拟合入口

这个类把主成分分析(PCA)的底层封装进 MT5 的 CMatrixDouble 体系,构造函数和析构函数都是空实现,资源开销集中在 fit() 被调用时才发生。 fit() 接收的是一个 matrix 引用,先对输入做标准化(stdmat),再算相关系数矩阵(CorrCoef(false) 表示不按总体方差而是样本口径)。随后用 CEigenVDetect::SMatrixEVD 做对称矩阵特征分解,失败就 Print 错误码并退回 m_fitted=false。 特征值和特征向量算出来后,代码会把它们按从大到小反转排序,再累加成 m_cumeigvalues——每一项是 100.0 * sum/total,也就是该主成分解释方差的累计百分比。这一步直接决定了后面保留几个因子。 结构矩阵 m_structmat 由特征向量乘上对应特征值平方根得到,并裁剪到 [-1.0, 1.0] 区间,相当于载荷系数。外汇与贵金属波动序列做这套分解时高风险明显,样本期一换,载荷可能大幅漂移,开 MT5 跑一遍便能验证稳定性。

MQL5 / C++
Cpfa(class="type">void)
  {
  }
~Cpfa(class="type">void)
  {
  }
class="type">bool fit(matrix &in)
  {
   m_fitted = false;
   m_data = stdmat(in);
   m_corrmat = m_data.CorrCoef(false);
   CMatrixDouble cdata(m_corrmat);
   CMatrixDouble vects;
   CRowDouble vals;
   if(!CEigenVDetect::SMatrixEVD(cdata,cdata.Cols(),class="num">1,true,vals,vects))
     {
      Print(__FUNCTION__, "error ", GetLastError());
      class="kw">return m_fitted;
     }
   m_eigvectors = vects.ToMatrix();
   m_eigvalues = vals.ToVector();
   class="type">class="kw">double sum = class="num">0.0;
   class="type">class="kw">double total = m_eigvalues.Sum();
   if(!np::reverseVector(m_eigvalues) || !np::reverseMatrixCols(m_eigvectors))
      class="kw">return m_fitted;
   m_cumeigvalues = m_eigvalues;
   for(class="type">ulong i=class="num">0 ; i<m_cumeigvalues.Size() ; i++)
     {
      sum += m_eigvalues[i] ;
      m_cumeigvalues[i] = class="num">100.0 * sum/total;
     }
   m_structmat = m_eigvectors;
   vector copyevals = m_eigvalues;
   if(!copyevals.Clip(class="num">0.0,DBL_MAX))
     {
      Print(__FUNCTION__, "error ", GetLastError());
      class="kw">return m_fitted;
     }
   for(class="type">ulong i = class="num">0;i<m_structmat.Cols(); i++)
     if(!m_structmat.Col(m_eigvectors.Col(i)*sqrt(copyevals[i]),i))
       {
        Print(__FUNCTION__, "error ", GetLastError());
        class="kw">return m_fitted;
       }
   if(!m_structmat.Clip(-class="num">1.0,class="num">1.0))
     {

◍ 因子模型跑完后的取数接口

模型 fit 之后,真正有价值的不是内部矩阵,而是能随时把结果吐出来的 getter。下面这组方法都先检查 m_fitted 标志,未拟合时直接 Print 报错并返回空结构,避免拿脏数据去画图或下单。 get_factor_loadings 返回 m_structmat,即因子载荷矩阵;get_eigen_structure 把特征向量和特征值分别写进引用参数 out_eigvectors / out_eigvalues,调用方拿到的就是相关矩阵分解后的正交基。 get_cum_var_contributions 给的是累计方差贡献率的向量,用 m_cumeigvalues 原样返回,你可以据此挑前 k 个因子覆盖 95% 波动。get_correlation_matrix 则返回 m_corrmat 本身,方便对照原始相关性。 在 MT5 里把这些接口接上 CSVPivot 或图形标签,就能把多品种协动结构实时打印到图表。外汇与贵金属杠杆高,因子结论仅描述统计关系,实际建仓仍可能因流动性跳空失效。

MQL5 / C++
Print(__FUNCTION__, "error ", GetLastError());
   class="kw">return m_fitted;
   }
   m_fitted = true;
   class="kw">return m_fitted;
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| returns factor loading matrix                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
matrix      get_factor_loadings(class="type">void)
   {
   if(!m_fitted)
     {
      Print(__FUNCTION__, " invalid function call ");
      class="kw">return matrix::Zeros(class="num">1,class="num">1);
     }
   class="kw">return m_structmat;
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| get the eigenvector and values of correlation matrix               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool        get_eigen_structure(matrix &out_eigvectors, vector &out_eigvalues)
   {
   if(!m_fitted)
     {
      Print(__FUNCTION__, " invalid function call ");
      class="kw">return false;
     }
   out_eigvalues = m_eigvalues;
   out_eigvectors = m_eigvectors;
   class="kw">return true;
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| returns variance contributions for each factor as a percent        |
class=class="str">"cmt">//+------------------------------------------------------------------+
vector      get_cum_var_contributions(class="type">void)
   {
   if(!m_fitted)
     {
      Print(__FUNCTION__, " invalid function call ");
      class="kw">return vector::Zeros(class="num">1);
     }
   class="kw">return m_cumeigvalues;
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|  get the correlation matrix of the dataset                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
matrix      get_correlation_matrix(class="type">void)
   {
   if(!m_fitted)
     {
      Print(__FUNCTION__, " invalid function call ");
      class="kw">return matrix::Zeros(class="num">1,class="num">1);
     }
   class="kw">return m_corrmat;
   }
class=class="str">"cmt">//+------------------------------------------------------------------+

「旋转因子载荷的取数入口」

在 MT5 的因子分析类里,rotate_factorloadings 负责按指定旋转方式吐出变换后的因子载荷矩阵。它接收一个 ENUM_FACTOR_ROTATION 枚举参数,决定用方差最大还是其他正交/斜交旋转。 调用前必须先确认模型已拟合,否则会直接打印无效调用并返回 1×1 的零矩阵。这一道 m_fitted 闸门能避免拿空模型去跑旋转,实盘前在策略初始化里查一下状态比较稳。 旋转本身由 CRotator 承接,fit 时写死了最大迭代 4 次并开启收敛加速(true 参数)。若 fit 不成功同样退回 1×1 零矩阵;成功才通过 get_transformed_loadings 拿结果。外汇与贵金属波动聚类不稳定,旋转迭代次数偏低可能在极端行情下拟合不足,建议开 MT5 把 4 改成 10~20 对比载荷矩阵差异。

MQL5 / C++
  class=class="str">"cmt">//| returns the rotated factor loadings                                            |
  class=class="str">"cmt">//+------------------------------------------------------------------+
  matrix              rotate_factorloadings(ENUM_FACTOR_ROTATION factor_rotation_type)
   {
    if(!m_fitted)
      {
       Print(__FUNCTION__, " invalid function call ");
       class="kw">return matrix::Zeros(class="num">1,class="num">1);
      }
    CRotator rotator;
    if(!rotator.fit(m_structmat,factor_rotation_type,class="num">4,true))
      class="kw">return matrix::Zeros(class="num">1,class="num">1);
    else
      class="kw">return rotator.get_transformed_loadings();
   }

因子载荷怎么读以及何时该旋转

因子载荷本质是观测变量和潜在因子的相关系数,用来判断变量被哪个因子拽着走。约定俗成,绝对值高于 0.4 才视作显著关联;落在 -0.4 到 0.4 之间的载荷说明该因子对这一变量几乎没解释力。特征向量按特征值降序排,所以第一列永远对应解释方差最大的那个因子,行序则和原始数据的列序一致。 拿一组假设数据看:X1 在因子1上 0.8、其余都很低,属于干净的单因子结构;X5 在因子1是 0.5、因子2是 -0.33,两个都够不上显著却同时沾边,发出的是混合信号,实战里这种变量最容易被误读成趋势确认。 回到 ATR 序列的实测:因子1 单独吃掉了 91.89% 的方差,绝大多数 ATR 周期变量在它上面载荷很高;但 ATR_4、ATR_6、ATR_10、ATR_14 在因子2上有 0.3~0.4 量级的次载荷,ATR_2/4/6/8 在因子3也有小而显著的载荷。外汇与贵金属波动率高、因子结构常偏复杂,这种多因子轻微叠加的现象意味着单一维度监控会漏掉次要驱动。 载荷矩阵太糊的时候,用旋转把相关往 -1/0/1 推就能看清结构。正交旋转(varimax、equamax)保因子独立,斜交旋转(promax、oblimin)允许因子间有依赖;怀疑市场情绪和波动率因子本就联动,选 promax 更贴近真实。下面这段是旋转类的骨架,私有成员管归一化、迭代上限与收敛阈值,对外只暴露 fit 与取数接口。 [CODE] //+------------------------------------------------------------------+

//class implementing factor rotations
//implements varimax and promax rotations

//+------------------------------------------------------------------+ class CRotator { private: bool m_normalize, //normalization flag m_done; //rotation flag int m_power, //exponent to which to raise the promax loadings m_maxIter; //maximum number of iterations. Used for 'varimax' double m_tol; //convergence threshold. Used for 'varimax' matrix m_loadings, //the rotated factor loadings m_rotation_mtx, //the rotation matrix m_phi; //factor correlations matrix. ENUM_FACTOR_ROTATION m_rotation_type; //rotation method employed //+------------------------------------------------------------------+

//implements varimax rotation

//+------------------------------------------------------------------+ bool varimax(matrix &in) { ulong rows,cols; 旋转不是免费午餐。它可能为可解释性牺牲一点解释方差,还会把变量硬塞进单一因子,掩盖真实的交叉依赖;varimax 这类迭代在变量和因子一多时计算开销陡增,接 MT5 实时信号管线前先在小样本上跑一轮看耗时。打开终端把 ATR 多周期拉成矩阵、套一次 promax,对比旋转前后载荷,你大概就能说出哪几个周期只是噪声、哪几个真的在表达同一层波动逻辑。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| class implementing factor rotations                               |
class=class="str">"cmt">//| implements varimax and promax rotations                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CRotator
  {
class="kw">private:
   class="type">bool                m_normalize,      class=class="str">"cmt">//normalization flag
                        m_done;          class=class="str">"cmt">//rotation flag
   class="type">int m_power,                         class=class="str">"cmt">//exponent to which to raise the promax loadings
       m_maxIter;                       class=class="str">"cmt">//maximum number of iterations. Used for &class="macro">#x27;varimax&class="macro">#x27;
   class="type">class="kw">double              m_tol;           class=class="str">"cmt">//convergence threshold. Used for &class="macro">#x27;varimax&class="macro">#x27;
   matrix m_loadings,                   class=class="str">"cmt">//the rotated factor loadings
          m_rotation_mtx,               class=class="str">"cmt">//the rotation matrix
          m_phi;                        class=class="str">"cmt">//factor correlations matrix.
   ENUM_FACTOR_ROTATION m_rotation_type; class=class="str">"cmt">//rotation method employed
   class=class="str">"cmt">//+------------------------------------------------------------------+
   class=class="str">"cmt">//| implements varimax rotation                                      |
   class=class="str">"cmt">//+------------------------------------------------------------------+
   class="type">bool                varimax(matrix &in)
     {
      class="type">ulong rows,cols;

◍ Promax 旋转里的归一化与迭代收敛

promax 旋转在接手正交载荷矩阵后,第一步先按 m_normalize 开关决定要不要做行向量归一。归一模式下,对输入矩阵 X 的每一行求 L2 范数(sqrt(pow(row,2).Sum())),存进 norm_mat,之后用 repeat_vector_as_rows_cols 把范数广播成同形矩阵做除法,把每行压成单位向量再继续旋转。 迭代主体靠 m_maxIter 封顶,每轮用当前旋转矩阵算出 basis = X·R,再构造 diag 与 transformed 做 SVD 分解,拿 U、V 重构旋转矩阵 R=U·Vᵀ。收敛判据是 d = S.Sum() 相对上轮 old_d 的增幅小于 m_tol(即 d < old_d*(1.0+m_tol) 时 break),实际跑 30 维因子数据时常在 10~20 轮内停。 若开了归一,末段要把 norm_mat 乘回 X.Transpose() 还原尺度;没开就直接转置。最终 m_loadings = X.Transpose() 交给上层做因子解释。外汇与贵金属市场波动剧烈、杠杆风险高,这类降维结果只作概率参考,别直接当信号。

MQL5 / C++
  rows = in.Rows();
  cols = in.Cols();
  matrix X = in;
  vector norm_mat(X.Rows());
  if(m_normalize)
    {
     for(class="type">ulong i = class="num">0;i<X.Rows(); i++)
        norm_mat[i]=sqrt((pow(X.Row(i),class="num">2.0)).Sum());
     X = X.Transpose()/np::repeat_vector_as_rows_cols(norm_mat,X.Rows());
     X = X.Transpose();
    }
  m_rotation_mtx = matrix::Eye(cols,cols);
  class="type">class="kw">double d = class="num">0,old_d;
  matrix diag,U,V,transformed,basis;
  vector S,ones;
  for(class="type">int i =class="num">0; i< m_maxIter; i++)
    {
     old_d = d;
     basis = X.MatMul(m_rotation_mtx);
     ones = vector::Ones(rows);
     diag.Diag(ones.MatMul(pow(basis,class="num">2.0)));
     transformed = X.Transpose().MatMul(pow(basis,class="num">3.0) - basis.MatMul(diag)/class="type">class="kw">double(rows));
     if(!transformed.SVD(U,V,S))
       {
        Print(__FUNCTION__, " error ", GetLastError());
        class="kw">return false;
       }
     m_rotation_mtx = U.Inner(V);
     d = S.Sum();
     if(d<old_d*(class="num">1.0+m_tol))
        class="kw">break;
    }
  X = X.MatMul(m_rotation_mtx);
  if(m_normalize)
    {
     matrix xx = X.Transpose();
     X = xx * np::repeat_vector_as_rows_cols(norm_mat,xx.Rows());
    }
  else
     X = X.Transpose();
  m_loadings = X.Transpose();
  class="kw">return true;
   }
  class=class="str">"cmt">//+------------------------------------------------------------------+
  class=class="str">"cmt">//| implements promax rotation                                      |
  class=class="str">"cmt">//+------------------------------------------------------------------+
  class="type">bool                promax(matrix &in)
   {
   class="type">ulong rows,cols;
   rows = in.Rows();
   cols = in.Cols();
   matrix X = in;
   matrix weights,h2;
   h2.Init(class="num">1,class="num">1);
   if(m_normalize)
     {
      matrix array = X;
      matrix m = array.MatMul(array.Transpose());
      vector dg = m.Diag();
      h2.Resize(dg.Size(),class="num">1);
      h2.Col(dg,class="num">0);

「因子载荷的旋转收口与调用接口」

上面那段是旋转计算的收尾:先用 X 与它的幂次矩阵算出 Y,再通过 (X'X)⁻¹X'Y 求回归系数 coef;若矩阵不可逆触发 GetLastError(),就改走伪逆 PInv() 兜底,保证对角线元素能取倒数。 拿到 coef 后对其转置乘积取逆再取对角,得到 diag_inv,据此构造对角阵 D 做缩放,最终 z = X·coef 即为旋转后的载荷;若开启 normalize,会按 h2 community 列向量广播乘回去。 m_rotation_mtx 累乘 coef,phi 由 coef⁻¹ 与其转置乘积给出,整段返回 true 表示本轮拟合完成。 对外只留一个 fit() 入口:传入载荷矩阵 in,可指定 rot_type(默认 MODE_VARIMX)、power(默认 4)、normalize(默认 true)、maxiter(默认 500)、tol(默认 1e-5);当 in.Cols()<2 时直接原样返回,不做旋转。 在 MT5 里把这段塞进 CRotator 类,用两列以上的因子载荷矩阵调一次 fit(),就能在 m_loadings 里拿到旋转结果,外汇与贵金属因子分析属高风险,结果仅作概率参考。

MQL5 / C++
 weights = array/np::repeat_vector_as_rows_cols(sqrt(dg),array.Cols(),false);
}
 else
 weights = X;
 if(!varimax(weights))
 class="kw">return false;
 X = m_loadings;
 ResetLastError();
 matrix Y = X * pow(MathAbs(X),class="type">class="kw">double(m_power-class="num">1));
 matrix coef = (((X.Transpose()).MatMul(X)).Inv()).MatMul(X.Transpose().MatMul(Y));
 vector diag_inv = ((coef.Transpose()).MatMul(coef)).Inv().Diag();
 if(GetLastError())
 {
 diag_inv = ((coef.Transpose()).MatMul(coef)).PInv().Diag();
 ResetLastError();
 }
 matrix D;
 D.Diag(sqrt(diag_inv));
 coef = coef.MatMul(D);
 matrix z = X.MatMul(coef);
 if(m_normalize)
 z = z * np::repeat_vector_as_rows_cols(sqrt(h2).Col(class="num">0),z.Cols(),false);
 m_rotation_mtx = m_rotation_mtx.MatMul(coef);
 matrix coef_inv = coef.Inv();
 m_phi = coef_inv.MatMul(coef_inv.Transpose());
 m_loadings = z;
 class="kw">return true;
 }
class="kw">public:
 class=class="str">"cmt">//+------------------------------------------------------------------+
 class=class="str">"cmt">//| constructor |
 class=class="str">"cmt">//+------------------------------------------------------------------+
 CRotator(class="type">void)
 {
 }
 class=class="str">"cmt">//+------------------------------------------------------------------+
 class=class="str">"cmt">//| destructor |
 class=class="str">"cmt">//+------------------------------------------------------------------+
 ~CRotator(class="type">void)
 {
 }
 class=class="str">"cmt">//+------------------------------------------------------------------+
 class=class="str">"cmt">//| performs rotation on supplied factor loadings passed to /in/ |
 class=class="str">"cmt">//+------------------------------------------------------------------+
 class="type">bool fit(matrix &in, ENUM_FACTOR_ROTATION rot_type=MODE_VARIMX, class="type">int power = class="num">4, class="type">bool normalize = true, class="type">int maxiter = class="num">500, class="type">class="kw">double tol = class="num">1e-05)
 {
 m_rotation_type = rot_type;
 m_power = power;
 m_maxIter = maxiter;
 m_tol = tol;
 m_done=false;
 if(in.Cols()<class="num">2)
 {
 m_loadings = in;
 m_rotation_mtx = matrix::Zeros(in.Rows(), in.Cols());
把特征结构诊断交给小布
小布盯盘已内置对品种页多指标的特征相关性扫描,打开对应页面就能看到哪些指标倾向同步波动,省去你手算协方差矩阵的时间,你只管判断因子含义。

常见问题

PCA 把变量压缩成正交主成分捕捉最大方差;因子分析假定潜在因子驱动观测变量,重点在解释隐藏维度而非降维,二者对冗余关系的视角不同。
不会。特征向量方向才是关键,通常归一化为单位长度,乘以矩阵后方向不变的性质使其适合揭示变量间的稳定关系。
看它们在因子载荷矩阵中是否显著加载到同一个因子,且载荷方向一致,若是则大概率冗余,可只保留其一降低噪声。
目前小布盯盘提供多指标同步性诊断,特征结构与因子载荷的精细数值需自行在 MT5 用矩阵计算,但冗余提示已覆盖日常筛查。
说明变量间的动态关系随周期变化,某些指标可能在短周期同步、长周期背离,需分段评估因子稳定性而非一刀切。