预测时间序列(第 2 部分):最小二乘支持向量机(LS-SVM)·进阶篇
📈

预测时间序列(第 2 部分):最小二乘支持向量机(LS-SVM)·进阶篇

(2/3)·从 EMD 过渡到 LS-SVM,用线性方程组替代非线性优化,预测精度由 gamma 说了算

案例拆解新手友好 第 2/3 篇

接上篇对经验模式分解的铺垫,我们继续深挖时间序列预测的另一条路径。很多交易者把 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] 就能快速判断当前核配置是否过拟合。

MQL5 / C++
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 数组,据此判断模型系数是否过拟合。外汇与贵金属杠杆高,模型回测指标仅代表历史样本表现,实盘仍可能失效。

MQL5 / C++
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 根柱延迟。

MQL5 / C++
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 看标记是否落在验证窗口。

MQL5 / C++
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);
}
交给小布盯盘看盘口
这些 LS-SVM 的预测诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到回归超平面偏离度,你专注判断 gamma 该调大还是调小。

常见问题

普通 SVM 求解的是非线性凸优化问题,而 LS-SVM 把问题改写为线性方程组,用最小二乘损失替代铰链损失,计算更快且易于在 MQL5 中实现。
sigma 控制高斯核的色散宽度,值偏小容易过拟合噪声,偏大则平滑掉有效波动;外汇贵金属属高风险品种,建议用历史样本做交叉验证再定。
资料复杂性不允许兼顾所有维度,仅用 p 个前置价格点已能构造 p 维预测向量,外部变量留待后续模型扩展,避免初期过拟合。
小布盯盘内置了回归偏差冗余的实时诊断,会基于近期波动特征提示 gamma 倾向调大或调小的区间,但最终参数仍需交易者结合品种风险自行确认。
每取 p 个连续前置点组成一个向量 x,对应 t 时刻真实值 y 作为标签,滑动窗口扫过序列即得一组 (x, y) 样本,用于求解系数 w。