预测时间序列(第 2 部分):最小二乘支持向量机(LS-SVM)·进阶篇
(2/3)·从 EMD 过渡到 LS-SVM,用线性方程组替代非线性优化,预测精度由 gamma 说了算
接上篇对经验模式分解的铺垫,我们继续深挖时间序列预测的另一条路径。很多交易者把 SVM 当成黑箱直接调参,却没意识到 LS-SVM 把非线性问题折成了线性方程组——这一步省下的算力,足够你在 MT5 上实时跑多个品种。
核矩阵装配与最小二乘求解链路
LS-SVM 的训练核心是先拼出 (KernelNumber+1)×(KernelNumber+1) 的增广矩阵,再把目标向量 Y 塞进 B 数组,最后交给 RMatrixSolveLS 出解。下面这段是矩阵填充与方程求解的直接实现,Threshold 作为正则阈值参与最小二乘过程。 [CODE] CMatrixDouble MATRIX(KernelNumber + 1, KernelNumber + 1); for(int i = 1; i <= KernelNumber; i++) { for(int j = 1; j <= KernelNumber; j++) { MATRIX[j].Set(i, Omega[(i - 1) * KernelNumber + (j - 1)]); } } MATRIX[0].Set(0, 0); for(int i = 1; i <= KernelNumber; i++) { MATRIX[i].Set(0, 1); MATRIX[0].Set(i, 1); } double B[]; ArrayResize(B, KernelNumber + 1); B[0] = 0; for(int j = 1; j <= KernelNumber; j++) { B[j] = Y[j - 1]; } int info; CDenseSolverLSReport rep; double x[]; CDenseSolver::RMatrixSolveLS(MATRIX, KernelNumber + 1, KernelNumber + 1, B, Threshold, info, rep, x); Beta = x[0]; ArrayResize(Alpha, KernelNumber); ArrayCopy(Alpha, x, 0, 1); return true; } [/CODE] 逐行看:前两层循环把 Omega 里的核值按 (i-1)*KernelNumber+(j-1) 线性索引搬进 MATRIX 的 [1..K][1..K] 区;随后第 0 行第 0 列填偏置项结构,边界全置 1 对应偏置求解。B 数组首位补 0,其余直接取 Y 的历史样本,RMatrixSolveLS 返回的 x 中 x[0] 是 Beta 偏置,x[1..K] 拷贝为 Alpha 权重。 process() 里按 KernelNumber 分流:等于 -1 或大于样本数走普通线性回归 regress();等于 0 或样本数走标准 LS-SVM 的 buildOmega();否则进 SOM-LS-SVM 的 buildKernels()。外汇与贵金属行情下用这套做拟合,样本外表现可能漂移,属高风险验证。 拟合优度不是只看 RMSE。checkAll() 在归一化后(均值视为 0)同时算 RMSE[0]、CC[0] 相关系数和 R2[0],PCT[0] 统计 given*trained>0 的方向一致率;例如 VectorNumber 个样本里 correct 计数乘 100.0/VectorNumber 即得百分比。开 MT5 把这段接进 EA,先打印 RMSE[0] 与 PCT[0] 就能快速判断当前核配置是否过拟合。
CMatrixDouble MATRIX(KernelNumber + class="num">1, KernelNumber + class="num">1); for(class="type">int i = class="num">1; i <= KernelNumber; i++) { for(class="type">int j = class="num">1; j <= KernelNumber; j++) { MATRIX[j].Set(i, Omega[(i - class="num">1) * KernelNumber + (j - class="num">1)]); } } MATRIX[class="num">0].Set(class="num">0, class="num">0); for(class="type">int i = class="num">1; i <= KernelNumber; i++) { MATRIX[i].Set(class="num">0, class="num">1); MATRIX[class="num">0].Set(i, class="num">1); } class="type">class="kw">double B[]; ArrayResize(B, KernelNumber + class="num">1); B[class="num">0] = class="num">0; for(class="type">int j = class="num">1; j <= KernelNumber; j++) { B[j] = Y[j - class="num">1]; } class="type">int info; CDenseSolverLSReport rep; class="type">class="kw">double x[]; CDenseSolver::RMatrixSolveLS(MATRIX, KernelNumber + class="num">1, KernelNumber + class="num">1, B, Threshold, info, rep, x); Beta = x[class="num">0]; ArrayResize(Alpha, KernelNumber); ArrayCopy(Alpha, x, class="num">0, class="num">1); class="kw">return true; }
「交叉验证与回归求解的绑定实现」
下面这段 MQL5 把 LSSVM 模型的外部校验器绑进来,并在样本上跑交叉验证指标。bindCrossValidator 只在校验器向量维度等于当前 VectorSize 时才接管对象,否则直接返回 false,避免维度错配导致后续矩阵越界。 crossvalidate 函数逐向量调用 approximate 算预测值 z,同步累计 RMSE、R2、相关系数 CC 与方向正确率 PCT。其中 result.RMSE[1] 先按误差平方和累加,最后除以 vectorNumber 再开根号;result.PCT[1] 用 _Y[i]*z>0 统计同号样本,乘以 100.0/vectorNumber 得到百分比。 regress 用 CMatrixDouble 拼出 (VectorNumber × (VectorSize+1)) 的增广矩阵,最后一列塞入目标 Y,前面列按 X[j*VectorSize+i] 铺输入。CLRBuildZ 建最小二乘模型,info 不等于 1 就弹 Alert 并返回 false;成功则用 LRUnpack 取出系数到 Solution,并打印 RMSE 与系数数组。 在 MT5 里把 VectorSize、VectorNumber 换成你的 K 线窗口与样本数,直接挂 EA 调用 regress,能从终端日志看到 Solution 数组,据此判断模型系数是否过拟合。外汇与贵金属杠杆高,模型回测指标仅代表历史样本表现,实盘仍可能失效。
class="type">bool bindCrossValidator(LSSVM *tester) { if(tester.getVectorSize() == VectorSize) { crossvalidator = tester; class="kw">return true; } class="kw">return class="kw">false; } class="type">void crossvalidate(LSSVM_Error &result) { class="kw">const class="type">int vectorNumber = crossvalidator.getVectorNumber(); class="type">class="kw">double out[]; class="type">class="kw">double _Y[]; crossvalidator.getY(_Y); class=class="str">"cmt">// assumed normalized by validator class="type">class="kw">double xy = class="num">0; class="type">class="kw">double x2 = class="num">0; class="type">class="kw">double y2 = class="num">0; class="type">int correct = class="num">0; for(class="type">int i = class="num">0; i < vectorNumber; i++) { crossvalidator.vector(i, out); class="type">class="kw">double z = approximate(out); result.RMSE[class="num">1] += (_Y[i] - z) * (_Y[i] - z); xy += (_Y[i]) * (z); x2 += (_Y[i]) * (_Y[i]); y2 += (z) * (z); if(_Y[i] * z > class="num">0) correct++; } result.R2[class="num">1] = class="num">1 - result.RMSE[class="num">1] / x2; result.RMSE[class="num">1] = sqrt(result.RMSE[class="num">1] / vectorNumber); result.CC[class="num">1] = xy / sqrt(x2 * y2); result.PCT[class="num">1] = correct * class="num">100.0 / vectorNumber; } class="type">bool regress(class="type">void) { CMatrixDouble MATRIX(VectorNumber, VectorSize + class="num">1); class=class="str">"cmt">// +class="num">1 stands for b column for(class="type">int i = class="num">0; i < VectorNumber; i++) { MATRIX[i].Set(VectorSize, Y[i]); } for(class="type">int i = class="num">0; i < VectorSize; i++) { for(class="type">int j = class="num">0; j < VectorNumber; j++) { MATRIX[j].Set(i, X[j * VectorSize + i]); } } CLinearModel LM; CLRReport AR; class="type">int info; CLinReg::LRBuildZ(MATRIX, VectorNumber, VectorSize, info, LM, AR); if(info != class="num">1) { Alert("Error in regression model!"); class="kw">return class="kw">false; } class="type">int _size; CLinReg::LRUnpack(LM, Solution, _size); Print("RMSE=" + (class="type">class="kw">string)AR.m_rmserror); ArrayPrint(Solution); class="kw">return true; }
◍ LS-SVM 预测指标的微分阶数权衡
SOMLSSVM.mq5 在独立窗口跑 4 个缓冲区:前两个画训练集的原始与预测,后两个画测试集。它只计算一次,演示用途下不每根柱刷新,真要高频重算改 OnCalculate 里的 calculated 标志即可,但成本会明显上升。 微分阶数(DifferencingOrder)是这套回归的核心旋钮。阶数越高,序列平稳性越好,对导数本身的预测品质会上去;但高阶差值的微小误差会在还原公式里被放大成整段偏移。一阶还原只用 y[i+1]=y[i]+d1[i],三阶却要 y[i+3]=3*y[i+2]-3*y[i+1]+y[i]+d3[i],前置依赖越来越多。 EURUSD D1 上跑过三组实例:训练 250 向量、验证 60 向量、向量大小 20,Gamma=2048、Sigma=8。微分阶 1/2/3 的验证相关系数分别是 -0.055、0.429、0.749,匹配增量 45%、58%、72%。肉眼看高阶线重合更好,但把三阶预测恢复回主图价格,满屏跳动点;完全关微分反而价格贴近真实,只是有约 1 根柱滞后——本质等同于 N 向量移动平均的数字滤波。 别把高阶当圣经 高阶微分在差值空间好看,恢复成价格就露怯。实盘外汇、贵金属波动剧烈、高风险,建议先用 DifferencingOrder=1 或 2 在 MT5 里加载指标看滞后幅度,再决定是否做多步超前预测来吃掉那 1~2 根柱延迟。
class="kw">input class="type">int _VectorNumber = class="num">250; class=class="str">"cmt">// VectorNumber(training) class="kw">input class="type">int _VectorNumber2 = class="num">50; class=class="str">"cmt">// VectorNumber(validating) class="kw">input class="type">int _VectorSize = class="num">20; class=class="str">"cmt">// VectorSize class="kw">input class="type">class="kw">double _Gamma = class="num">0; class=class="str">"cmt">// Gamma(class="num">0 - auto) class="kw">input class="type">class="kw">double _Sigma = class="num">0; class=class="str">"cmt">// Sigma(class="num">0 - auto) class="kw">input class="type">int _KernelNumber = class="num">0; class=class="str">"cmt">// KernelNumber(class="num">0 - auto) class="kw">input class="type">int _TrainingOffset = class="num">50; class=class="str">"cmt">// Offset of training bars class="kw">input class="type">int _ValidationOffset = class="num">0; class=class="str">"cmt">// Offset of validation bars class="kw">input class="type">int DifferencingOrder = class="num">1; LSSVM *lssvm = NULL; LSSVM *test = NULL; class="type">int OnInit() { class="kw">static class="type">class="kw">string titles[BUF_NUM] = {"Training set", "Trained output", "Test class="kw">input", "Test output"}; for(class="type">int i = class="num">0; i < BUF_NUM; i++) { PlotIndexSetInteger(i, PLOT_DRAW_TYPE, DRAW_LINE); PlotIndexSetString(i, PLOT_LABEL, titles[i]); } lssvm = new LSSVM(_VectorNumber, _VectorSize, _KernelNumber, _Gamma, _Sigma, _TrainingOffset); test = new LSSVM(_VectorNumber2, _VectorSize, _KernelNumber, class="num">1, class="num">1, _ValidationOffset); lssvm.setDifferencingOrder(DifferencingOrder); test.setDifferencingOrder(DifferencingOrder); class="kw">return INIT_SUCCEEDED; } class="type">int OnCalculate(class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated, class="kw">const class="type">class="kw">datetime& Time[], class="kw">const class="type">class="kw">double& Open[], class="kw">const class="type">class="kw">double& High[], class="kw">const class="type">class="kw">double& Low[], class="kw">const class="type">class="kw">double& Close[], class="kw">const class="type">long& Tick_volume[], class="kw">const class="type">long& Volume[], class="kw">const class="type">int& Spread[]) { ArraySetAsSeries(Open, true); ArraySetAsSeries(Time, true); class="kw">static class="type">bool calculated = class="kw">false; if(calculated) class="kw">return rates_total; calculated = true; for(class="type">int k = class="num">0; k < BUF_NUM; k++) { buffers[k].empty(); } lssvm.bindCrossValidator(test); class="type">bool processed = lssvm.process(true); if(processed) {
把标准化残差还原成可视预测点
LSSVM 训练与验证阶段都做了 Z-score 标准化,所以模型吐出来的 out[] 是零均值单位方差的缩放值,必须乘回各自的 stdDev 再加 mean 才能对应真实价格量纲。训练集用 m1、s1 还原后写进 buffers[0] 和 buffers[1],验证集则用 m2、s2 还原,二者统计参数分开存,避免样本分布漂移互相污染。 验证循环里把 test 向量送进 lssvm.approximate,得到 z 后同样用 s2、m2 反标准化,再按 _ValidationOffset 倒序填进 buffers[3],这一步直接决定图上预测线是否和右侧未知区间对齐。若错位,先查 _VectorNumber2 - i - 1 的索引方向。 DifferencingOrder 控制预测目标还原方式:0 阶直接用 z;1 阶加前一根 Open;2 阶用 2*Open[t+1]-Open[t+2]+z;3 阶展开成 3*Open[t+1]-3*Open[t+2]+Open[t+3]+z。外汇与贵金属波动剧烈,高阶差分对噪声敏感,实盘前请在 MT5 用历史数据核对各阶还原误差。 ShowPredictionOnChart 开启后,用 OBJ_TEXT 在对应 Bar 时间挂 Wingdings 字符“l”标红点,锚点居中。想快速验证,把这段贴进指标 OnCalculate 末尾,调 _ValidationOffset 看标记是否落在验证窗口。
class="kw">const class="type">class="kw">double m1 = lssvm.getMean(); class="kw">const class="type">class="kw">double s1 = lssvm.getStdDev(); class="kw">const class="type">class="kw">double m2 = test.getMean(); class="kw">const class="type">class="kw">double s2 = test.getStdDev(); class=class="str">"cmt">// training class="type">class="kw">double out[]; lssvm.getY(out, true); for(class="type">int i = class="num">0; i < _VectorNumber; i++) { out[i] = out[i] * s1 + m1; } buffers[class="num">0].set(_TrainingOffset, out); lssvm.getResult(out, true); for(class="type">int i = class="num">0; i < _VectorNumber; i++) { out[i] = out[i] * s1 + m1; } buffers[class="num">1].set(_TrainingOffset, out); class=class="str">"cmt">// validation test.getY(out, true); for(class="type">int i = class="num">0; i < _VectorNumber2; i++) { out[i] = out[i] * s2 + m2; } buffers[class="num">2].set(_ValidationOffset, out); for(class="type">int i = class="num">0; i < _VectorNumber2; i++) { test.vector(i, out); class="type">class="kw">double z = lssvm.approximate(out); z = z * s2 + m2; buffers[class="num">3][_VectorNumber2 - i - class="num">1 + _ValidationOffset] = z; ... } } if(ShowPredictionOnChart) { class="type">class="kw">double target = class="num">0; if(DifferencingOrder == class="num">0) { target = z; } else if(DifferencingOrder == class="num">1) { target = Open[_VectorNumber2 - i - class="num">1 + _ValidationOffset + class="num">1] + z; } else if(DifferencingOrder == class="num">2) { target = class="num">2 * Open[_VectorNumber2 - i - class="num">1 + _ValidationOffset + class="num">1] - Open[_VectorNumber2 - i - class="num">1 + _ValidationOffset + class="num">2] + z; } else if(DifferencingOrder == class="num">3) { target = class="num">3 * Open[_VectorNumber2 - i - class="num">1 + _ValidationOffset + class="num">1] - class="num">3 * Open[_VectorNumber2 - i - class="num">1 + _ValidationOffset + class="num">2] + Open[_VectorNumber2 - i - class="num">1 + _ValidationOffset + class="num">3] + z; } else { class=class="str">"cmt">// unsupported yet } class="type">class="kw">string name = prefix + (class="type">class="kw">string)i; ObjectCreate(class="num">0, name, OBJ_TEXT, class="num">0, Time[_VectorNumber2 - i - class="num">1 + _ValidationOffset], target); ObjectSetString(class="num">0, name, OBJPROP_TEXT, "l"); ObjectSetString(class="num">0, name, OBJPROP_FONT, "Wingdings"); ObjectSetInteger(class="num">0, name, OBJPROP_ANCHOR, ANCHOR_CENTER); ObjectSetInteger(class="num">0, name, OBJPROP_COLOR, clrRed); }