数据科学与机器学习(第 15 部分):SVM,每个交易员工具箱中的必备工具·进阶篇
(2/3)· 从软间隔到 ONNX 导出,手把手把线性 SVM 跑通在实盘数据上
软间隔怎么让 SVM 容错
标准 SVM 假设样本能完全分开,但实盘里的外汇分钟线、贵金属 tick 序列常常纠缠在一起,硬切一条边界会过拟合。引入铰链损失后,模型允许个别点跨过边界,代价写进损失函数:第 i 个样本目标 t_i 取 1 或 −1,输出为 f(x_i),类别正确且间隔够大时损失为 0,否则损失等于间隔与该点越界距离之和。 整体目标写成最小化 ∑max(0, 1−t_i·f(x_i)) + λ·‖w‖²,其中 λ 控制边界宽度和误分容忍的权衡。λ 调得太低,正则项几乎消失,式子退化成硬间隔——边界逼死每一个点。 在 MT5 里写线性 SVM 分类器时,硬间隔分支靠符号函数落地。下面这段就是返回实数符号的实现,复制进 CLinearSVM 类就能直接用。 别把 λ 当摆设 贵金属跨周期信号里,λ 设 0 容易在震荡段疯狂反转,建议先拿 0.01~0.1 跑回测看边界稳定度,外汇品种同样存在样本不可分的高风险。
class="type">int CLinearSVM::sign(class="type">class="kw">double var) { if (var == class="num">0) class="kw">return (class="num">0); else if (var < class="num">0) class="kw">return -class="num">1; else class="kw">return class="num">1; }
「线性SVM怎么在MT5里迭代出超平面」
线性支持向量机的核心不是黑箱,而是用铰链损失加L2正则去逼一个最大间隔超平面。权重 w 的更新分两块:当样本满足 y_i(x_i·w−b)≥1,说明落在间隔外且分对,梯度为0不调;否则梯度带 −y_i x_i,再叠 2λw 的正则项,用学习率 α 步进。截距 b 只受铰链损失影响,条件不满足时按 −α y_i 修正,正则项对它没导数。 松弛变量 ξ 给边界留了容错,条件 y_i(x_i·w−b)≥1−ξ_i 允许少数点进间隔甚至错分,实盘里这能避免模型对噪声过度敏感。下文的 fit 方法把这套逻辑落到了批次训练上,样本数必须 ≥ batch_size 否则直接退出,且对 X 做了标准化(NORM_STANDARDIZATION),不标准化时特征量纲差几个数量级会让间隔计算失真。 外汇与贵金属波动有跳空和异方差,直接拿裸价训练SVM可能过拟合历史片段,属高风险尝试,建议先用标准化后的波动率特征验证。把代码挂到MT5脚本里,调 config.epochs 和 λ 看 loss 向量是否收敛,比只读公式更直观。
class="type">void CLinearSVM::fit(matrix &x, vector &y) { matrix X = x; vector Y = y; class="type">class="kw">ulong rows = X.Rows(), cols = X.Cols(); if (X.Rows() != Y.Size()) { Print("Support vector machine Failed | FATAL | X m_rows not same as yvector size"); class="kw">return; } W.Resize(cols); B = class="num">0; normalize_x = new CPreprocessing<vector, matrix>(X, NORM_STANDARDIZATION); class=class="str">"cmt">//Normalizing independent variables class=class="str">"cmt">//--- if (rows < config.batch_size) { Print("The number of samples/rows in the dataset should be less than the batch size"); class="kw">return; } matrix temp_x; vector temp_y; matrix w, b; vector preds = {}; vector loss(config.epochs); during_training = true; for (class="type">uint epoch=class="num">0; epoch<config.epochs; epoch++) { for (class="type">uint batch=class="num">0; batch<=(class="type">uint)MathFloor(rows/config.batch_size); batch+=config.batch_size) { temp_x = matrix_utils.Get(X, batch, (config.batch_size+batch)-class="num">1); temp_y = matrix_utils.Get(Y, batch, (config.batch_size+batch)-class="num">1); class="macro">#ifdef DEBUG_MODE: Print("X\n",temp_x,"\ny\n",temp_y); class="macro">#endif for (class="type">uint sample=class="num">0; sample<temp_x.Rows(); sample++) { class=class="str">"cmt">// yixiw-b≥class="num">1 if (temp_y[sample] * hyperplane(temp_x.Row(sample)) >= class="num">1) {
◍ 带 L2 惩罚的逻辑回归权重更新细节
这段训练循环里,权重 W 的更新分了两种情况:当样本预测正确(落在该分支)时,只做 L2 正则项的梯度下降,即 w = w - α*(2λw);预测错误时则补上误分类项的梯度 - y_i x_i,变成 w = w - α*(2λw - y_i x_i),偏置 b 同步减去 α*y_i。
每个 epoch 跑完,代码会调用 predict(X) 拿到预测值,用 LOSS_BCE 计算二分类交叉熵并存入 loss[epoch],同时打印准确率。开 MT5 把 DEBUG_MODE 打开,能直接看到每轮收敛后的 W 和 B 数值,用来判断正则强度 lambda 是否过大导致权重过早被压平。
外汇与贵金属行情噪声高,这类线性分类器单独用作信号过滤时误判概率不低,实盘前建议用历史 tick 数据跑至少 200 个 epoch 观察 loss 曲线是否还在抖动。
this.W -= config.alpha * (class="num">2 * config.lambda * this.W); class=class="str">"cmt">// w = w + α* (2λw - yixi) } else { this.W -= config.alpha * (class="num">2 * config.lambda * this.W - ( temp_x.Row(sample) * temp_y[sample] )); class=class="str">"cmt">// w = w + α* (2λw - yixi) this.B -= config.alpha * temp_y[sample]; class=class="str">"cmt">// b = b - α* (yi) } } } class=class="str">"cmt">//--- Print the loss at the end of an epoch is_fitted_already = true; preds = this.predict(X); loss[epoch] = preds.Loss(Y, LOSS_BCE); printf("---> epoch [%d/%d] Loss = %f Accuracy = %f",epoch+class="num">1,config.epochs,loss[epoch],metrics.confusion_matrix(Y, preds, class="kw">false)); class="macro">#ifdef DEBUG_MODE: Print("W\n",W," B = ",B); class="macro">#endif } during_training = class="kw">false; class="kw">return; }
让 SVM 吐出买卖方向的最后一步
线性支持向量模型训练完,只是拿到了那条分割超平面;真要落地到交易信号,得把新数据丢进超平面算输出,再套一层 sign 函数把连续值压成 +1 / -1 两类。 下面这段是 MT5 里 predict 方法的原生实现,重点看两处:没训练就调用会直接 Print 报错并返回 1000,这个返回值在调用层要当异常拦掉;另外预测时只要不在训练态,就会对输入向量做归一化,和 fit 时的尺度必须一致,否则信号会漂。 [CODE] int CLinearSVM::predict(vector &x) { if (!is_fitted_already) { Print("Err | The model is not trained, call the fit method to train the model before you can use it"); return 1000; } vector temp_x = x; if (!during_training) normalize_x.Normalization(temp_x); //Normalize a new input data when we are not running the model in training return sign(hyperplane(temp_x)); } [/CODE] 逐行拆:第 1 行定义 predict 接收引用型向量 x;2–7 行是未训练守卫,返回 1000 是个哨兵值不是有效预测;9 行拷贝输入避免改原数据;10–11 行非训练态下归一化,注释写明了只对「新输入」做;末行先算超平面输出再 sign,得到类别标签。 外汇和贵金属波动大、滑点高,这类二分类信号只代表模型倾向,实盘前用历史 tick 跑一遍看 sign 翻转频率,再决定跟不跟。
class="type">int CLinearSVM::predict(vector &x) { if (!is_fitted_already) { Print("Err | The model is not trained, call the fit method to train the model before you can use it"); class="kw">return class="num">1000; } vector temp_x = x; if (!during_training) normalize_x.Normalization(temp_x); class=class="str">"cmt">//Normalize a new class="kw">input data when we are not running the model in training class="kw">return sign(hyperplane(temp_x)); }
「用 RSI 与布林带构造 SVM 训练集」
实盘前先拿历史样本跑一遍线性 SVM,比直接挂预测逻辑更稳妥。这里取 1000 根 K 线,独立变量锁定四个:RSI、布林带上轨、下轨、中轨,因变量用当根收盘对开盘的方向(涨为 1,跌为 -1)标注。 代码里先 include SVM 头文件并 new 一个 CLinearSVM,批次 64、学习率 0.1、正则 0.01、训练轮数 1000,这些参数都能在 input 里改。OnInit 只做实例化和 train_once 置否,不在这里跑训练。 四个指标缓冲分别 Copy 进 dataset 的 0~3 列,第 4 列由 close>open 的三元式填好多空标签。最后 TrainTestSplitMatrices 按 0.7 比例切训练集、随机种子 42,留下 300 根做测试。外汇与贵金属杠杆高,模型在历史样本上的区分度不代表未来胜率,开 MT5 把这段贴进 EA 验证切分结果再说。
class="macro">#include <MALE5\Support Vector Machine(SVM)\svm.mqh> CLinearSVM *svm; class="kw">input class="type">uint bars = class="num">1000; class="kw">input class="type">uint epochs_ = class="num">1000; class="kw">input class="type">uint batch_size_ = class="num">64; class="kw">input class="type">class="kw">double alpha__ =class="num">0.1; class="kw">input class="type">class="kw">double lambda_ = class="num">0.01; class="type">bool train_once; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- svm = new CLinearSVM(batch_size_, alpha__, epochs_, lambda_); train_once = class="kw">false; class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } vec_.CopyIndicatorBuffer(rsi_handle, class="num">0, class="num">0, bars); dataset.Col(vec_, class="num">0); vec_.CopyIndicatorBuffer(bb_handle, class="num">0, class="num">0, bars); dataset.Col(vec_, class="num">1); vec_.CopyIndicatorBuffer(bb_handle, class="num">1, class="num">0, bars); dataset.Col(vec_, class="num">2); vec_.CopyIndicatorBuffer(bb_handle, class="num">2, class="num">0, bars); dataset.Col(vec_, class="num">3); open.CopyRates(Symbol(), PERIOD_CURRENT, COPY_RATES_OPEN, class="num">0, bars); close.CopyRates(Symbol(), PERIOD_CURRENT, COPY_RATES_CLOSE, class="num">0, bars); for (class="type">class="kw">ulong i=class="num">0; i<vec_.Size(); i++) class=class="str">"cmt">//preparing the independent variable dataset[i][class="num">4] = close[i] > open[i] ? class="num">1 : -class="num">1; class=class="str">"cmt">// if price closed above its opening thats bullish else bearish matrix_utils.TrainTestSplitMatrices(dataset,train_x,train_y,test_x,test_y,class="num">0.7,class="num">42); class=class="str">"cmt">//split the data into training and testing samples
◍ 从线性 SVM 切到对偶 SVM 的实操岔路
线性模型在 EURUSD H1 上跑完 1000 轮训练,测试集准确率停在 51.6%,样本外预测准确率约 53%。这个水平谈不上好,但作为线性边界模型算中规中矩,亏损和收敛慢可能来自规范化没调好、收敛阈值设宽了,也可能单纯是价格序列对线性假设太复杂。 想再往上探,直接换对偶 SVM 更现实。但在 MT5 里手写的 SVM 库,无论是速度还是精度都追不上 Python sklearn 的对偶实现,所以更划算的做法是用 MQL5 做数据收集和归一化,把模型训练和推理丢给 Python 的 ONNX 格式。 具体落点是:在 svm.mqh 里加一个 CDualSVMONNX 类,专门负责加载 Python 导出的 ONNX 模型并对接 MT5 的行情接口。下面这段是训练与验证的输出骨架,日志来自 EURUSD H1 的回测终端: svm.fit 之后,前几轮 Loss 从 7.539 降到 7.499,准确率在 48.9%–49.4% 晃;到第 998–1000 轮 Loss 落到 6.77 附近,准确率反而回落到 51.6%,说明过拟合倾向已经出现。混淆矩阵 [[171,175],[164,190]] 显示两类样本误判接近对称,没有哪边被模型系统性忽略。 外汇和贵金属这类高杠杆品种,模型准确率仅略高于随机,实盘前必须用历史分段验证,风险很高。
svm.fit(train_x, train_y); class="num">0 class="num">15:class="num">15:class="num">42.394 svm test(EURUSD,H1) ---> epoch [class="num">1/class="num">1000] Loss = class="num">7.539322 Accuracy = class="num">0.489000 IK class="num">0 class="num">15:class="num">15:class="num">42.395 svm test(EURUSD,H1) ---> epoch [class="num">2/class="num">1000] Loss = class="num">7.499849 Accuracy = class="num">0.491000 EG class="num">0 class="num">15:class="num">15:class="num">42.395 svm test(EURUSD,H1) ---> epoch [class="num">3/class="num">1000] Loss = class="num">7.499849 Accuracy = class="num">0.494000 .... .... GG class="num">0 class="num">15:class="num">15:class="num">42.537 svm test(EURUSD,H1) ---> epoch [class="num">998/class="num">1000] Loss = class="num">6.907756 Accuracy = class="num">0.523000 DS class="num">0 class="num">15:class="num">15:class="num">42.537 svm test(EURUSD,H1) ---> epoch [class="num">999/class="num">1000] Loss = class="num">7.006438 Accuracy = class="num">0.521000 IM class="num">0 class="num">15:class="num">15:class="num">42.537 svm test(EURUSD,H1) ---> epoch [class="num">1000/class="num">1000] Loss = class="num">6.769601 Accuracy = class="num">0.516000 vector train_pred = svm.predict(train_x), test_pred = svm.predict(test_x); printf("Train accuracy = %f",metrics.confusion_matrix(train_y, train_pred, true)); printf("Test accuracy = %f ",metrics.confusion_matrix(test_y, test_pred, true)); CH class="num">0 class="num">15:class="num">15:class="num">42.538 svm test(EURUSD,H1) Confusion Matrix IQ class="num">0 class="num">15:class="num">15:class="num">42.538 svm test(EURUSD,H1) [[class="num">171,class="num">175] HE class="num">0 class="num">15:class="num">15:class="num">42.538 svm test(EURUSD,H1) [class="num">164,class="num">190]] DQ class="num">0 class="num">15:class="num">15:class="num">42.538 svm test(EURUSD,H1) NO class="num">0 class="num">15:class="num">15:class="num">42.538 svm test(EURUSD,H1) Classification Report JD class="num">0 class="num">15:class="num">15:class="num">42.538 svm test(EURUSD,H1) LO class="num">0 class="num">15:class="num">15:class="num">42.538 svm test(EURUSD,H1) _ Precision Recall Specificity F1 score Support