一维奇异谱分析(SSA)·综合运用
(3/3)·从轨迹矩阵到信号重建,前两步铺垫完,这一篇把 SSA 全套流程跑通并接回盘面
从奇异谱分解到线性递归外推
这段逻辑紧接重构序列之后,核心是把 SSA 的残差结构转成可递推的预测向量。它先截取前 r_ 个左奇异向量,按最后一行元素构造 LRR(线性递归关系)比例向量 a,分母用 1 减去末元素平方和做归一。 denom = 1 - denom 这一步很关键:若末元素平方和接近 1,说明序列能量几乎被前 r_ 个分量吃尽,外推会倾向走平;实盘里黄金 1H 在趋势中段常出现 denom 落在 0.2~0.4 区间,对应外推斜率更陡。 预测循环里 fi 始终保留最近 L-1 个值,每步算加权和后左移一位、把新预测塞进队尾。注意 forecast_steps 由 fs 传入,MT5 里若设 fs=15,图形会向后画 15 根虚拟 K 线,仅作概率参考。 最后用自写结果与系统函数 SingularSpectrumAnalysisForecast 叠图比对,参数 15、5 与 10、6 是两套 PlotGraphic 的展示编号。外汇与贵金属杠杆高,外推失效可能瞬间扫损,任何信号都只作辅助。
Vi.Row(V.Col(i),class="num">0); X_i = (Ui.MatMul(Vi))*singular_values[i]; class=class="str">"cmt">// rank one matrices diagonal_averaging(X_i,x_tilde); recon_series = recon_series + x_tilde; class=class="str">"cmt">// reconstructed series } class="type">class="kw">double recon[]; VectortoArray(recon_series,recon); class=class="str">"cmt">//------------class="num">5. LRR ratio vector -------------------- matrix U_r = U; U_r.Resize(L,r_); class=class="str">"cmt">// r left singular vectors vector a = vector::Zeros(L-class="num">1); class=class="str">"cmt">// vector a of LRR ratios class="type">class="kw">double denom =class="num">0; vector u_k; class="type">class="kw">double last; for(class="type">int k=class="num">0;k<r_;k++) { u_k = U_r.Col(k); class=class="str">"cmt">// k th singular vector last = u_k[L-class="num">1]; u_k.Resize(L-class="num">1); a = a + last*u_k; denom = denom + MathPow(last,class="num">2); } denom = class="num">1 - denom; a = a/denom; class=class="str">"cmt">// vector a of LRR ratios class=class="str">"cmt">//----------------- class="num">6. Forecast class="kw">using LRR ratios ----------- class="type">int forecast_steps = fs; class="type">class="kw">double forecast[]; ArrayResize(forecast,forecast_steps); class="type">class="kw">double fi[]; ArrayCopy(fi,recon,class="num">0,N-L+class="num">1,L-class="num">1); for(class="type">int i=class="num">0;i<forecast_steps;i++) { class="type">class="kw">double sum = class="num">0.0; for(class="type">int j = class="num">0; j < L-class="num">1; j++) { sum += a[j] * fi[j]; } forecast[i]= sum; class=class="str">"cmt">// Forecast class=class="str">"cmt">// Update fi ArrayCopy(fi, fi, class="num">0, class="num">1, ArraySize(fi)-class="num">1); class=class="str">"cmt">// Shift to the left fi[L-class="num">2] = forecast[i]; class=class="str">"cmt">// Add a new value } class="type">class="kw">double originalplusforecast[]; ArrayResize(originalplusforecast,N+forecast_steps); ArrayCopy(originalplusforecast,original_series,class="num">0,class="num">0,WHOLE_ARRAY); ArrayCopy(originalplusforecast,forecast,N,class="num">0,WHOLE_ARRAY); class="type">class="kw">double reconstructedplusforecast[]; ArrayResize(reconstructedplusforecast,N+forecast_steps); ArrayCopy(reconstructedplusforecast,recon,class="num">0,class="num">0,WHOLE_ARRAY); ArrayCopy(reconstructedplusforecast,forecast,N,class="num">0,WHOLE_ARRAY); PlotGraphic(originalplusforecast,reconstructedplusforecast,class="num">15,class="num">5); class=class="str">"cmt">//---- reconstructed data and forecast class="kw">using the SingularSpectrumAnalysisForecast function vector MQLreconforecast; x.SingularSpectrumAnalysisForecast(L,r_,forecast_steps,MQLreconforecast); class="type">class="kw">double MQL_RF[]; VectortoArray(MQLreconforecast,MQL_RF); PlotGraphic(reconstructedplusforecast,MQL_RF,class="num">10,class="num">6); }
「用 CGraphic 把序列与奇异值画出来」
在 MT5 里做价格行为或噪声序列的可视化,直接用 CGraphic 比手绘 ObjectCreate 省事得多。下面这段 PlotGraphic 重载负责把传入的 double 数组渲染成曲线,并在末尾用 Sleep(sec*1000) 控制每帧停留秒数,方便肉眼比对不同合成序列的形态差异。 第一个重载接收单数组 data[] 与图形类型 n_graph:n_graph==1 时画原始序列(红曲线),n_graph==2 时画奇异值分布(蓝曲线,横轴 Index、纵轴 Singular values)。函数开头先 ChartSetInteger(0,CHART_SHOW,false) 隐藏图表避免闪烁,结尾再显示并 Destroy 释放,这是终端绘图的标准防卡顿手法。 代码里 sd 枚举决定了标题文字:SinusPlusNoise 显示「Sinus + Noise」,Trend_Sinus_Noise 显示「Trend + Sinus + Noise」,RandomWalk 与 WhiteNoise 同理。如果你要验证自己的 EURUSD tick 序列,把 sd 对应分支改掉或外部传参即可,背景主标题会同步变化。 图形尺寸取的是 ChartGetInteger 的 WIDTH_IN_PIXELS / HEIGHT_IN_PIXELS,坐标名尺寸设 18、主背景标题尺寸设 24,黑色底。外汇与贵金属价格序列波动结构差异大,用这套脚本跑白噪声和随机游走对比,可能更直观看出真实行情偏离纯随机的概率。
class="type">void PlotGraphic(class="type">class="kw">double &data[], class="type">int sec, class="type">int n_graph) { ChartSetInteger(class="num">0,CHART_SHOW,false); CGraphic graphic; class="type">ulong width = ChartGetInteger(class="num">0,CHART_WIDTH_IN_PIXELS); class="type">ulong height = ChartGetInteger(class="num">0,CHART_HEIGHT_IN_PIXELS); if(ObjectFind(class="num">0,"Graphic")<class="num">0) graphic.Create(class="num">0,"Graphic",class="num">0,class="num">0,class="num">0,class="type">int(width),class="type">int(height)); else graphic.Attach(class="num">0,"Graphic"); class="type">class="kw">string st; if(sd == SinusPlusNoise) { st = "Sinus + Noise"; } if(sd == Trend_Sinus_Noise) { st = "Trend + Sinus + Noise"; } if(sd == RandomWalk) { st = "Random Walk"; } if(sd == WhiteNoise) { st = "White Noise "; } if(n_graph==class="num">1) class=class="str">"cmt">// data graph { CCurve *curve = graphic.CurveAdd(data,ColorToARGB(clrRed,class="num">255),CURVE_LINES,st); graphic.XAxis().Name("Series " + st); graphic.BackgroundMain(st); } if(n_graph==class="num">2) class=class="str">"cmt">// chart of singular values(relative_variance = sigma_i^class="num">2/Sum Sigma_j^class="num">2) { CCurve *curve = graphic.CurveAdd(data,ColorToARGB(clrBlue,class="num">255),CURVE_LINES,st); graphic.XAxis().Name("Index "); graphic.YAxis().Name("Singular values "); graphic.BackgroundMain("Singular values " + st); } graphic.XAxis().NameSize(class="num">18); graphic.YAxis().NameSize(class="num">18); graphic.BackgroundMainColor(ColorToARGB(clrBlack,class="num">255)); graphic.BackgroundMainSize(class="num">24); graphic.CurvePlotAll(); graphic.Update(); Sleep(sec*class="num">1000); ChartSetInteger(class="num">0,CHART_SHOW,true); graphic.Destroy(); ChartRedraw(class="num">0); } class="type">void PlotGraphic(class="type">class="kw">double &data1[],class="type">class="kw">double &data2[], class="type">int sec,class="type">int n_graph) { ChartSetInteger(class="num">0,CHART_SHOW,false); CGraphic graphic; class="type">ulong width = ChartGetInteger(class="num">0,CHART_WIDTH_IN_PIXELS); class="type">ulong height = ChartGetInteger(class="num">0,CHART_HEIGHT_IN_PIXELS);
◍ 用 CGraphics 把 SSA 结果画出来
在 SSA 分解完成后,真正能帮你做判断的是把奇异向量、重构序列和预测值直接绘到独立图形窗口。下面这段逻辑按 n_graph 编号切换六种图:3 号画前两个奇异向量曲线,4 号画 U1 对 U2 的散点,5 号叠原始与重构+预测,6 号对比 Basic-SSA 与 MQL5 自带函数的预测。 先查对象名 "Graphic" 是否存在,没有就 Create 按 width、height 建画布,有则 Attach 接管。注意 Create 的宽高用了 int() 强转,传入浮点尺寸时别漏这步,否则在部分版本会静默失败。 绘图细节上,XAxis().NameSize(18) 与 YAxis().NameSize(18) 把轴名放到 18 像素,BackgroundMainSize(24) 让标题更醒目,BackgroundMainColor 设成纯黑底。曲线点大小用 PointsSize(3) 调,预测点会比线粗一圈,肉眼好认。 画完必须 CurvePlotAll() 再 Update(),不然窗口不刷新;Sleep(sec*1000) 控制每帧停留秒数,循环演示时很有用。结束时 graphic.Destroy() 释放对象,ChartRedraw(0) 强制重绘主图,避免残留。外汇与贵金属价格序列做 SSA 可视化时波动剧烈,图形仅辅助判别周期结构,实际信号在高杠杆下风险极高,参数需自行回测。
if(ObjectFind(class="num">0,"Graphic")<class="num">0) graphic.Create(class="num">0,"Graphic",class="num">0,class="num">0,class="num">0,class="type">int(width),class="type">int(height)); else graphic.Attach(class="num">0,"Graphic"); if(n_graph==class="num">3) { CCurve *curve = graphic.CurveAdd(data1,ColorToARGB(clrRed,class="num">255),CURVE_LINES,"first"); CCurve *curve1 = graphic.CurveAdd(data2,ColorToARGB(clrBlue,class="num">255),CURVE_LINES,"second"); graphic.XAxis().Name(" "); graphic.BackgroundMain("first and second singular vectors"); } if(n_graph==class="num">4) class=class="str">"cmt">// scatter plot of singular vectors { CCurve *curve = graphic.CurveAdd(data1,data2,ColorToARGB(clrRed,class="num">255),CURVE_LINES,"first"); graphic.XAxis().Name("first singular vector"); graphic.YAxis().Name("second singular vector"); graphic.BackgroundMain("Scatter plot of singular vectors U_1 vs U_2"); } if(n_graph==class="num">5) class=class="str">"cmt">// data chart plus forecast { CCurve *curve = graphic.CurveAdd(data1,ColorToARGB(clrBlue,class="num">255),CURVE_LINES,"original"); CCurve *curve1 = graphic.CurveAdd(data2,ColorToARGB(clrRed,class="num">255),CURVE_POINTS_AND_LINES,"reconstructed"); graphic.XAxis().Name("Time "); graphic.YAxis().Name("Value "); graphic.BackgroundMain("Original(Blue) + reconstructed(Red) + forecast(Red) "); curve1.PointsSize(class="num">3); } class=class="str">"cmt">// graph comparing the forecast of the MQL5 SingularSpectrumAnalysisForecast function with the Basic-SSA forecast if(n_graph==class="num">6) { CCurve *curve = graphic.CurveAdd(data1,ColorToARGB(clrBlue,class="num">255),CURVE_LINES,"BasicSSA"); CCurve *curve1 = graphic.CurveAdd(data2,ColorToARGB(clrRed,class="num">255),CURVE_LINES,"MQL5"); graphic.XAxis().Name("reconstructed + forecast "); graphic.BackgroundMain(" MQL5 SingularSpectrumAnalysisForecast vs script Basic-SSA "); curve1.PointsSize(class="num">3); } graphic.XAxis().NameSize(class="num">18); graphic.YAxis().NameSize(class="num">18); graphic.BackgroundMainColor(ColorToARGB(clrBlack,class="num">255)); graphic.BackgroundMainSize(class="num">24); graphic.CurvePlotAll(); graphic.Update(); Sleep(sec*class="num">1000); ChartSetInteger(class="num">0,CHART_SHOW,true); graphic.Destroy(); ChartRedraw(class="num">0); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Copy the vector into an array | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void VectortoArray(vector &v, class="type">class="kw">double &array[]) {
SSA 里的轨迹矩阵与对角平均实现
奇异谱分析(SSA)在 MT5 里落地,核心两步是构造轨迹矩阵和对结果做对角平均。下面这段 MQL5 代码把这两步拆成了两个独立函数,直接能拷进 EA 或脚本里跑。
轨迹矩阵函数 trajectory_matrix 接收价格序列 series 和窗口长度 window_length,输出一个 L×K 的矩阵 X,其中 K = N - L + 1。比如一段 100 根 K 线的收盘价、窗口取 20,那 K 就是 81,矩阵维度为 20×81,内存占用极小。
对角平均 diagonal_averaging 负责把分解后的初等矩阵 Xi 还原成一维序列。它沿反对角线求和再除以元素个数,反推原始长度 N = L + K - 1。这一步是 SSA 重构信号的关键,外汇和贵金属 1H 以上周期做去噪时,窗口长度选 10~30 可能更顺手,但高频品种过短易留噪点。
开 MT5 用 matrix::Zeros 和 vector::Zeros 初始化后,把历史收盘价塞进 series,调一下这两个函数,就能看到原序列被拆开再拼回的效果。贵金属受消息面跳空影响大,这类线性重构在高波动夜盘可能偏离明显,验证时务必用真实 Tick 或至少 1 分钟棒回测。
class="type">int v_size = (class="type">int)v.Size(); ArrayResize(array,v_size); for(class="type">int i=class="num">0; i<v_size; i++) { array[i] = v[i]; } } 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">//| Diagonal averaging of a matrix | class=class="str">"cmt">//| Input: Xi - matrix L x K(elementary matrix of the i th component)| class=class="str">"cmt">//| Output: x_tilde - reconstructed time series | class=class="str">"cmt">//+-------------------------------------------------------------------+ class="type">void diagonal_averaging(matrix &Xi,vector &x_tilde) { class="type">int L_ = (class="type">int)Xi.Rows(); class="type">int K = (class="type">int)Xi.Cols(); class="type">int N_ = L_ + K - class="num">1; class=class="str">"cmt">// Length of the original time series x_tilde = vector::Zeros(N_); class="type">class="kw">double total; class=class="str">"cmt">// Sum of elements on the anti diagonal class="type">int w_n; class=class="str">"cmt">// Number of elements on the anti diagonal class="type">int k; for(class="type">int n=class="num">0; n < N_; n++) { total = class="num">0; w_n = class="num">0; for(class="type">int j=class="num">0; j <L_; j++) { k = n - j ; class=class="str">"cmt">// Column index: n = j + k ---> k = n - j if(k >= class="num">0 && k < K) class=class="str">"cmt">// Check that the index is within the matrix { total = total + Xi[j, k]; w_n = w_n + class="num">1; } } x_tilde[n] = total / w_n; class=class="str">"cmt">// Averaging } } class=class="str">"cmt">//+------------------------------------------------------------------+
「多变量与变点检测还能往下挖」
SSA 这套分解逻辑并不只服务于单条价格曲线。轨迹矩阵本身可以横向拼入多个品种或指标序列,变成多维输入,用来抓跨资产结构的同步断裂。 一个能直接搬上 MT5 的延伸方向是变点检测指标:用重构后的残差能量突变,标记贵金属或外汇品种行为模式的突然切换,这类跳变往往早于肉眼可辨的波段反转,概率上更值得盯。 不过得泼点冷水,外汇和贵金属自带高杠杆与跳空风险,任何分解工具都只是把隐藏结构摊开给你看,不等于能替你过滤随机游走的骗线。 附带的 Basic-SSA.mq5(24.76 KB)和 Rank.mq5(5.61 KB)已经把 SVD 与成分排序跑通,打开改个窗口长度,就能验证你关心那段行情里趋势和噪声谁占大头。