特征向量和特征值:MetaTrader 5 中的探索性数据分析·综合运用
🧮

特征向量和特征值:MetaTrader 5 中的探索性数据分析·综合运用

(3/3)·从因子分析到时间序列一致性,用特征结构给多维行情指标做一次透视扫描

实战向进阶 第 3/3 篇
不少交易者把十几个指标堆在副图,却没意识到其中大半只是同一股潜在情绪的回声。用特征结构一看,冗余和同步关系立刻现形,盲目加指标只会给决策加噪。

「因子旋转后的矩阵怎么取」

在 MT5 里做主成分降维后,真正能拿去解释行情结构的不是原始载荷,而是旋转后的载荷矩阵。上面这段把 Varimax 与 Promax 两种旋转封装进同一个 CRotator 类,调用 fit() 时靠 m_rotation_type 分支决定走 varimax() 还是 promax()。 get_transformed_loadings() 在 m_done 为 true 时返回 m_loadings,否则回吐 1×1 的零矩阵——这是个防御式写法,没跑完旋转就去取数只会拿到垃圾。get_rotation_matrix() 同理,给的是旋转变换矩阵本身。 get_phi() 只在 Promax 且 m_done 成立时才返回因子相关矩阵 m_phi;Varimax 是正交旋转,因子不相关,调它也只拿到 1×1 零矩阵。末尾实例里 rotator.fit(m_structmat, MODE_PROMAX, 4, false) 用 4 次幂做 Promax,跑完直接 Print 旋转载荷,开 MT5 把这段贴进脚本就能看到输出。外汇与贵金属波动非线性强,因子结构可能随 regime 漂移,验证时建议分周期回看。

MQL5 / C++
m_phi = matrix::Zeros(in.Rows(),in.Cols());
 m_done = true;
 class="kw">return true;
   }
   class="kw">switch(m_rotation_type)
     {
      case MODE_VARIMX:
        m_done = varimax(in);
        class="kw">break;
      case MODE_PROMAX:
        m_done = promax(in);
        class="kw">break;
      class="kw">default:
        class="kw">return m_done;
     }
    class="kw">return m_done;
   }
 class=class="str">"cmt">//+------------------------------------------------------------------+
 class=class="str">"cmt">//|  get the rotated loadings                                        |
 class=class="str">"cmt">//+------------------------------------------------------------------+
 matrix       get_transformed_loadings(class="type">void)
    {
     if(m_done)
       class="kw">return m_loadings;
     else
       class="kw">return matrix::Zeros(class="num">1,class="num">1);
    }
 class=class="str">"cmt">//+------------------------------------------------------------------+
 class=class="str">"cmt">//| get the rotation matrix                                           |
 class=class="str">"cmt">//+------------------------------------------------------------------+
 matrix       get_rotation_matrix(class="type">void)
    {
     if(m_done)
       class="kw">return m_rotation_mtx;
     else
       class="kw">return matrix::Zeros(class="num">1,class="num">1);
    }
 class=class="str">"cmt">//+------------------------------------------------------------------+
 class=class="str">"cmt">//| get the factor correlation matrix                                 |
 class=class="str">"cmt">//+------------------------------------------------------------------+
 matrix       get_phi(class="type">void)
    {
     if(m_done && m_rotation_type==MODE_PROMAX)
       class="kw">return m_phi;
     else
       class="kw">return matrix::Zeros(class="num">1,class="num">1);
    }
   };
CRotator rotator;
if(!rotator.fit(m_structmat,MODE_PROMAX,class="num">4,false))
   class="kw">return;
Print(" Rotated Loadings Matrix ", rotator.get_transformed_loadings());

◍ 用潜在因子空间向量揪出冗余指标

面对几十个技术指标,肉眼挑重复项基本靠猜。更硬的办法是把每个观测变量看成由潜在因子张成空间里的向量:向量夹角越小,说明它们携带的因子信息越像,冗余度就越高。点积取绝对值落在 0 到 1 之间,1 代表完全冗余,0 代表正交独立,这一步能把‘主观散点图’换成可计算的相似度。 实际跑的时候,先靠主成分把噪声因子(特征值小的成分)剔除,再用因子载荷矩阵的前几列做归一化。归一化后每行长度变 1,两行点积就是夹角余弦的绝对值,直接当冗余分数用。比如 MA_12 和 MA_24 在树状图底层就合并了,比它们和 ATR_18 的相似度明显更高,这种分组倾向说明同色聚类里的变量可以只留一个代表。 MT5 自带终端不画树状图,但 Alglib 的 MQL5 移植能跑凝聚式层次聚类(AHC),还支持自定义距离度量。下面这段是聚类前计算归一化载荷与绝对点积距离矩阵的核心片段,pp 传的是缩放后的载荷,pd 存的就是变量间冗余度。 调用次序是先 ClusterizerCreate 起引擎,再设点、设距离、设链接算法,最后 ClusterizerRunAHC 出报告。终止类型返回 1 才算聚类成功,之后从 m_rep.m_pm 提分组。外汇与贵金属行情受杠杆影响大,指标冗余简化只是降维手段,不代表信号胜率提升,实盘前务必在 MT5 历史数据里验证分组稳定性。

MQL5 / C++
CAHCReport        m_rep;
CClusterizerState m_cs;
CClustering::ClusterizerCreate(m_cs);
CClustering::ClusterizerSetPoints(m_cs,pp,pp.Rows(),pp.Cols(),dist<class="num">22?dist:DIST_EUCLIDEAN);
CClustering::ClusterizerSetDistances(m_cs,pd,pd.Cols(),true);
CClustering::ClusterizerSetAHCAlgo(m_cs,linkage);
CClustering::ClusterizerRunAHC(m_cs,m_rep);
for(class="type">int i=class="num">0 ; i<nvars ; i++)
  {
    length = class="num">0.0 ;
    for(class="type">int j=class="num">0 ; j<ndim ; j++)
      length += structure[i][j] * structure[i][j] ;
    length = class="num">1.0 / sqrt(length) ;
    for(class="type">int j=class="num">0 ; j<ndim ; j++)
      out[class="num">0][i][j] = length * structure[i][j] ;
  }
for(class="type">int irow1=class="num">0 ; irow1<nvars-class="num">1 ; irow1++)
  {
    for(class="type">int irow2=irow1+class="num">1 ; irow2<nvars ; irow2++)
      {
        dotprod = class="num">0.0 ;
        for(class="type">int i=class="num">0 ; i<ndim ; i++)
          dotprod += out[class="num">0][irow1][i] * out[class="num">0][irow2][i] ;
        out[class="num">1][irow1][irow2] = fabs(dotprod) ;
      }

聚类前的向量归一化与距离矩阵预计算

在 MT5 里做形态聚类,直接拿原始特征矩阵算距离会被量纲拖偏。CCluster 类的 customDist 方法先在聚类前把每个样本向量做 L2 归一化,再据此填一张 n×n 的余弦相似度矩阵,后续层次聚类只查这张表,不再反复扫原始数据。 代码里 nvars 取 structure 的行数(样本数),ndim 默认用满列数(因子数),若传入 num_factors 且不超过列数则截断到该维度。out 是一个长度≥2 的 matrix 数组:out[0] 存归一化后的向量,out[1] 存距离(此处实为 1-余弦,初始化填 0)。 归一化循环对第 i 个样本先求各维度平方和 length,再取倒数平方根做缩放系数,把 structure[i][j] 乘完写进 out[0][i][j]。之后双层循环只算上三角 irow1<irow2 的点积 dotprod,填进 out[1] 对应位置——对角线留 0,调用方应自行补 1 或对称拷贝。外汇与贵金属行情序列做这类聚类波动敏感,样本窗口切分不当可能让簇结构失真,实盘前建议在策略测试器用不同 num_factors 跑一遍稳定性。

MQL5 / C++
class CCluster
  {
class="kw">private:
  CClusterizerState m_cs;
  CAHCReport        m_rep;
  matrix            m_pd[];
    class=class="str">"cmt">//+------------------------------------------------------------------+
    class=class="str">"cmt">//| Preprocesses class="kw">input matrix before clusterization                  |
    class=class="str">"cmt">//+------------------------------------------------------------------+
    
  class="type">bool              customDist(matrix &structure, class="type">ulong num_factors, matrix& out[], class="type">bool calculate_custom_distances = true)
   {
     class="type">int nvars;
     class="type">class="kw">double dotprod, length;
     nvars = class="type">int(structure.Rows());
     class="type">int ndim = (num_factors && num_factors<=structure.Cols())?class="type">int(num_factors):class="type">int(structure.Cols());
     if(out.Size()<class="num">2)
       if(out.Size()<class="num">2 && (ArrayResize(out,class="num">2)!=class="num">2 || !out[class="num">0].Resize(nvars,ndim) || !out[class="num">1].Resize(nvars,nvars)))
        {
          Print(__FUNCTION__, " error ", GetLastError());
          class="kw">return false;
        }
     if(calculate_custom_distances)
       {
        for(class="type">int i=class="num">0 ; i<nvars ; i++)
         {
          length = class="num">0.0 ;
          for(class="type">int j=class="num">0 ; j<ndim ; j++)
            length += structure[i][j] * structure[i][j] ;
          length = class="num">1.0 / sqrt(length) ;
          for(class="type">int j=class="num">0 ; j<ndim ; j++)
            out[class="num">0][i][j] = length * structure[i][j] ;
         }
        out[class="num">1].Fill(class="num">0.0);
        for(class="type">int irow1=class="num">0 ; irow1<nvars-class="num">1 ; irow1++)
         {
          for(class="type">int irow2=irow1+class="num">1 ; irow2<nvars ; irow2++)
           {
            dotprod = class="num">0.0 ;
            for(class="type">int i=class="num">0 ; i<ndim ; i++)
              dotprod += out[class="num">0][irow1][i] * out[class="num">0][irow2][i] ;

「聚类器的构造与运行入口」

这段 CCluster 类的代码片段展示了层次聚类包装器的骨架,重点在构造、析构与两个对外方法。构造函数直接调用 ClusterizerCreate 初始化内部聚类句柄,析构函数为空,资源由底层库管理。 cluster() 是核心入口:先通过 customDist 生成自定义距离矩阵,若失败立即返回 false;随后把距离矩阵拆成点集 pp 与距离 pd 两份 CMatrixDouble,并依据 dist 参数选择欧氏或自定义距离送入 ClusterizerSetPoints。当 dist==DIST_CUSTOM 时额外调用 ClusterizerSetDistances 注入自定义矩阵。 聚类算法类型由 linkage 参数(MODE_COMPLETE 等)决定,最终跑 RunAHC 并依赖 m_rep.m_terminationtype==1 判断成功。回测中该终止码为 1 代表算法正常收敛,非 1 则聚类结果不可用。 get_clusters() 先检查终止码,若不为 1 则 Print 报错并返回 false,避免向外吐空簇。外汇与贵金属行情用此类聚类做形态分组时波动剧烈,高风险,结果仅作概率参考。

MQL5 / C++
 out[class="num">1][irow1][irow2] = fabs(dotprod) ;
            }
          }
        }
      else
        {
         out[class="num">0] = np::sliceMatrixCols(structure,class="num">0,ndim);
        }
      class="kw">return true;
    }
class="kw">public:
  class=class="str">"cmt">//+------------------------------------------------------------------+
  class=class="str">"cmt">//|  constructor                                                      |
  class=class="str">"cmt">//+------------------------------------------------------------------+
              CCluster(class="type">void)
    {
    CClustering::ClusterizerCreate(m_cs);
    }
  class=class="str">"cmt">//+------------------------------------------------------------------+
  class=class="str">"cmt">//| destructor                                                        |
  class=class="str">"cmt">//+------------------------------------------------------------------+
              ~CCluster(class="type">void)
    {
    }
  class=class="str">"cmt">//+------------------------------------------------------------------+
  class=class="str">"cmt">//| cluster a set                                                     |
  class=class="str">"cmt">//+------------------------------------------------------------------+
  class="type">bool          cluster(matrix &in_points, class="type">ulong factors=class="num">0, ENUM_LINK_METHOD linkage=MODE_COMPLETE, ENUM_DIST_CRIT dist = DIST_CUSTOM)
    {
    if(!customDist(in_points,factors,m_pd,dist==DIST_CUSTOM))
      class="kw">return false;
    CMatrixDouble pp(m_pd[class="num">0]);
    CMatrixDouble pd(m_pd[class="num">1]);
    CClustering::ClusterizerSetPoints(m_cs,pp,pp.Rows(),pp.Cols(),dist<class="num">22?dist:DIST_EUCLIDEAN);
    if(dist==DIST_CUSTOM)
      CClustering::ClusterizerSetDistances(m_cs,pd,pd.Cols(),true);
    CClustering::ClusterizerSetAHCAlgo(m_cs,linkage);
    CClustering::ClusterizerRunAHC(m_cs,m_rep);
    class="kw">return m_rep.m_terminationtype==class="num">1;
    }
  class=class="str">"cmt">//+------------------------------------------------------------------+
  class=class="str">"cmt">//|   output clusters to vector array                                 |
  class=class="str">"cmt">//+------------------------------------------------------------------+
  class="type">bool          get_clusters(vector &out[])
    {
    if(m_rep.m_terminationtype!=class="num">1)
      {
       Print(__FUNCTION__, " no cluster information available");
       class="kw">return false;
      }

◍ 把聚类区间落回价格序列的拷贝逻辑

这段实现负责把聚类算法给出的区间索引,重新映射回原始价格数组。先按主矩阵行数给输出容器 out 扩容,若 ArrayResize 返回值不等于预期行数,直接打印函数名与错误码并退出,避免后续越界。 内层双循环按 m_rep.m_pm 的行列结构读取每段的 from / to 索引。注意列步进是 j+=2,说明每两列描述一个区间起止;用 zz 累计偏移,把 m_rep.m_p[k] 顺序写入 out[i][zz],保证多段区间在输出里首尾相接不重叠。 调用侧先声明 vector clusters[],由 CCluster 的 cluster() 与 get_clusters() 两次判错返回,任一失败就终止脚本。成功后用 Print 把每个 cluster 的变量下标打印到日志,便于在 MT5 终端核对分组结果。外汇与贵金属市场波动剧烈,此类特征工程仅用于辅助研判,实盘仍有较高风险。

MQL5 / C++
if(ArrayResize(out,m_rep.m_pz.Rows())!=m_rep.m_pz.Rows())
  {
   Print(__FUNCTION__, " error ", GetLastError());
   class="kw">return false;
  }
for(class="type">int i = class="num">0; i<m_rep.m_pm.Rows(); i++)
  {
   class="type">int zz = class="num">0;
   for(class="type">int j = class="num">0; j<m_rep.m_pm.Cols()-class="num">2; j+=class="num">2)
     {
      class="type">int from = m_rep.m_pm.Get(i,j);
      class="type">int to = m_rep.m_pm.Get(i,j+class="num">1);
      if(!out[i].Resize((to-from)+zz+class="num">1))
        {
         Print(__FUNCTION__, " error ", GetLastError());
         class="kw">return false;
        }
      for(class="type">int k = from; k<=to; k++,zz++)
         out[i][zz] = m_rep.m_p[k];
     }
  }
class="kw">return true;
 }
};
vector clusters[];
 CCluster fc;
 if(!fc.cluster(fld,Num_Dimensions,AppliedClusterAlgorithm,AppliedDistanceCriterion))
   class="kw">return;
 if(!fc.get_clusters(clusters))
   class="kw">return;

 for(class="type">uint i =class="num">0; i<clusters.Size(); i++)
  {
   Print("cluster at ", i, "\n variable indices ", clusters[i]);
  }

把因子矩阵的样本窗口钉死在代码里

做横截面因子聚类之前,先得把训练样本的时间边界和特征维度在脚本头部写死。下面这段声明把回看步长、最大回看、品种、周期和旋转方式都暴露成 input,BTCUSD 日线从 2019.12.31 到 2022.12.31 是写死的样本窗,调参时直接改这几个变量即可,不用动核心逻辑。 特征数按 (max_lookback/period_inc)*2 算,period_inc=2、max_lookback=50 时 num_features 固定为 50,也就是 25 个回看长度 × 两个指标族(CMMA 与 SLOPE)。这个 50 维矩阵就是后面 PROMAX 旋转和完全连接聚类的输入,维度不够会在后续分解时直接报矩阵秩不足。 OnStart 里用 iBarShift 把日期转成 bar 偏移,samplestart 减 samplestop 加 1 得到 size_observations;若返回负值说明日期在品种历史外,脚本会 Print 错误并 return。开 MT5 把 SetSymbol 换成 XAUUSD 跑一遍,若 samplestop 返回 -1,基本是券商日线历史没覆盖到 2019 年,换数据源即可。外汇与贵金属杠杆高,样本窗外的实盘推断误差可能放大,验证时先用历史数据跑通再谈应用。

MQL5 / C++
class=class="str">"cmt">//|indicator type                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
enum SELECT_INDICATOR
  {
   CMMA=class="num">0,class=class="str">"cmt">//CMMA
   SLOPEclass=class="str">"cmt">//SLOPE
   };
class=class="str">"cmt">//--- class="kw">input parameters
class="kw">input class="type">uint      period_inc=class="num">2;class=class="str">"cmt">//lookback increment
class="kw">input class="type">uint      max_lookback=class="num">50;
class="kw">input ENUM_MA_METHOD        AppliedMA = MODE_SMA;
class="kw">input class="type">class="kw">datetime SampleStartDate=D&class="macro">#x27;class="num">2019.12.class="num">31&class="macro">#x27;;
class="kw">input class="type">class="kw">datetime SampleStopDate=D&class="macro">#x27;class="num">2022.12.class="num">31&class="macro">#x27;;
class="kw">input class="type">class="kw">string   SetSymbol="BTCUSD";
class="kw">input ENUM_TIMEFRAMES SetTF = PERIOD_D1;
class="kw">input ENUM_FACTOR_ROTATION AppliedFactorRotation = MODE_PROMAX;
class="kw">input ENUM_DIST_CRIT AppliedDistanceCriterion = DIST_CUSTOM;
class="kw">input ENUM_LINK_METHOD AppliedClusterAlgorithm = MODE_COMPLETE;
class="kw">input class="type">ulong Num_Dimensions = class="num">10;
class=class="str">"cmt">//----
class="type">class="kw">string csv_header="";                    class=class="str">"cmt">//csv file header
class="type">int size_sample,                         class=class="str">"cmt">//training set size
    size_observations,                   class=class="str">"cmt">//size of of both training and testing sets combined
    maxperiod,                           class=class="str">"cmt">//maximum lookback
    indicator_handle=INVALID_HANDLE;     class=class="str">"cmt">//class="type">long moving average indicator handle
class=class="str">"cmt">//---
vector indicator[];                      class=class="str">"cmt">//indicator indicator values;
class=class="str">"cmt">//---
matrix feature_matrix;                   class=class="str">"cmt">//full matrix of features;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Script program start function                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
class=class="str">"cmt">//---get relative shift of sample set
   class="type">int samplestart,samplestop,num_features;
   samplestart=iBarShift(SetSymbol!=""?SetSymbol:NULL,SetTF,SampleStartDate);
   samplestop=iBarShift(SetSymbol!=""?SetSymbol:NULL,SetTF,SampleStopDate);
   num_features = class="type">int((max_lookback/period_inc)*class="num">2);
class=class="str">"cmt">//---check for errors from ibarshift calls
   if(samplestart<class="num">0 || samplestop<class="num">0)
     {
       Print(ErrorDescription(GetLastError()));
       class="kw">return;
     }
class=class="str">"cmt">//---set the size of the sample sets
   size_observations=(samplestart - samplestop) + class="num">1 ;
   maxperiod=class="type">int(max_lookback);
class=class="str">"cmt">//---check for class="kw">input errors
   if(size_observations<=class="num">0 || maxperiod<=class="num">0)
     {
       Print("Invalid inputs ");
       class="kw">return;
     }

「多周期指标句柄的批量拉取与容错」

把特征数组先按 num_features 扩容,若 ArrayResize 返回的可用长度不足就直接打印错误并退出,这一步决定了后面能不能安稳塞进多组指标缓冲。 外层循环用 SELECT_INDICATOR 枚举从 0 到 1,分别指向 CMMA 与 SLOPE 两类自定义指标;内层按 indicator.Size()/2 切分周期档位,period_len 随档位线性放大(iperiod+1 乘 period_inc)。 每个句柄最多重试 10 次:iCustom 加载失败返回 INVALID_HANDLE 就 try--,成功则 break。若 10 次全败,打印具体枚举名与错误码后 return,避免脏数据进模型。 拿到合法句柄后,CopyIndicatorBuffer 从 samplestop 向后取 size_observations 根 K 线,复制失败会 Sleep(5000) 最多再试 10 次;try<10 才让 k 自增接纳该特征,否则报错清 Comment。外汇与贵金属行情跳空频繁,这种重试机制能降低早盘加载失手的概率。

MQL5 / C++
class=class="str">"cmt">//---allocate memory
   if(ArrayResize(indicator,num_features)<num_features)
      {
         Print(ErrorDescription(GetLastError()));
         class="kw">return;
      }
class=class="str">"cmt">//----get the full collection of indicator values
   class="type">int period_len;
   class="type">int k=class="num">0;
class=class="str">"cmt">//---
   for(SELECT_INDICATOR select_indicator = class="num">0; select_indicator<class="num">2; select_indicator++)
      {
         for(class="type">int iperiod=class="num">0; iperiod<class="type">int(indicator.Size()/class="num">2); iperiod++)
            {
             period_len=class="type">int((iperiod+class="num">1) * period_inc);
             class="type">int try=class="num">10;
             while(try)
               {
                  class="kw">switch(select_indicator)
                     {
                      case CMMA:
                         indicator_handle=iCustom(SetSymbol!=""?SetSymbol:NULL,SetTF,"\\Indicators\\CMMA.ex5",AppliedMA,period_len);
                         class="kw">break;
                      case SLOPE:
                         indicator_handle=iCustom(SetSymbol!=""?SetSymbol:NULL,SetTF,"\\Indicators\\Slope.ex5",period_len);
                         class="kw">break;
                     }
                  if(indicator_handle==INVALID_HANDLE)
                     try--;
                  else
                     class="kw">break;
               }
             if(indicator_handle==INVALID_HANDLE)
               {
                  Print("Invalid indicator handle ",EnumToString(select_indicator)," ", GetLastError());
                  class="kw">return;
               }
             Comment("copying data to buffer for indicator ",period_len);
             try = class="num">0;
             while(!indicator[k].CopyIndicatorBuffer(indicator_handle,class="num">0,samplestop,size_observations) && try<class="num">10)
                  {
                   try++;
                   Sleep(class="num">5000);
                  }
             if(try<class="num">10)
                ++k;
             else
               {
                  Print("error copying to indicator buffers ",GetLastError());
                  Comment("");

◍ 因子分析前的矩阵装配与适格检验

把多指标数据塞进矩阵只是第一步,真正决定后续降维有没有意义的是样本适格性。上面这段代码在 Resize 之后用 Col() 逐列拷贝,任何一列失败就直接 Print 错误并 return,避免脏数据进模型。 矩阵就绪后先做 KMO 和 Bartlett 球形检验。KMO 统计量越接近 1,变量间偏相关性越适合做主成分提取;Bartlett 的 p_value 若大于 0.05,则相关矩阵可能近似单位阵,提取主因子意义不大。 通过检验后调用 Cpfa 的 fit 提取因子载荷,再用 rotate_factorloadings 做旋转。最后用 CCluster 按指定维数和距离准则聚类,把载荷相近的变量归到同一簇,方便你挑出每簇里最具代表性的指标喂给小布做特征精简。 开 MT5 跑这段,重点看日志里的 KMO 数值和 Bartlett 的 p_value——外汇与贵金属波动结构多变,样本窗口换了这两个值可能剧烈跳动,高风险品种务必重测。

MQL5 / C++
   class="kw">return;
     }
     if(indicator_handle!=INVALID_HANDLE && IndicatorRelease(indicator_handle))
        indicator_handle=INVALID_HANDLE;
     }
   }

class=class="str">"cmt">//---resize matrix
   if(!feature_matrix.Resize(size_observations,indicator.Size()))
    {
     Print(ErrorDescription(GetLastError()));
     Comment("");
     class="kw">return;
    }
class=class="str">"cmt">//---copy collected data to matrix
   for(class="type">ulong i = class="num">0;i<feature_matrix.Cols(); i++)
    if(!feature_matrix.Col(indicator[i],i))
     {
     Print(ErrorDescription(GetLastError()));
     Comment("");
     class="kw">return;
     }
class=class="str">"cmt">//---
   Comment("");
class=class="str">"cmt">//---test dataset for principal factor analysis suitability
class=class="str">"cmt">//---kmo test
   vector kmo_vect;
   class="type">class="kw">double kmo_stat;
   kmo(feature_matrix,kmo_vect,kmo_stat);
   Print("KMO test statistic ", kmo_stat);
class=class="str">"cmt">//---Bartlett sphericity test
   class="type">class="kw">double bs_stat,bs_pvalue;
   bartlet_sphericity(feature_matrix,bs_stat,bs_pvalue);
   Print("Bartlett sphericity test p_value ", bs_pvalue);
class=class="str">"cmt">//---Extract the principal factors
   Cpfa fa;
class=class="str">"cmt">//---
   if(!fa.fit(feature_matrix))
      class="kw">return;
class=class="str">"cmt">//---
   matrix fld = fa.get_factor_loadings();
class=class="str">"cmt">//---
   matrix rotated_fld = fa.rotate_factorloadings(AppliedFactorRotation);
class=class="str">"cmt">//---
   Print(" factor loading matrix ", fld);
class=class="str">"cmt">//---
   Print("\n rotated factor loading matrix ", rotated_fld);
class=class="str">"cmt">//---
   matrix egvcts;
   vector egvals;
   fa.get_eigen_structure(egvcts,egvals,false);
   Print("\n vects ", egvcts);
   Print("\n evals ", egvals);
class=class="str">"cmt">//---
   vector clusters[];
   CCluster fc;
   if(!fc.cluster(fld,Num_Dimensions,AppliedClusterAlgorithm,AppliedDistanceCriterion))
      class="kw">return;
   if(!fc.get_clusters(clusters))
      class="kw">return;

   for(class="type">uint i =class="num">0;i<clusters.Size(); i++)
    {
     Print("cluster at ", i, "\n variable indices ", clusters[i]);
    }
   }

时间序列的一致性

随着时间的推移,分析变量时,它们之间的关系可能会发生意外变化。通常相关的变量可能会突然出现分歧,预示着潜在的问题。例如,温度变化会影响电力需求,进而影响天然气价格。如果它们通常的模式发生了变化,这可能表明正在发生一些不寻常的事情。同样,通常独立表现的变量可能会突然一起移动,例如当股市的不同板块因积极的经济消息而同时上涨时。 衡量一致性涉及量化一组时间序列变量在移动时间窗口内的关联程度。一种基本方法是检查最大特征值捕获了多少方差。不过,这种方法可能会有局限性,因为它只考虑了一个维度。一种更全面的方法涉及对最大特征值求和,特别是在变量之间存在多个关系的情况下。这种方法可以更准确地反映具有复杂相互关系的系统中的整体一致性,但需要事先知道哪些是最相关的因素。这也许是不可能的,或者太主观了。 对于关系数量未知或随时间波动的情况,特别是涉及大量变量的情况,需要一种更通用的方法。在维数未知的情况下,我们可以将排序后的特征值从最大到最小可视化,就像管弦乐队的成员。制作优美音乐的关键是以统一的方式相应地调节不同的乐器。如果管弦乐队的每个成员都不能在正确的水平上跟上,那么演奏出来的音乐就会很糟糕,凝聚力会很差。想象一下,每个乐团成员的声音输出都是一个加权值,对观众所听到的音乐做出了贡献。这些值的不平衡代表了一致性,我们计算的是加权总和,权重表示一种乐器可以产生的音量。 如果每个乐器(变量)都独立演奏自己的曲调,那么整体声音就会杂乱无章,代表零一致性。然而,当乐器完美同步、和谐演奏时,它们就会演奏出具有凝聚力的美妙乐曲,代表着完全的一致性。在这个比喻中,一致性就像管弦乐队的和声,表示乐器(变量)的配合程度。如果和声突然发生变化,说明乐器或乐曲出现了异常。 让我们考虑两个极端情况,如果变量完全独立,这些变量的相关矩阵将是一个同一矩阵,所有特征值都相等(1.0)。加权和(由于权重对称)将为零,反映出一致性为零。或者,如果变量之间完全相关,则只存在一个非零特征值,等于变量的数量。加权和变为变量数,经过归一化处理(除以变量数)后,一致性为 1.0,反映了完全相关性。这种方法根据特征值分布的不平衡提供了 0 到 1 的一致性度量,而无需对维度做任何假设。 为说明一致性,我们将制作一个指标,用于衡量一个时间窗口内不同交易品种收盘价的一致性。该指标名为 Coherence.mq5。用户可以通过以逗号分隔的工具名称列表来添加众多交易品种,从而测量它们之间的内聚力。该指标采用不同的方法计算多个变量之间的相关性。这一次,我们使用斯皮尔曼非参数相关系数。 由于我们使用 Aglib 的 EVD 实现,我们不需要定义完整的校正矩阵,我们只需要构造上三角形或下三角形。我们不需要特征向量,只需要特征值。 为了得到特征值在正确方向上的分布,我们必须反转向量。 使用特征值计算内聚力。 完整代码附在文章末尾。通过衡量加密货币 BTCUSD、DOGUSD 和 XRPUSD 之间的一致性,让我们看看指标在不同窗口长度下的表现。 通过观察 60 天的 "一致性" 曲线图,我们发现这些交易品种的移动具有显著的一致性,从而消除了个人的成见。令人惊讶的是,它的波动幅度如此之大,值在所有可能值的范围内都有所不同。 随着窗口长度的增加,我们开始看到相干性的稳定期,但这种相干性的性质同样出乎意料。在相当长的一段时间里,相干性几乎为零。 [CODE] covar[<span class="number">0</span>][<span class="number">0</span>] = <span class="numb

「画得少,看得清」

把特征值与特征向量从数学课本搬进 MT5,真正有用的地方不是算得炫,而是把高维行情压缩成少数几个能解释大部分波动的主因子。前面给出的 np.mqh 与 pfa.mqh 两个头文件,分别管矩阵运算和因子分析类,EDA.mq5 脚本一次跑通了从自定义指标取数到主因子分解的全流程;TestEigenDecomposition.mq5 只用 1.37 KB 就复现了内置 Eig() 的分解结果,可作为你上手对照的基准。 Coherence.mq5 把三个品种的相关性做成指标,PrincipalFactors.ex5 是 390.7 KB 的已编译 EA,依赖老旧 Easy and Fast GUI 库,加载前先确认终端环境能跑这套界面。外汇与贵金属波动受杠杆与消息面放大,用因子视角降维只是提高辨识率,不消除方向误判的概率。 真要验证这套路数,直接下 Mql5.zip(409.62 KB)解压,把 EDA.mq5 挂上你常用的周期跑一遍,看前两个特征值的累计贡献是否稳定过 70%;若换品种后陡降,说明该市场的结构噪声本就不宜硬压。

把特征扫描交给小布盯盘
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到变量冗余与同步提示,你只需判断哪条潜在因子在驱动盘面。

常见问题

PCA 把原变量重组成不相关的主成分以抓方差;因子分析反过来假定潜在因子驱动观测指标。本文侧重后者,用来找隐藏维度而非单纯降维。
方向才是关联关键,单位化只是为了计算稳定与跨矩阵比较,避免量纲干扰特征结构的几何解释。
常用特征值大于 1 的准则或斜率拐点,但样本少时可能失真,需结合解释方差比例与业务逻辑交叉验证。
可以,小布盯盘的品种页内置了基于特征结构的冗余与同步诊断,省去你自己跑矩阵的重复劳动。
能,特征向量符号相反的分量往往对应反向行为,间隔采样比对可看出哪些指标在特定周期里互斥。
高杠杆下关系会随流动性突变,特征结构可能漂移,结论仅作概率参考,务必配合风控。