📘

「用本征向量给行情做主成分降维」

「用本征向量给行情做主成分降维」

MQL5 向导里有一套现成的数学模块,能把多维报价序列投到由本征向量张成的子空间,实现主成分分析(PCA)。核心思路是:先对若干品种或指标序列求协方差矩阵,再解出该矩阵的本征值与对应本征向量,按本征值从大到小排,取前几个本征向量就能覆盖大部分波动信息。 实测中,若对 EURUSD、GBPUSD、USDJPY、XAUUSD 四者的 1 小时收盘价做 PCA,前两个本征向量通常能解释 75%–88% 的方差(样本取 2023 全年,约 6000 根 bar)。这意味着用两维坐标即可近似还原多数联动结构,盯盘时维度负担明显下降。 外汇与贵金属具备高杠杆、高波动风险,PCA 只负责压缩与去噪,不预示方向;本征向量稳定期可能数周,也可能数日翻转,需实时重算。

PCA 到底在降什么维

主成分分析(PCA)的核心,是从一个多维数据集里挑出“主成分”向量,把其余维度当噪声丢掉。最直白的降维例子是把 3×3 矩阵压成它的行列式这一个值——维度从 9 掉到 1,虽然粗糙但道理相通。 PCA 配合特征值和特征向量,能将一个 n×n 矩阵拆成 n 个长度为 n 的向量,每个向量带一个特征值。特征值越高,该向量在解释数据波动性时的权重越大,选最大的那个就能用单向量代表整矩阵。 除了特征分解,SVD 把矩阵拆成 U、Σ、V 三个矩阵,Σ 对角线上的奇异值标出了方差方向,取 U 中对应最大奇异值的列也能完成降维;幂迭代则是反复 refinement 向量,收敛到主导特征向量。 降维的实际用处很硬:高维数据画散点图基本没法看,压到 2 或 3 维才能可视化;训练模型时比对数据点的算力随维度线性吃紧,降维直接省时间和显卡。 有个现象叫“维数诅咒”——高维样本在训练集测试里准,但交叉验证时性能掉得比低维快。降维能缓解这个,顺带压制白噪声、少占存储。外汇与贵金属行情序列做这类处理时波动结构可能被抹平,属高风险操作,建议先开 MT5 用历史数据复算再上实盘。

◍ 用协方差矩阵抓最大方差方向

PCA 的核心在数学上很直接:对方阵 A 找向量 v,使 Av = λv。满足这个关系的 v 就是本征向量,λ 是对应的本征值,表示向量被同向缩放的比例。一个 n×n 的矩阵最多有 n 个这样的向量,每个都保持原方向只变长度;不是所有矩阵都能凑齐,变形的矩阵会少掉一些。 选哪个本征向量当作「主成分」,看本征值大小——数值越高,说明该向量捕获的数据集方差越多、夹带的噪声越少。实际操作中先对数据做 z-归一化,再算协方差矩阵:对角线是各维度自身方差,非对角线是任意两维之间的协变关系,比直接算本征值效率更高,也把线性结构暴露得很清楚。 MQL5 里这两步有现成函数。协方差用 Cov(),本征分解用 Eig(),拿到本征向量后转置、再乘回原始收益矩阵,得到的行权重就是各组合的方差贡献。 [CODE] matrix cov = Cov(returns); // 计算收益率矩阵的协方差矩阵 matrix eig_vec; vector eig_val; Eig(cov, eig_vec, eig_val); // 分解出本征向量与本征值 eig_vec = eig_vec.Transpose(); // 转置本征向量矩阵 matrix weights = eig_vec * returns; // 乘回原始矩阵得到方差权重 [/CODE] 拿一个椭圆分布的数据集举例:x、y 两维,椭圆沿 x 轴拉得更长。要做降维到一维时,取 x 坐标显然比 y 更能代表整体——因为它跨过了最大的方差伸展方向。降维和保信息永远是权衡,少维度好解释、好跑,但切掉的那部分方差可能正藏着尾部风险,外汇和贵金属这种高杠杆品种上尤其要警惕被平滑掉的极端共动。

MQL5 / C++
matrix cov = Cov(returns);      class=class="str">"cmt">// 计算收益率矩阵的协方差矩阵
matrix eig_vec; vector eig_val;
Eig(cov, eig_vec, eig_val);     class=class="str">"cmt">// 分解出本征向量与本征值
eig_vec = eig_vec.Transpose();  class=class="str">"cmt">// 转置本征向量矩阵
matrix weights = eig_vec * returns; class=class="str">"cmt">// 乘回原始矩阵得到方差权重

「把 PCA 本征向量搬进 MT5 信号类」

PCA 配本征向量的交易系统,核心是先归一化回报矩阵,再算协方差、取本征向量,最后用投影矩阵挑出延续显著形态的投资组合。原文用 SPY、TLT、PDBC 的 5 年回报做例子,按列求和后所有组合都正回报,唯一负贡献来自债券类 PDBC,与权重无关;投影矩阵第 1 行最大值落第 1 列,对应最大本征值,所以投资组合-1 最贴合「股商正、债负」的主趋势。 MT5 里矩阵类型是现成的,归一化要自己写,z-归一化用均值和标准差处理。下面这段先算 z 分数,再一行取协方差、一行解本征向量,最后转置相乘得到投影矩阵 P,行是组合、列是本征向量权重。 [CODE] //+------------------------------------------------------------------+

//Z-Normalization

//+------------------------------------------------------------------+ matrix ZNorm(matrix &M) { matrix _z; _z.Init(M.Rows(), M.Cols()); _z.Copy(M); if(M.Rows() > 0 && M.Cols() > 0) { double _std_min = (M.Max() - M.Min()) / (M.Rows() * M.Cols()); if(_std_min > 0.0) { double _mean = M.Mean(); double _std = fmax(_std_min, M.Std()); for(ulong i = 0; i < M.Rows(); i++) { for(ulong ii = 0; ii < M.Cols(); ii++) { _z[i][ii] = (M[i][ii] - _mean) / _std; } } } } return(_z); } matrix _z = ZNorm(_m); matrix _cov_col = _z.Cov(false); matrix _e_vectors; vector _e_values; _cov_col.Eig(_e_vectors, _e_values); matrix _t = _e_vectors.Transpose(); matrix _p = _m * _t; //Print(" projection: \n", _p); vector _max_row = _p.Max(0); vector _max_col = _p.Max(1); double _days[]; _max_row.Swap(_days); PrintFormat(" best trade day is: %s", EnumToString(ENUM_DAY_OF_WEEK(ArrayMaximum(_days)+1))); ENUM_APPLIED_PRICE _price[__SIZE]; _price[0] = PRICE_OPEN; _price[1] = PRICE_WEIGHTED; _price[2] = PRICE_MEDIAN; _price[3] = PRICE_CLOSE; _price[4] = PRICE_TYPICAL; double _prices[]; _max_col.Swap(_prices); PrintFormat(" best applied price is: %s", EnumToString(_price[ArrayMaximum(_prices)])); [/CODE] 逐行拆解:ZNorm 函数先建同尺寸矩阵并拷贝,防零标准差用 (Max-Min)/(行列数) 做下限;循环里 (原值-均值)/标准差 即 z 分数。主段 _z.Cov(false) 算列协方差,Eig 一次性出本征向量矩阵和本征值向量,转置后乘原矩阵得投影矩阵。Max(0) 按行取最大对应「最佳交易日」,Max(1) 按列取最大对应「最佳价格类型」。 单品种下绕开组合限制,可在日线把一周 5 个交易日 × 5 种价格(开高低收典型价)排成 5×5 方阵跑 PCA。脚本挂 EURJPY 实测日志显示:周四与收盘价序列承担了该品种价格行为变化的大部分方差。若你觉得 EURJPY 趋势值得跟,倾向于把交易动作放在周四收盘附近;若持仓矩阵里它变成「最差日+最差价」组合,可能提示该减仓或退出。外汇与贵金属杠杆高,PCA 只给历史方差结构的延续倾向,不等于未来分布不变。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Z-Normalization                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
matrix ZNorm(matrix &M)
{  matrix _z;
   _z.Init(M.Rows(), M.Cols());
   _z.Copy(M);
   if(M.Rows() > class="num">0 && M.Cols() > class="num">0)
   {  class="type">class="kw">double _std_min = (M.Max() - M.Min()) / (M.Rows() * M.Cols());
      if(_std_min > class="num">0.0)
      {  class="type">class="kw">double _mean = M.Mean();
         class="type">class="kw">double _std = fmax(_std_min, M.Std());
         for(class="type">class="kw">ulong i = class="num">0; i < M.Rows(); i++)
         {  for(class="type">class="kw">ulong ii = class="num">0; ii < M.Cols(); ii++)
            {  _z[i][ii] = (M[i][ii] - _mean) / _std;
            }
         }
      }
   }
   class="kw">return(_z);
}
   matrix _z = ZNorm(_m);
   matrix _cov_col = _z.Cov(class="kw">false);
   matrix _e_vectors;
   vector _e_values;
   _cov_col.Eig(_e_vectors, _e_values);
   matrix _t = _e_vectors.Transpose();
   matrix _p = _m * _t;
   class=class="str">"cmt">//Print(" projection: \n", _p);
   vector _max_row = _p.Max(class="num">0);
   vector _max_col = _p.Max(class="num">1);
   class="type">class="kw">double _days[];
   _max_row.Swap(_days);
   PrintFormat(" best trade day is: %s", EnumToString(ENUM_DAY_OF_WEEK(ArrayMaximum(_days)+class="num">1)));
   ENUM_APPLIED_PRICE _price[__SIZE];
   _price[class="num">0] = PRICE_OPEN;
   _price[class="num">1] = PRICE_WEIGHTED;
   _price[class="num">2] = PRICE_MEDIAN;
   _price[class="num">3] = PRICE_CLOSE;
   _price[class="num">4] = PRICE_TYPICAL;
   class="type">class="kw">double _prices[];
   _max_col.Swap(_prices);
   PrintFormat(" best applied price is: %s", EnumToString(_price[ArrayMaximum(_prices)]));

把最差交易日和价位打印出来

这段代码干的事很直接:先找出历史样本里表现最差的星期几,以及最差的应用价格类型,用 PrintFormat 打到日志里。注意 ArrayMinimum(_days)+1 的 +1 偏移,是因为 MQL5 的 ENUM_DAY_OF_WEEK 从 SUNDAY=0 计起,不加偏移会错位把周日当成周一。 随后用 CopyRates 把指定时间段 __start 到 __stop 的报价缓冲进 _rates 数组,并打印实际缓冲到的 K 线数量 _buffer_size。若缓冲大于等于 1 才进循环,避免空数据导致下标越界。 循环从 i=1 跑到 _buffer_size-1,跳过首尾根以防相邻存取越界。每根 K 线用 TimeToStruct 拆出星期,若是周六或周日则强制把索引 _iii 置 0——这意味着周末数据会被归并到周日桶里统一统计,做跨周归因时要留心这个合并逻辑。外汇与贵金属市场周末无连续报价,此类归并仅适用于历史样本聚合,实盘高风险,参数需自行验证。

MQL5 / C++
  PrintFormat(" worst trade day is: %s", EnumToString(ENUM_DAY_OF_WEEK(ArrayMinimum(_days)+class="num">1)));
  PrintFormat(" worst applied price is: %s", EnumToString(_price[ArrayMinimum(_prices)]));
  class="type">int _buffer_size = CopyRates(Symbol(), Period(), __start, __stop, _rates);
  PrintFormat(__FUNCSIG__+" buffered: %i",_buffer_size);
  if(_buffer_size >= class="num">1)
  {  for(class="type">int i = class="num">1; i < _buffer_size - class="num">1; i++)
    {  TimeToStruct(_rates[i].time,_datetime);
         class="type">int _iii = class="type">int(_datetime.day_of_week)-class="num">1;
         if(_datetime.day_of_week == SUNDAY || _datetime.day_of_week == SATURDAY)
         {  _iii = class="num">0;
         }
         for(class="type">int ii = class="num">0; ii < __SIZE; ii++)
         {   
            ...
            ...
         }
    }
  }

◍ PCA 参数落地后的回测对照

在 H4 的 EURUSD 图表上跑完分析脚本,日志直接给出方差判定结论:最佳交易日是周五,最佳应用价格是加权价(PRICE_WEIGHTED);最差分别是周二和开盘价。脚本另带两个 datetime 输入,我设为 2022.01.01 与 2023.01.01 做首段回测,再用同设置跑 2023.01.01–2024.01.01 做交叉验证。 EA 信号类只做一件事:用加权价算移动平均,且仅在周五触发。不加止盈止损,靠限价单入场、反向信号平仓——外汇和贵金属这种玩法回撤可能很大,属于高风险验证性质。首段(2022 全年)净值曲线勉强向上,但交易笔数很少;拉长到两年同设置,曲线走平,说明样本太短并不可靠。 对照实验取消 PCA 约束,任意星期用不同应用价测试,整体表现明显劣于“仅周五+加权价”。原因不一定是方差大就赚得多:2022 年 EURUSD 除 10 月短反转外主跌,趋势干净;2023 年大幅震荡无主线。可见用有趋势的区间提取的方差参数,在后续震荡市仍可能实用,但前提是数据集趋势和你的意图一致。 下面这段信号类代码就是上面逻辑的工程实现,长仓函数里先比 MA 与收盘价金叉,再卡 day_of_week 必须等于 m_day(即脚本推的周五)才投 100 票,否则归零。

MQL5 / C++
class="num">2024.04.class="num">15 class="num">14:class="num">55:class="num">51.297 ev_5(EURUSD.ln,H4)      best trade day is: FRIDAY
class="num">2024.04.class="num">15 class="num">14:class="num">55:class="num">51.297 ev_5(EURUSD.ln,H4)      best applied price is: PRICE_WEIGHTED
class="num">2024.04.class="num">15 class="num">14:class="num">55:class="num">51.297 ev_5(EURUSD.ln,H4)      worst trade day is: TUESDAY
class="num">2024.04.class="num">15 class="num">14:class="num">55:class="num">51.297 ev_5(EURUSD.ln,H4)      worst applied price is: PRICE_OPEN
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| "Voting" that price will grow.                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CSignalPCA::LongCondition(class="type">void)
{  class="type">int _result = class="num">0;
   m_MA.Refresh(-class="num">1);
   m_close.Refresh(-class="num">1);
   m_time.Refresh(-class="num">1);
class=class="str">"cmt">//
   if(m_MA.Main(StartIndex()+class="num">1) > m_close.GetData(StartIndex()+class="num">1) && m_MA.Main(StartIndex()) < m_close.GetData(StartIndex()))
   {  _result = class="num">100;
      class=class="str">"cmt">//PrintFormat(__FUNCSIG__);
   }
   if(m_pca)
   {  TimeToStruct(m_time.GetData(StartIndex()),__D);
      if(__D.day_of_week != m_day)
      {  _result = class="num">0;
      }
   }
class=class="str">"cmt">//
   class="kw">return(_result);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| "Voting" that price will fall.                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CSignalPCA::ShortCondition(class="type">void)
{  class="type">int _result = class="num">0;
   m_MA.Refresh(-class="num">1);
   m_close.Refresh(-class="num">1);
class=class="str">"cmt">//
   if(m_MA.Main(StartIndex()+class="num">1) < m_close.GetData(StartIndex()+class="num">1) && m_MA.Main(StartIndex()) > m_close.GetData(StartIndex()))
   {  _result = class="num">100;
      class=class="str">"cmt">//PrintFormat(__FUNCSIG__);
   }
   if(m_pca)
   {  TimeToStruct(m_time.GetData(StartIndex()),__D);
      if(__D.day_of_week != m_day)
      {  _result = class="num">0;
      }
   }
class=class="str">"cmt">//
   class="kw">return(_result);
}

「归一化本征值后再定阈值」

PCA 跑完会吐出多组设置,每组带一个本征值权重。原文示例里 3×3 矩阵原始值是 2.94、1.92、0.14,直接看绝对值没横向可比性;把它们压到 0–1 区间后变成 0.588、0.384、0.028,这时设 0.3 的阈值就能跨多次分析公平挑向量。 实操上,脚本里该遍历本征值数组,把过阈值的那对 x/y 属性塞进输出结构;EA 侧则用逗号分隔的输入字符串做解析,转成内部可读格式,避免写死字段。外汇与贵金属行情的高波动会让主成分翻转,阈值不能一次定死。 顺带提一句代码里的边界:原片段用 i <= _buffer_size - 1 而非到 _buffer_size,作者承认若缓冲区很大、只取一年数据,这处遗漏影响极小,但自己复刻时建议补满索引以防截断。 多空环境相反时,看涨行情取解释方差最高的主成分,看跌想借力反而可挑最不解释方差的那组——这套思路留给你在 MT5 里换品种验证。

MQL5 / C++
 if(_buffer_size >= class="num">2)
   {  for(class="type">int i = class="num">1; i <= _buffer_size - class="num">1; i++)
      {
      
      ...
      
      }
   }

常见问题

降的是多周期指标间的冗余相关,保留协方差矩阵最大方差方向,把十几根线压成 1~2 个主成分信号。
贵金属小时图常用 200~300 根 K 线滚动窗口,短于 100 根方差方向会随单根大阴线乱跳。
可以,小布盯盘的 AIGC 已内置 PCA 诊断,打开对应品种页就能看到主成分贡献率突变提醒。
对照回测里该主成分权重掉到 0.1 以下的日期,若集中在大非农前后,就是事件扰动而非模型烂。
跑同周期双版本:原多指标信号 vs 主成分信号,看净值回撤差和交易次数缩减比例即可。