数据科学与机器学习(第 15 部分):SVM,每个交易员工具箱中的必备工具·综合运用
(3/3)· 把前两篇的 SVM 理论转成 MT5 能跑的线性与对偶模型,含 Python 训练与 ONNX 导出全流程
SVM 回测日志里藏着什么
在 MT5 策略测试器里跑 EURUSD H1 的 SVM 分类脚本,日志第一行就吐出了训练精度:Train accuracy = 0.516000,也就是刚过随机猜的 51.6%,外汇品种上这类线性核很容易卡在五成出头。 混淆矩阵直接打印成 [[79,74],[68,79]],意思是类别 0 真值 153 笔里判对 79 错 74,类别 1 真值 147 笔里判对 79 错 68,两类支持度接近但错分都偏多。 报告区还给了宏平均:Average 与 W Avg 的 Precision / Recall / F1 全停在 0.52,总 Support 700 笔。EURUSD 受宏观扰动大,SVM 单模型在此精度下直接跟单属高风险,建议先拿小布盯盘做样本外复核再谈仓位。
◍ SVM 分类器在 EURUSD H1 上的实测命中率
把双 SVM 模型以 ONNX 形式跑在 EURUSD 的 H1 周期上,日志里打印的测试准确率只有 0.527000,也就是约 52.7%。这个数值贴近随机猜边(50%),说明直接用默认预处理喂数据,模型在外汇小时线分类上几乎没有信息增益。 日志中 NN 行给出 -1.0 类对应的精确率 0.54、召回率 0.52、F1 0.53、支持数 153;PQ 行给出 1.0 类精确率 0.52、召回率 0.54、F1 0.53、支持数 147。两类样本量接近,但指标全卡在 0.53 附近,整体 Accuracy 行也是 0.53。 承载这套推理的是 CDualSVMONNX 类,它内部用 CPreprocessing 做归一化、用 CMatrixutils 做矩阵处理,并通过 LoadONNX 载入模型缓冲、用 Predict 输出类别。外汇与贵金属属高风险品种,这类 53% 命中率倾向不足以单独支撑实盘信号,建议先调归一化或特征再做验证。
class CDualSVMONNX { class="kw">private: CPreprocessing<vectorf, matrixf> *normalize_x; CMatrixutils matrix_utils; class="kw">struct data_struct { class="type">class="kw">ulong rows, cols; } df; class="kw">public: CDualSVMONNX(class="type">void); ~CDualSVMONNX(class="type">void); class="type">long onnx_handle; class="type">void SendDataToONNX(matrixf &data, class="type">class="kw">string csv_name = "DualSVMONNX-data.csv", class="type">class="kw">string csv_header=""); class="type">bool LoadONNX(class="kw">const class="type">uchar &onnx_buff[], ENUM_ONNX_FLAGS flags=ONNX_NO_CONVERSION); class="type">int Predict(vectorf &inputs);
「预测接口的矩阵输入约定」
在 MT5 的神经网络封装里,Predict 方法只认 matrixf 类型的引用入参,这意味着调用前必须把特征行拼成浮点矩阵,而不是普通数组。 若直接传一维数组会编译报错,实战中常见做法是先用 matrixf::Init(inputs.Rows(), n_features) 定维,再逐行填值,否则预测结果会偏移或返回空向量。 该接口不负责归一化,喂进去的报价差、波动率等字段必须提前缩放到模型训练时的量纲,外汇与贵金属品种波动幅度差异大,缩放遗漏会带来较高回测失真风险。
vector Predict(matrixf &inputs);一次性抓取并留存归一化参数
SVM 这类模型对输入量纲极其敏感,训练时做一次归一化,推理时必须用同一组参数还原,否则预测倾向漂移。把数据收集塞进脚本而不是 EA,是因为训练集只需一次性导出,脚本跑完即退,不占用实时行情线程。 GetDataforONNX.mq5 在 MQL5 文件目录下生成 DualSVMONNX-data.csv,同时把 min_max_scaler 的 min 和 max 分别写成 min_max_scaler.min.csv 与 min_max_scaler.max.csv。只归一化自变量(X),目标列(Y)保持原值,因此写参数文件前要先把表头里最后一列摘掉。 默认输入里 rsi_period=13、bb_period=20、bb_deviation=2.0,bars=1000 意味着抓 1000 根 K 线做样本。外汇与贵金属杠杆高、滑点随机,这套数据仅反映历史片段,实盘复用须重算归一化参数,模型信号失效概率不低。 下面这段是 SendDataToONNX 的原文,重点看末尾保存归一化参数的部分: void CDualSVMONNX::SendDataToONNX(matrixf &data, string csv_name = "DualSVMONNX-data.csv", string csv_header="") { df.cols = data.Cols(); df.rows = data.Rows();
| if (df.cols == 0 | df.rows == 0) |
|---|
{ Print(__FUNCTION__," data matrix invalid size "); return; } matrixf split_x; vectorf split_y; matrix_utils.XandYSplitMatrices(data, split_x, split_y); //since we are going to be normalizing the independent variable only we need to split the data into two normalize_x = new CPreprocessing<vectorf,matrixf>(split_x, NORM_MIN_MAX_SCALER); //Normalizing Independent variable only matrixf new_data = split_x; new_data.Resize(data.Rows(), data.Cols()); new_data.Col(split_y, data.Cols()-1); if (csv_header == "") { for (ulong i=0; i<df.cols; i++) csv_header += "COLUMN "+string(i+1) + (i==df.cols-1 ? "" : ","); //do not put delimiter on the last column } //--- Save the Normalization parameters also matrixf params = {}; string sep=","; ushort u_sep; string result[]; u_sep=StringGetCharacter(sep,0); int k=StringSplit(csv_header,u_sep,result); ArrayRemove(result, k-1, 1); //remove the last column header since we do not have normalization parameters for the target variable as it is not normalized normalize_x.min_max_scaler.min.Swap(params); matrix_utils.WriteCsv("min_max_scaler.min.csv",params,result,false,8); normalize_x.min_max_scaler.max.Swap(params); matrix_utils.WriteCsv("min_max_scaler.max.csv",params,result,false,8); //--- matrix_utils.WriteCsv(csv_name, new_data, csv_header, false, 8); //Save dataset to a csv file } #include <MALE5\Support Vector Machine(SVM)\svm.mqh> CDualSVMONNX dual_svm; input uint bars = 1000; input uint epochs_ = 1000; input uint batch_size_ = 64; input double alpha__ =0.1; input double lambda_ = 0.01; input int rsi_period = 13; input int bb_period = 20; input double bb_deviation = 2.0; int rsi_handle, bb_handle; 逐行拆解:函数开头先记下列数行数,空矩阵直接打印报错退出;XandYSplitMatrices 把数据拆成 X 和 Y,因为只归一化自变量。CPreprocessing 用 NORM_MIN_MAX_SCALER 压缩 X 到 [0,1] 区间。new_data 重组后把 Y 塞回最后一列。表头若为空就自动生成 COLUMN 1..N,注意末列不加逗号。关键在末尾:从 csv_header 拆出的列名数组删掉最后一项(Y 不归一化),再把 scaler 的 min、max 分别 Swap 进 params 并写两个独立 csv。最后才写主数据集文件,精度保留 8 位。 开 MT5 把这段直接贴进脚本,改 bars 和指标周期跑一遍,去文件目录核对四个 csv 是否齐全——缺了 min/max 文件,后面加载 ONNX 做预测时大概率对不上。
class="type">void CDualSVMONNX::SendDataToONNX(matrixf &data, class="type">class="kw">string csv_name = "DualSVMONNX-data.csv", class="type">class="kw">string csv_header="") { df.cols = data.Cols(); df.rows = data.Rows(); if (df.cols == class="num">0 || df.rows == class="num">0) { Print(__FUNCTION__," data matrix invalid size "); class="kw">return; } matrixf split_x; vectorf split_y; matrix_utils.XandYSplitMatrices(data, split_x, split_y); class=class="str">"cmt">//since we are going to be normalizing the independent variable only we need to split the data into two normalize_x = new CPreprocessing<vectorf,matrixf>(split_x, NORM_MIN_MAX_SCALER); class=class="str">"cmt">//Normalizing Independent variable only matrixf new_data = split_x; new_data.Resize(data.Rows(), data.Cols()); new_data.Col(split_y, data.Cols()-class="num">1); if (csv_header == "") { for (class="type">class="kw">ulong i=class="num">0; i<df.cols; i++) csv_header += "COLUMN "+class="type">class="kw">string(i+class="num">1) + (i==df.cols-class="num">1 ? "" : ","); class=class="str">"cmt">//do not put delimiter on the last column } class=class="str">"cmt">//--- Save the Normalization parameters also matrixf params = {}; class="type">class="kw">string sep=","; class="type">class="kw">ushort u_sep; class="type">class="kw">string result[]; u_sep=StringGetCharacter(sep,class="num">0); class="type">int k=StringSplit(csv_header,u_sep,result); ArrayRemove(result, k-class="num">1, class="num">1); class=class="str">"cmt">//remove the last column header since we do not have normalization parameters for the target variable as it is not normalized normalize_x.min_max_scaler.min.Swap(params); matrix_utils.WriteCsv("min_max_scaler.min.csv",params,result,class="kw">false,class="num">8); normalize_x.min_max_scaler.max.Swap(params); matrix_utils.WriteCsv("min_max_scaler.max.csv",params,result,class="kw">false,class="num">8); class=class="str">"cmt">//--- matrix_utils.WriteCsv(csv_name, new_data, csv_header, class="kw">false, class="num">8); class=class="str">"cmt">//Save dataset to a csv file } class="macro">#include <MALE5\Support Vector Machine(SVM)\svm.mqh> CDualSVMONNX dual_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="kw">input class="type">int rsi_period = class="num">13; class="kw">input class="type">int bb_period = class="num">20; class="kw">input class="type">class="kw">double bb_deviation = class="num">2.0; class="type">int rsi_handle, bb_handle;
◍ 把 RSI 与布林带喂给 ONNX 模型
脚本启动后第一件事是拿指标句柄:iRSI 取当前品种当前周期的收盘价 RSI,iBands 取同周期的布林带(中轨、上轨、下轨共用 bb_period 与 bb_deviation)。这两组句柄后面直接进数据装配函数,不在 OnStart 里做计算。 GetTrainTestData 用模板写死列数为 5:第 0 列抄 RSI 缓冲,第 1~3 列分别是布林带上中下轨,第 4 列是标签——收盘大于开盘记 1,否则记 -1,相当于把每根 K 的阴阳当二分类目标。bars 是外部变量,决定喂多少根历史去训练测试。 数据矩阵拼好后,dual_svm.SendDataToONNX 会把它导出成 DualSVMONNX-data.csv,表头写死为 rsi,bb-high,bb-low,bb-mid,target,你可以直接在 MT5 跑完去沙盒目录翻这个文件核对。 归一化参数也要落盘:min_max_scaler 的 min 和 max 各自 Swap 进 params,再分别写 min_max_scaler.min.csv 与 min_max_scaler.max.csv,精度 8 位。外汇与贵金属波动大,缩放边界不固化到文件,下次换品种重训容易串味。
class=class="str">"cmt">//| Script program start function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { rsi_handle = iRSI(Symbol(),PERIOD_CURRENT,rsi_period, PRICE_CLOSE); bb_handle = iBands(Symbol(), PERIOD_CURRENT, bb_period,class="num">0, bb_deviation, PRICE_CLOSE); class=class="str">"cmt">//--- matrixf data = GetTrainTestData<class="type">class="kw">float>(); dual_svm.SendDataToONNX(data,"DualSVMONNX-data.csv","rsi,bb-high,bb-low,bb-mid,target"); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Getting data for Training and Testing the model | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">template <class="kw">typename T> matrix<T> GetTrainTestData() { matrix<T> data(bars, class="num">5); vector<T> v; class=class="str">"cmt">//Temporary vector for storing Inidcator buffers v.CopyIndicatorBuffer(rsi_handle, class="num">0, class="num">0, bars); data.Col(v, class="num">0); v.CopyIndicatorBuffer(bb_handle, class="num">0, class="num">0, bars); data.Col(v, class="num">1); v.CopyIndicatorBuffer(bb_handle, class="num">1, class="num">0, bars); data.Col(v, class="num">2); v.CopyIndicatorBuffer(bb_handle, class="num">2, class="num">0, bars); data.Col(v, class="num">3); vector open, close; 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<v.Size(); i++) class=class="str">"cmt">//preparing the independent variable data[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 class="kw">return data; } normalize_x.min_max_scaler.min.Swap(params); matrix_utils.WriteCsv("min_max_scaler.min.csv",params,result,class="kw">false,class="num">8); normalize_x.min_max_scaler.max.Swap(params); matrix_utils.WriteCsv("min_max_scaler.max.csv",params,result,class="kw">false,class="num">8);
「DualSVMONNX 初始化时在做什么」
把训练集喂给 DualSVMONNX 类,第一步是读 CSV 并把 target 列剥离成独立标签向量,其余字段转成 float32 矩阵。注意这里用了 np.random.seed(42) 和 random_state=42,意味着同一份数据跑两次切分结果完全一致,方便你在 MT5 外复现回测。 切分比例写死 test_size=0.2,也就是 80% 训练、20% 验证;SVM 本体用的是 SVC(kernel='rbf', C=1.0),RBF 核配默认惩罚系数,对小样本非线性边界比较友好,但外汇小时线噪声大时容易过拟合,倾向先调 C 再看泛化。 初始化只负责备料和建空模型,onnx_model_name 先记成 "DualSVMONNX",真正导出 ONNX 是后面训练完才触发。开 Python 把这段贴进类里,换你自己的 EURUSD_M1.csv 跑一下,能直接 print(X_train.shape) 确认维度没错位。
class DualSVMONNX: def __init__(self, dataset, c=class="num">1.0, kernel=&class="macro">#x27;rbf&class="macro">#x27;): data = pd.read_csv(dataset) # reading a csv file np.random.seed(class="num">42) self.X = data.drop(columns=[&class="macro">#x27;target&class="macro">#x27;]).astype(np.float32) # dropping the target column from independent variable self.y = data["target"].astype(class="type">int) # storing the target variable in its own vector self.X = self.X.to_numpy() self.y = self.y.to_numpy() # Split the data into training and testing sets self.X_train, self.X_test, self.y_train, self.y_test = train_test_split(self.X, self.y, test_size=class="num">0.2, random_state=class="num">42) self.onnx_model_name = "DualSVMONNX" class="macro">#our final onnx model file name for saving purposes really # Create a dual SVM model with a kernel self.svm_model = SVC(kernel=kernel, C=c)
对偶 SVM 训练与交叉验证实测
把对偶 SVM 在训练集上拟合后,直接打印训练准确率,得到 63% 左右。这个数字只能说明模型对当前样本的分类能力处于中等水平,谈不上强。 为了排除单次切分的偶然性,用 5 折交叉验证跑了一遍,平均准确率落在 59.875%。和单次训练的 63.3% 差距不大,说明模型在不同参数切分下表现稳定,方向没有跑偏。 外汇与贵金属行情受宏观事件驱动,这类中等准确率模型直接用于实盘信号概率偏低,仅适合作为辅助过滤层。 下面这段是训练与验证的核心调用,逐行看逻辑:fit 用训练集喂模型;predict 拿训练集自身预测;accuracy_score 算训练准确率;cross_val_score 做 5 折交叉验证;np.mean 取验证均值。
def fit(self): self.svm_model.fit(self.X_train, self.y_train) # fitting/training the model y_preds = self.svm_model.predict(self.X_train) print("accuracy = ",accuracy_score(self.y_train, y_preds)) scores = cross_val_score(self.svm_model, self.X_train, self.y_train, cv=class="num">5) mean_cv_accuracy = np.mean(scores) print(f"\nscores {scores} mean_cv_accuracy {mean_cv_accuracy}")
◍ 把 sklearn 的 SVC 塞进 MT5 跑起来
在 MetaEditor 里打开 ONNX 文件,最先看输入参数段:float_input 标明浮点输入,张量形态 (?, 4) 表示行数不定、列数固定为 4,对应训练时的 4 个自变量。输出段有两个节点,一个吐 INT64 标签(-1 或 1),一个吐浮点概率矩阵(?×2),所以 OnnxRun 之后要分两次取输出,输入则只需喂一次 1×4 向量。 LoadONNX 函数本身不携带归一化参数,而 Min Max Scaler 的 max/min 必须和新数据维度对齐,否则推理会偏。策略测试器里从 CSV 动态读参有时不灵,稳妥做法是把归一化上下限直接写进 EA 代码,让 LoadONNX 显式接收这两个向量。 我们在 svm test.mq5 的 OnInit 里复跑训练集,得到的准确度和 Python 端一致,都是 63%——这证明两边走的是同一条推理路径。预测函数只做一件事:跑 ONNX 拿标签,返回整数。 策略极简:SVM 判类 == 1 开买,== -1 开卖。线性 SVM 与对偶 SVM 在测试器里对比,除 svm_type 外输入全同,对偶版表现偏弱,可能源于 ONNX 导出时未收敛,这块留待后续拆。外汇与贵金属杠杆高,此类信号仅作概率参考,实盘前务必在策略测试器验证。
def saveONNX(self): initial_type = [(&class="macro">#x27;float_input&class="macro">#x27;, FloatTensorType(shape=[None, class="num">4]))] # None means we don&class="macro">#x27;t know the rows but we know the columns for sure, Remember !! we have class="num">4 independent variables onnx_model = convert_sklearn(self.svm_model, initial_types=initial_type) # Convert the scikit-learn model to ONNX format onnx.save_model(onnx_model, dataset_path + f"\{self.onnx_model_name}.onnx") class="macro">#saving the onnx model class="type">long outputs_0[] = {class="num">1}; if (!OnnxSetOutputShape(onnx_handle, class="num">0, outputs_0)) class=class="str">"cmt">//giving the onnx handle first node output shape { Print(__FUNCTION__," Failed to set the output shape Err=",GetLastError()); class="kw">return class="kw">false; } class="type">long outputs_1[] = {class="num">1,class="num">2}; if (!OnnxSetOutputShape(onnx_handle, class="num">1, outputs_1)) class=class="str">"cmt">//giving the onnx handle second node output shape { Print(__FUNCTION__," Failed to set the output shape Err=",GetLastError()); class="kw">return class="kw">false; } class="kw">const class="type">long inputs[] = {class="num">1,class="num">4}; if (!OnnxSetInputShape(onnx_handle, class="num">0, inputs)) class=class="str">"cmt">//Giving the Onnx handle the class="kw">input shape { Print(__FUNCTION__," Failed to set the class="kw">input shape Err=",GetLastError()); class="kw">return class="kw">false; } class="type">bool CDualSVMONNX::LoadONNX(class="kw">const class="type">uchar &onnx_buff[], ENUM_ONNX_FLAGS flags=ONNX_NO_CONVERSION) { onnx_handle = OnnxCreateFromBuffer(onnx_buff, flags); class=class="str">"cmt">//creating onnx handle buffer if (onnx_handle == INVALID_HANDLE) { Print(__FUNCTION__," OnnxCreateFromBuffer Error = ",GetLastError()); class="kw">return class="kw">false; } class=class="str">"cmt">//--- class="kw">const class="type">long inputs[] = {class="num">1,class="num">4}; if (!OnnxSetInputShape(onnx_handle, class="num">0, inputs)) class=class="str">"cmt">//Giving the Onnx handle the class="kw">input shape { Print(__FUNCTION__," Failed to set the class="kw">input shape Err=",GetLastError()); class="kw">return class="kw">false; } class="type">long outputs_0[] = {class="num">1};
「给 ONNX 双 SVM 喂对输出维度与归一化边界」
加载 ONNX 模型时,输出节点的 shape 必须显式声明,否则推理句柄拿不到张量布局。第一个输出节点用 outputs_0 设形状,失败就打印错误码并退出;第二个节点用 {1,2} 的 long 数组给 outputs_1,两步任一失败整个 LoadONNX 返回 false。 归一化参数直接写死在代码里:min_v 四个分量分别是 14.32424641、1.04674852、1.04799891、1.04392886,max_v 对应 86.28263092、1.07385755、1.07907069、1.07267821。这四个维度大概率是 RSI 与布林带相关特征的缩放边界,外汇与贵金属行情跳空多,越界值可能让模型输出失真。 OnInit 里先抓 iRSI 和 iBands 句柄,再把 datasetf 按 0.8 比例、随机种子 42 切分训练测试集。dual_svm.LoadONNX 吃进模型字节流、默认 flag 和转成 float 的 max/min 向量;随后 x_trainf、x_testf 直接进 Predict,得到 train_preds 与 test_preds。 开 MT5 把这段 min/max 换成你品种的真实分位边界,再跑 Predict,否则跨周期推理准确率可能掉一截。
if (!OnnxSetOutputShape(onnx_handle, class="num">0, outputs_0)) class=class="str">"cmt">//giving the onnx handle first node output shape { Print(__FUNCTION__," Failed to set the output shape Err=",GetLastError()); class="kw">return class="kw">false; } class="type">long outputs_1[] = {class="num">1,class="num">2}; if (!OnnxSetOutputShape(onnx_handle, class="num">1, outputs_1)) class=class="str">"cmt">//giving the onnx handle second node output shape { Print(__FUNCTION__," Failed to set the output shape Err=",GetLastError()); class="kw">return class="kw">false; } class="kw">return true; } class="type">bool CDualSVMONNX::LoadONNX(class="kw">const class="type">uchar &onnx_buff[], ENUM_ONNX_FLAGS flags, vectorf &norm_max, vectorf &norm_min) normalize_x = new CPreprocessing<vectorf,matrixf>(norm_max, norm_min); class=class="str">"cmt">//Load min max scaler with parameters vector min_v = {class="num">14.32424641,class="num">1.04674852,class="num">1.04799891,class="num">1.04392886}; vector max_v = {class="num">86.28263092,class="num">1.07385755,class="num">1.07907069,class="num">1.07267821}; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- rsi_handle = iRSI(Symbol(),PERIOD_CURRENT, rsi_period, PRICE_CLOSE); bb_handle = iBands(Symbol(), PERIOD_CURRENT, bb_period, class="num">0 , bb_deviation, PRICE_CLOSE); vector y_train, y_test; class=class="str">"cmt">// class="type">class="kw">float values matrixf datasetf = GetTrainTestData<class="type">class="kw">float>(); matrixf x_trainf, x_testf; vectorf y_trainf, y_testf; class=class="str">"cmt">//--- matrix_utils.TrainTestSplitMatrices(datasetf,x_trainf,y_trainf,x_testf,y_testf,class="num">0.8,class="num">42); class=class="str">"cmt">//split the data into training and testing samples vectorf max_vf = {}, min_vf = {}; class=class="str">"cmt">//convertin the parameters into class="type">class="kw">float type max_vf.Assign(max_v); min_vf.Assign(min_v); dual_svm.LoadONNX(SVMModel, ONNX_DEFAULT, max_vf, min_vf); y_train.Assign(y_trainf); y_test.Assign(y_testf); vector train_preds = dual_svm.Predict(x_trainf); vector test_preds = dual_svm.Predict(x_testf);
EURUSD 小时图上的 SVM 分类回测实况
在 MT5 里跑完 SVM 分类器初始化后,直接把训练集和测试集的混淆矩阵打进日志,能看清模型到底在哪类样本上犯浑。下面这段就是在 OnInit 末尾打印双报告的调用,跑的是 EURUSD 的 H1 周期。 [CODE] Print("\n<<<<< Train Classification Report >>>>>\n"); metrics.confusion_matrix(y_train, train_preds); Print("\n<<<<< Test Classification Report >>>>>\n"); metrics.confusion_matrix(y_test, test_preds); return(INIT_SUCCEEDED); [/CODE] 逐行拆:第1行向日志输出训练集报告的分隔标识;第2行调用 metrics 对象的 confusion_matrix,用 y_train 真值和 train_preds 预测值算训练集混淆矩阵;第3行输出测试集报告标识;第4行对 y_test 与 test_preds 算测试集混淆矩阵;第5行返回 INIT_SUCCEEDED 结束初始化。 实盘日志里训练集混淆矩阵是 [[245,148],[150,257]],总样本 800,类1召回 0.62、类-1召回 0.63,整体准确率 0.63。测试集缩到 200 样本,矩阵 [[61,31],[40,68]],类1精确 0.60、类-1精确 0.69,准确率同样卡在 0.63 附近。 外汇和贵金属属高风险品种,H1 上 63% 的二分类准确率只说明模型略好于抛硬币,过拟合不明显但边际信号弱。真要上实盘,建议把核函数 gamma 调小一档,看测试集召回是否还能撑住再决定。
Print("\n<<<<< Train Classification Report >>>>>\n"); metrics.confusion_matrix(y_train, train_preds); Print("\n<<<<< Test Classification Report >>>>>\n"); metrics.confusion_matrix(y_test, test_preds); class="kw">return(INIT_SUCCEEDED);
◍ SVM 回测输出与预测函数拆解
上面这段日志是 EURUSD H1 上跑 SVM 测试时终端打印的原始结果。GJ 行 label -1.0、概率分布 0.60/0.66/0.63/0.63,PO 行 label 1.0、分布 0.69/0.63/0.66/0.66,两组样本加权后 Accuracy 落在 0.65,W Avg 的 F1 也压在 0.65,说明分类边界在样本内偏中性,实盘外推时胜率可能只在六成出头,外汇高杠杆下仍属高风险。 DD 行没有数值,代表该折交叉验证的某些中间量未回显;LH 与 CJ 的 Average / W Avg 样本量都是 200.0,即整套测试用了 200 条标准化后的特征向量。打开 MT5 把品种切到 EURUSD、周期 H1,用同一份 ONNX 模型重跑,大概率会复现 0.65 附近的准确率。 Predict 函数先把外部传入的 inputs 复制给 temp_inputs,再走 normalize_x.Normalization 做特征归一化——这一步若漏掉,ONNX 推理的输出会漂移。随后调 OnnxRun,失败就 Print 错误码并直接返回 outputs[0] 的强转整型;成功也只返回 outputs[0],意味着分类标签是硬判定,没有把 x_output 里的概率对外暴露。
class="type">int CDualSVMONNX::Predict(vectorf &inputs) { vectorf outputs(class="num">1); class=class="str">"cmt">//label outputs vectorf x_output(class="num">2); class=class="str">"cmt">//probabilities vectorf temp_inputs = inputs; normalize_x.Normalization(temp_inputs); class=class="str">"cmt">//Normalize the class="kw">input features if (!OnnxRun(onnx_handle, ONNX_DEFAULT, temp_inputs, outputs, x_output)) { Print("Failed to get predictions from onnx Err=",GetLastError()); class="kw">return (class="type">int)outputs[class="num">0]; } class="kw">return (class="type">int)outputs[class="num">0]; }
「画得少,看得清」
双 SVM 在样本外平均准确率 63%、线性 SVM 59%,放在如今深度学习当道的语境里不算亮眼,但对 MT5 上数据量有限、算力吃紧的账户而言,它仍是一套能跑通的分类起点。 噪声敏感和计算开销是硬伤:行情毛刺会直接拉低边界质量,大数据集训练在本地终端也容易卡死,所以特征工程别偷懒,先靠 preprocessing.mqh 做规范化和标准化再说。 把 dual_svm.py 和 svm test.mq5 拖进对应目录,用 GetDataforONNX.mq5 落一盘真实 tick 的 csv,你就能复现这两组数字。外汇与贵金属杠杆高、跳空频繁,SVM 信号只该当工具箱里的一块,别拿它替你压上全部仓位。