一维奇异谱分析(SSA)基础篇:把金融序列拆成趋势周期与噪声的线性代数骨架(基础篇)
(1/3)·多数交易者把 SSA 当黑箱调用,却不知轨迹矩阵与秩一分解才是识别市场结构的根
金融市场的高波动让模式识别异常困难,而 SSA 不需平稳性假设就能拆出趋势、周期与噪声。可惜大量向量矩阵代数术语把新手挡在门外,只会点按钮却不懂轨迹矩阵怎么来的,等于蒙眼调参。本文是系列第一篇,先把四个步骤的数学骨架讲透,不堆公式套话。
◍ 用 SSA 把价格序列拆成可交易的成分
一维奇异谱分析(SSA)在 MT5 里常被当成去趋势和降噪的工具。它把一条价格序列投影到若干正交分量上,再按特征值大小把分量分成趋势、周期和噪声三类,交易者可以只重建前几类来过滤杂波。 在 MT5 的统计分析实践中,窗口长度 L 通常取序列长度 N 的一半左右;若 N=256,L=128 时前 3 个主分量往往已能解释 70% 以上的方差,剩下的大概率是高频噪声。 外汇与贵金属杠杆高、跳空频繁,SSA 重建信号只代表历史结构倾向,实盘仍可能失效,开 MT5 用自带时间序列跑一遍分解再决定参数更稳妥。
「为什么 SSA 值得手动拆一遍」
外汇与贵金属价格序列普遍呈现高波动、非平稳特征,传统滤波手段常因强假设而失真。奇异谱分析(SSA)基于线性代数,不要求序列平稳,能把一段报价拆成趋势、周期与噪声三层结构,对 MT5 历史数据复盘有直接可用性。 但 SSA 文献里铺满向量与矩阵代数,没受过对应训练的实盘者很容易把它当黑箱,只调库不调参。本文系列用可验证的方式把原理落地,让你能在 MT5 里自己跑分解、看成分。 SSA 不是单一算法,而是一族方法,核心都绕不开四步:把序列排成 Hankel 轨迹矩阵;对该矩阵做秩一初等矩阵分解;按结构对初等矩阵分组;重建子序列。下一节我们逐阶段拆开看。
把价格序列折成轨迹矩阵
想把一段一维报价里藏着的连续依赖挖出来,第一步是把序列折成矩阵。拿长度 N 的时间序列,用长度 L 的滑动窗口去截,能切出 K = N − L + 1 个列向量,每个向量长 L,像 {x1,x2,…,xL}、{x2,x3,…,xL+1} 这样依次排,就拼成了轨迹矩阵 X。 窗口长度 L 直接决定分析深度。经验上取 L = N/2 最常用,L 太小看不出结构,太大又会让列数 K 锐减、矩阵过瘦。 在 MT5 里用历史收盘价跑一遍就能验证:换不同 L,矩阵的奇异值分布会明显变化,这也意味着后续分解能抓到的周期成分不一样。外汇与贵金属波动高、跳空多,实盘前务必用历史数据回测确认参数敏感区间。
◍ 把轨迹矩阵拆成秩1基块的实操视角
构造好轨迹矩阵 X 之后,基础奇异谱分析(Basic-SSA)用奇异值分解把它拆开。SVD 产出特征三元组 (√λi, Ui, Vi):σi=√λi 是奇异值,等于 XX' 特征值的平方根;Ui 为左奇异向量,Vi 为右奇异向量,i 的个数就是 X 的秩。 奇异值的大小直接表示成分权重——值大对应趋势或周期这类主导模式,值小大概率是噪声。于是 X 能写成若干个秩为 1 的初等矩阵 Xi 之和,这些秩 1 矩阵就是拼出复杂结构的底料。 确定性序列(指数、正弦、多项式)满足线性递推,轨迹矩阵线性无关向量数有限:指数序列秩为 1,正弦秩为 2,k 次多项式秩为 k+1。实际金价或欧元美元报价混了噪声,是满秩序列,秩 = min(L, K);但若本质是无噪确定性信号加噪声,SSA 就能靠秩 1 基矩阵重组出信号、丢掉噪声再做预测。 下面这段 MT5 脚本直接验证了秩的结论,跑完终端会打印三种序列的秩。注意外汇贵金属报价受噪声主导,满秩是常态,高杠杆下误判信号风险极高,验证时只用历史数据别挂实盘。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Rank.mq5 | class=class="str">"cmt">//| Eugene | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Eugene" class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#class="kw">property script_show_inputs input class="type">int N = class="num">100; class=class="str">"cmt">// N - length of generated time series input class="type">int L = class="num">30; class=class="str">"cmt">// L - window length input class="type">int T = class="num">22; class=class="str">"cmt">// T - period length of sine function class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Script program start function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { matrix X=matrix::Zeros(L,N-L+class="num">1); vector x_exp= vector::Zeros(N); vector x_sinus= vector::Zeros(N); vector x_polynom= vector::Zeros(N); for(class="type">int t=class="num">0; t <N; t++) { x_exp[t] = MathPow(class="num">1.01,t); class=class="str">"cmt">// class="num">1. Exponential sequence: x_t = class="num">1.01^t x_sinus[t] = MathSin(class="num">2*M_PI*t/T); class=class="str">"cmt">// class="num">2. Sine wave: x_t = sin(class="num">2 * pi * t / T) x_polynom[t] = class="num">1 + t+ MathPow(t,class="num">2); class=class="str">"cmt">// class="num">3. Polynomial of degree class="num">2: x_t = class="num">1 + t + t^class="num">2 } trajectory_matrix(x_exp,L,X); Print("Rank Exponential sequence = ",Rank_SVD(X)); trajectory_matrix(x_sinus,L,X); Print("Rank Sinus sequence = ",Rank_SVD(X)); trajectory_matrix(x_polynom,L,X); Print("Rank Polynom sequence = ",Rank_SVD(X)); } class=class="str">"cmt">//+------------------------------------------------------------------+
「用轨迹矩阵和SVD给序列定秩」
做奇异谱分析(SSA)的第一步,是把一维价格序列铺成轨迹矩阵。下面这段把长度为 N 的 series 按窗口 L 切成 L 行、K=N-L+1 列的矩阵 X,每个元素 X[i,j] 直接取 series[i+j],相当于把原序列的所有长度为 L 的片段竖着排开。
class=class="str">"cmt">//| Trajectory matrix X | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void trajectory_matrix(vector & series,class="type">int window_length, matrix & X) { class="type">int N_ = (class="type">int)series.Size(); class="type">int L_ = window_length; class="type">int K = N_ - L_ + class="num">1; X=matrix::Zeros(L_,K); for(class="type">int i=class="num">0; i <L_; i++) { for(class="type">int j=class="num">0; j <K; j++) { X[i,j] = series[i+j]; } } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|Finds the rank of a matrix class="kw">using SVD | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int Rank_SVD(matrix & X) { vector sv; matrix U,V; class="type">class="kw">double tol = class="num">1e-8; class=class="str">"cmt">// Threshold for non-zero values X.SingularValueDecompositionDC(SVDZ_N,sv,U,V); class="type">class="kw">double threshold = tol * sv.Max(); class="type">int rank=class="num">0; for(class="type">int i=class="num">0; i<(class="type">int)sv.Size(); i++) { if(sv[i] > threshold) rank++; } class="kw">return rank; } class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Trajectory matrix X | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void trajectory_matrix(vector & series,class="type">int window_length, matrix & X) { class="type">int N_ = (class="type">int)series.Size(); class="type">int L_ = window_length; class="type">int K = N_ - L_ + class="num">1; X=matrix::Zeros(L_,K); for(class="type">int i=class="num">0; i <L_; i++) { for(class="type">int j=class="num">0; j <K; j++) { X[i,j] = series[i+j]; } } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|Finds the rank of a matrix class="kw">using SVD | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int Rank_SVD(matrix & X) { vector sv; matrix U,V; class="type">class="kw">double tol = class="num">1e-8; class=class="str">"cmt">// Threshold for non-zero values X.SingularValueDecompositionDC(SVDZ_N,sv,U,V); class="type">class="kw">double threshold = tol * sv.Max(); class="type">int rank=class="num">0; for(class="type">int i=class="num">0; i<(class="type">int)sv.Size(); i++) { if(sv[i] > threshold) rank++; } class="kw">return rank; } class=class="str">"cmt">//+------------------------------------------------------------------+
用奇异向量形态切分时序成分
分组阶段要把秩为1的基矩阵归到互不相交的集合里,每个集合对应一类时序成分:趋势、季节或噪声。最实用的判据是看轨迹矩阵 X 的奇异值是否接近——相近的归一组,因为同源成分在能量谱上会聚堆。 具体拆分时常见做法是:I_trend 单独吃掉最大那根奇异值;I_seasonal 取 {2,3} 这种成对索引(周期信号秩恒为2);I_noise 收编从4到 i 的剩余项,i 等于总奇异值数。这样 X 就被显式拆成趋势+季节+噪声的并。 肉眼看左奇异向量比数特征根更快。趋势项画出来是平滑单调轨迹;周期项必出成对正弦状向量;和理想高斯白噪声长得很像、且挂在很小奇异值上的,就是噪声。MT5 里把 SSA 的 U_i 向量导出来画线,基本一眼能定分组,不用死磕阈值。 这种视觉分组对外汇小时线尤其敏感:贵金属夜盘常冒出伪周期,单看数值易误归趋势,描一遍向量形态就能避开。