特征向量和特征值:MetaTrader 5 中的探索性数据分析·进阶篇
(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 跑一遍便能验证稳定性。
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 或图形标签,就能把多品种协动结构实时打印到图表。外汇与贵金属杠杆高,因子结论仅描述统计关系,实际建仓仍可能因流动性跳空失效。
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 对比载荷矩阵差异。
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,对比旋转前后载荷,你大概就能说出哪几个周期只是噪声、哪几个真的在表达同一层波动逻辑。
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() 交给上层做因子解释。外汇与贵金属市场波动剧烈、杠杆风险高,这类降维结果只作概率参考,别直接当信号。
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 里拿到旋转结果,外汇与贵金属因子分析属高风险,结果仅作概率参考。
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());