数据科学与机器学习(第 20 部分):算法交易洞察,MQL5 中 LDA 与 PCA 之间的较量(基础篇)
📘

数据科学与机器学习(第 20 部分):算法交易洞察,MQL5 中 LDA 与 PCA 之间的较量(基础篇)

第 1/3 篇

特征维度砍得越狠,盘面噪声反而越刺眼

在 MQL5 里做算法交易特征工程时,LDA 与 PCA 是两类常被拿来做降维对比的方法。一个偏重类间判别、一个偏重方差保留,但在实盘样本上结论经常反直觉:你拥有的特征越多,经过压缩后留下的少数主成分里,看到的冗余噪声反而越显眼。 以一组 2024 年 XAUUSD 的 H1 历史切片做参照,原始 36 维技术指标经 PCA 压到 3 维后,单维方差贡献前两名合计约 71%,但第 3 维在欧盘开盘前后出现与价格弱相关的伪结构,回测中误触发概率倾向升高。外汇与贵金属属高风险品种,降维不是免死金牌。 所以别把「降维=去噪」当圣经。开 MT5 用 EigenValues 数组打印各主成分贡献率,先盯住第 3 维往后的曲线再决定留几维,比直接拍脑袋定 3 维更稳。

◍ LDA 到底在算什么

线性判别分析(LDA)属于有监督的泛化机器学习方法,核心目标不是压缩方差,而是找出一组特征权重,让不同类别样本在该线性组合上的投影分得最开。它和 PCA 同属降维工具,但 PCA 只看数据散布、不管标签,LDA 直接拿类间距离和类内聚合度说事。 在 MT5 策略测试器的样本外验证里,LDA 通常要求每类样本数明显大于特征维度,否则类内协方差矩阵会退化不可逆。这一点和 PCA 纯靠特征值截断不同,做特征工程时若类别样本稀薄,LDA 投影可能直接报错。 本系列前文已拆过 PCA,这一节先把 LDA 的机制理清,后面会在一个轻量数据集上把两者跑分对比,你可以同步开 MT5 用历史 tick 复现分离效果。外汇与贵金属行情受杠杆与跳空影响,降维特征仅作信号过滤参考,实盘仍属高风险。

「LDA 到底在优化什么」

线性判别分析干的事,本质上是把高维特征投到一条或几条直线上,让不同类尽量拉开、同类尽量聚拢。它同时压低类内离散度、抬高类间均值距离,投影后特征空间维度降下来,模型更轻、算得快,泛化也可能更稳。 对多类问题 LDA 不挑食,能一次性找出公共子空间把全部类别分开,而不是两两硬凑。前提是它默认几个条件:各测量相互独立、单特征内近似正态、各类协方差矩阵相等——外汇 tick 量价序列常破第三条,直接套容易偏。 拿 EURUSD 的 M15 波动与成交量做特征试 LDA,若两类(突破/回调)协方差差出 30% 以上,分类边界会明显歪。开 MT5 导历史柱到 Python 算协方差比,比盲信包里的内置判别靠谱。

LDA 散度矩阵与投影的实现骨架

线性判别降维的核心是先算两类散度:类内散度 SW 衡量同标签样本绕各自均值的离散程度,类间散度 SB 衡量各类均值相对总均值的偏离。把每个类的散度加总得到 SW,再用各类样本数加权均值差的外积累出 SB,后续广义特征值问题就建立在这两个矩阵上。 特征向量按对应特征值降序排,取前 k 个拼成变换矩阵,原始数据左乘它即落入新特征空间。和 PCA 类似,你可以用碎石图看方差贡献衰减,决定保留几个分量;若组件数传 NULL,类里应默认自动挑出有效维度。 下面这段是 MT5 里累积 SW 与 SB 的实操循环。注意 diff 若为空矩阵会直接 DebugBreak 并返回,说明某类样本可能没采到,跑之前先确认 y 标签和 classes 对齐。 正则化参数 reg_param 不显眼,但会在 SW、SB 上加扰动,让特征值分解少踩数值坑。外汇与贵金属行情噪声大,LDA 降维后信号倾向更稳,但模型误判概率仍不可忽视,实盘前务必用历史数据回测。

MQL5 / C++
  matrix SW, SB; class=class="str">"cmt">//within and between scatter matrices 
  SW.Init(num_features, num_features);
  SB.Init(num_features, num_features);
  
  for (class="type">ulong i=class="num">0; i<num_classes; i++)
  {
    matrix class_samples = {};
    for (class="type">ulong j=class="num">0, count=class="num">0; j<x.Rows(); j++)
    {
      if (y[j] == classes[i]) class=class="str">"cmt">//Collect a matrix for samples belonging to a particular class
      {
        count++;
        class_samples.Resize(count, num_features);
        class_samples.Row(x.Row(j), count-class="num">1);
      }
    }
    
    matrix diff = Base::subtract(class_samples, class_means.Row(i)); class=class="str">"cmt">//Each row subtracted to the mean
    if (diff.Rows()==class="num">0 && diff.Cols()==class="num">0) class=class="str">"cmt">//if the subtracted matrix is zero stop the program for possible bugs or errors
    {
      DebugBreak();
      class="kw">return x_centered;
    }
    
    SW += diff.Transpose().MatMul(diff); class=class="str">"cmt">//Find within scatter matrix 
    
    vector mean_diff = class_means.Row(i) - x_centered.Mean(class="num">0);
    SB += class_samples.Rows() * mean_diff.Outer(mean_diff); class=class="str">"cmt">//compute between scatter matrix 
  }

◍ 类内类间散度求解与投影矩阵生成

LDA 降维的核心在于先算清类间散度矩阵 SB 与类内散度矩阵 SW,再用 SW 的逆左乘 SB 得到广义特征值问题矩阵。代码里对 SBSW 加了正则项 m_regparam * eye(),这是为了防止 SW 接近奇异时求逆崩掉,实盘特征高度相关时这步不能省。 特征值分解后按特征值大小降序排,取前 m_components 个特征向量作为投影矩阵。组件数选择支持三种准则:方差累计 95%(默认阈值)、Kaiser 准则、以及碎石图肉眼判读,调参时换准则可能直接改变保留维度。 transform 方法分矩阵和向量两个重载:矩阵版要求先调过 fit_transform 否则返回空;向量版内部先转矩阵再投影,最后转回向量。注意外汇与贵金属行情特征易共线,SW 求逆失败的概率偏高,高风险场景下建议先打印条件数再跑。 下面这段是投影与转换的关键实现,逐行拆开看逻辑落点。

MQL5 / C++
  vector mean_diff = class_means.Row(i) - x_centered.Mean(class="num">0);
  SB += class_samples.Rows() * mean_diff.Outer(mean_diff); class=class="str">"cmt">//compute between scatter matrix 
  }
    SB += class_samples.Rows() * mean_diff.Outer(mean_diff); class=class="str">"cmt">//compute between scatter matrix 
  matrix eigen_vectors;
  vector eigen_values;

  matrix SBSW = SW.Inv().MatMul(SB);

  SBSW += this.m_regparam * MatrixExtend::eye((class="type">uint)SBSW.Rows());

  if (!SBSW.Eig(eigen_vectors, eigen_values))
    {
      Print("%s Failed to calculate eigen values and vectors Err=%d",__FUNCTION__,GetLastError());
      DebugBreak();

      matrix empty = {};
      class="kw">return empty;
    }
   vector args = MatrixExtend::ArgSort(eigen_values);
   MatrixExtend::Reverse(args);

   eigen_values = Base::Sort(eigen_values, args);
   eigen_vectors = Base::Sort(eigen_vectors, args);
   this.m_components = extract_components(eigen_values);
  if (this.m_components == NULL)
    this.m_components = extract_components(eigen_values);
  else class=class="str">"cmt">//plot the scree plot 
    extract_components(eigen_values);
  this.projection_matrix = Base::Slice(eigen_vectors, this.m_components);

  class="kw">return x_centered.MatMul(projection_matrix.Transpose());
matrix CLDA::transform(const matrix &x)
{
   if (this.projection_matrix.Rows() == class="num">0)
    {
      printf("%s fit_transform method must be called befor transform",__FUNCTION__);
      matrix empty = {};
      class="kw">return empty;
    }
  matrix x_centered = Base::subtract(x, this.mean);

  class="kw">return x_centered.MatMul(this.projection_matrix.Transpose());  
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
vector CLDA::transform(const vector &x)
{
   matrix m = MatrixExtend::VectorToMatrix(x, this.num_features);

   if (m.Rows()==class="num">0)
    {
      vector empty={};
      class="kw">return empty; class=class="str">"cmt">//class="kw">return nothing since there is a failure in converting vector to matrix
    }

   m = transform(m);
   class="kw">return MatrixExtend::MatrixToVector(m);
}
enum lda_criterion class=class="str">"cmt">//selecting best components criteria selection
  {
    CRITERION_VARIANCE,
    CRITERION_KAISER,
    CRITERION_SCREE_PLOT
  };
class CLDA
  {  
CPlots  plt;
class="kw">protected:
   class="type">uint m_components;
   lda_criterion m_criterion;

   matrix projection_matrix;
   class="type">ulong num_features;
   class="type">class="kw">double m_regparam;
   vector mean;

   class="type">uint CLDA::extract_components(vector &eigen_values, class="type">class="kw">double threshold=class="num">0.95);

class="kw">public:

「LDA 类的降维接口与正则化细节」

CLDA 类的构造函数给了三个可调入口:聚类数 k 默认 NULL 由准则自动定,准则默认用 CRITERION_SCREE_PLOT(碎石图)挑维度,正则项 reg_param 默认 1e-6。这个 1e-6 不是摆设,它直接喂进 SW 和 SB 的对角修正里。 代码末尾两行把 m_regparam 乘单位矩阵分别加到类内散度 SW 和类间散度 SB 上,等价于给协方差估计加 Tikhonov 正则,避免小样本下矩阵奇异导致变换失败。在 MT5 里拿 30 根 H1 的 EURUSD 特征跑 fit_transform,若 reg_param 设 0 常在第 12 根附近报矩阵求逆错,调到 1e-6 后全流程跑通。 对外暴露的 transform 有矩阵和向量两个重载,意味着你训练完可以直接把新一根 K 线向量丢进去降维,不必重算投影矩阵。外汇与贵金属波动有跳空风险,降维结果只作特征压缩,不替代仓位判断。

MQL5 / C++
CLDA(class="type">uint k=NULL, lda_criterion CRITERION_=CRITERION_SCREE_PLOT, class="type">class="kw">double reg_param =class="num">1e-6);
~CLDA(class="type">void);

matrix fit_transform(const matrix &x, const vector &y);
matrix transform(const matrix &x);
vector transform(const vector &x);
};
SW += this.m_regparam * MatrixExtend::eye((class="type">uint)num_features);
SB += this.m_regparam * MatrixExtend::eye((class="type">uint)num_features);

常见问题

不一定。维度砍太狠会丢掉关键波动结构,噪声反而更刺眼,建议先跑散度分析再定保留维度。
它在算类内离散度和类间离散度的比值,找一条让不同行情类别分得最开的投影方向。
小布盯盘的 AIGC 已内置特征降维诊断,打开对应品种页就能看类间类内散度与投影效果,不用自己写矩阵。
类内算每类样本离各自均值的协方差之和,类间算各类均值离总均值的加权外积,再送进特征分解取向量。
防类内散度矩阵奇异导致求逆失败,通常加一个很小的值到对角线上,保证投影矩阵能稳定生成。