数据科学与机器学习(第 15 部分):SVM,每个交易员工具箱中的必备工具·进阶篇
📈

数据科学与机器学习(第 15 部分):SVM,每个交易员工具箱中的必备工具·进阶篇

(2/3)· 从软间隔到 ONNX 导出,手把手把线性 SVM 跑通在实盘数据上

进阶 第 2/3 篇
很多交易者把 SVM 当成黑箱,直接调库跑分类,却忽略了软间隔和支撑向量的含义,结果在噪声行情里过拟合。本篇接着第 1 篇的概念,把线性 SVM 的训练与预测落到 MT5 可用的工作流里。

软间隔怎么让 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 跑回测看边界稳定度,外汇品种同样存在样本不可分的高风险。

MQL5 / C++
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 向量是否收敛,比只读公式更直观。

MQL5 / C++
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 曲线是否还在抖动。

MQL5 / C++
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 翻转频率,再决定跟不跟。

MQL5 / C++
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 验证切分结果再说。

MQL5 / C++
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]] 显示两类样本误判接近对称,没有哪边被模型系统性忽略。 外汇和贵金属这类高杠杆品种,模型准确率仅略高于随机,实盘前必须用历史分段验证,风险很高。

MQL5 / C++
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
让小布替你跑这套
这些 SVM 训练与 ONNX 导出的诊断流程,小布盯盘的 AIGC 已内置,打开对应品种页即可看到模型间隔与支撑向量分布,你专注阈值决策。

常见问题

硬间隔要求完全线性可分,实盘几乎不满足;软间隔允许少量误分类并靠惩罚系数平衡,更贴合外汇贵金属的高噪声环境,泛化倾向更稳。
ONNX 提供跨语言轻量推理,避免在 MQL5 侧重写训练逻辑,只需加载模型做预测,降低部署复杂度。
量纲差异会扭曲超平面方向,间隔被大数值特征主导,模型可能对价格绝对值敏感而非形态,样本外表现可能明显退化。
小布盯盘内置了特征规范化和模型间隔可视化,可叠加你导出的 ONNX 线性 SVM 做辅助参考,但外汇贵金属高风险,信号仅作概率倾向。
预测输出为类别或决策函数值,可映射为多空倾向,再用独立过滤条件控手数,避免在低间隔置信区频繁交易。